From 54092c2f19de779b4b8e153f571d66a30b33ad45 Mon Sep 17 00:00:00 2001 From: John Rassa Date: Wed, 20 Nov 2024 07:44:26 -0500 Subject: [PATCH] feat: Replace express with fastify Fastify allows for unified schema definition that can drive TypeScript types, swagger docs, and request/response validation. --- config/default.js | 8 +- package-lock.json | 2818 +++++++++++------ package.json | 39 +- src/app/common/csv-stream.service.ts | 2 +- src/app/common/errors.ts | 4 +- src/app/common/express/auth-middleware.ts | 93 - src/app/common/express/error-handlers.ts | 106 - .../common/sockets/base-socket.provider.ts | 88 +- .../access-checker.components.yml | 9 - .../access-checker.controller.ts | 122 +- .../access-checker/access-checker.routes.ts | 53 - src/app/core/audit/audit.controller.ts | 163 +- src/app/core/audit/audit.routes.ts | 19 - src/app/core/audit/audit.service.ts | 7 +- src/app/core/config/config.controller.ts | 57 +- src/app/core/config/config.routes.ts | 10 - ...troller.spec.ts => config.service.spec.ts} | 8 +- src/app/core/config/config.service.ts | 34 + src/app/core/core.components.yml | 83 - src/app/core/core.schemas.ts | 35 + src/app/core/email/email.service.spec.ts | 10 +- src/app/core/email/email.service.ts | 3 +- .../core/export/export-config.controller.ts | 166 +- src/app/core/export/export-config.routes.ts | 21 - src/app/core/export/export-config.schemas.ts | 20 - src/app/core/feedback/feedback.components.yml | 108 - .../core/feedback/feedback.controller.spec.ts | 151 +- src/app/core/feedback/feedback.controller.ts | 345 +- src/app/core/feedback/feedback.routes.ts | 166 - src/app/core/feedback/feedback.schemas.ts | 44 - src/app/core/feedback/feedback.service.ts | 3 +- src/app/core/messages/message.controller.ts | 335 +- src/app/core/messages/message.socket.ts | 33 +- src/app/core/messages/messages.routes.ts | 29 - src/app/core/metrics/metrics.controller.ts | 22 +- src/app/core/metrics/metrics.routes.ts | 10 - .../notifications/notification.controller.ts | 36 +- .../core/notifications/notification.routes.ts | 10 - .../core/notifications/notification.socket.ts | 4 +- src/app/core/teams/team-auth.middleware.ts | 29 + src/app/core/teams/team.components.yml | 265 -- src/app/core/teams/team.model.ts | 7 +- src/app/core/teams/teams.controller.spec.ts | 410 --- src/app/core/teams/teams.controller.ts | 686 ++-- src/app/core/teams/teams.routes.ts | 248 -- src/app/core/teams/teams.schemas.ts | 49 - src/app/core/teams/teams.service.spec.ts | 50 +- src/app/core/teams/teams.service.ts | 23 +- .../user/admin/user-admin.controller.spec.ts | 246 -- .../core/user/admin/user-admin.controller.ts | 522 +-- src/app/core/user/admin/user-admin.routes.ts | 57 - src/app/core/user/auth/auth.controller.ts | 182 ++ src/app/core/user/auth/auth.middleware.ts | 140 + .../user-authentication.controller.spec.ts | 743 ----- .../auth/user-authentication.controller.ts | 98 - .../user/auth/user-authentication.service.ts | 139 +- .../user/auth/user-authenticiation.routes.ts | 74 - .../user/auth/user-password.controller.ts | 77 - .../core/user/auth/user-password.service.ts | 43 +- src/app/core/user/eua/eua.controller.spec.ts | 294 -- src/app/core/user/eua/eua.controller.ts | 319 +- src/app/core/user/eua/eua.middleware.ts | 22 + src/app/core/user/eua/eua.routes.ts | 32 - .../core/user/inactive/inactive-user.job.ts | 1 - src/app/core/user/user-auth.middleware.ts | 176 - src/app/core/user/user-email.service.ts | 4 - src/app/core/user/user.components.yml | 72 - src/app/core/user/user.controller.spec.ts | 338 -- src/app/core/user/user.controller.ts | 336 +- src/app/core/user/user.routes.ts | 44 - src/app/site/test/test.controller.ts | 11 + src/app/site/test/test.routes.ts | 10 - src/lib/express.spec.ts | 57 - src/lib/express.ts | 318 -- src/lib/fastify.ts | 203 ++ src/lib/passport.ts | 28 +- src/lib/socket.io.ts | 2 +- src/lib/strategies/local.ts | 9 +- src/lib/winston.ts | 16 +- src/server.ts | 2 +- src/spec/fastify.ts | 24 + src/spec/helpers.ts | 12 - src/startup.ts | 16 +- src/type-extensions.d.ts | 18 + 84 files changed, 4786 insertions(+), 6940 deletions(-) delete mode 100644 src/app/common/express/auth-middleware.ts delete mode 100644 src/app/common/express/error-handlers.ts delete mode 100644 src/app/core/access-checker/access-checker.components.yml delete mode 100644 src/app/core/access-checker/access-checker.routes.ts delete mode 100644 src/app/core/audit/audit.routes.ts delete mode 100644 src/app/core/config/config.routes.ts rename src/app/core/config/{config.controller.spec.ts => config.service.spec.ts} (66%) create mode 100644 src/app/core/config/config.service.ts delete mode 100644 src/app/core/core.components.yml create mode 100644 src/app/core/core.schemas.ts delete mode 100644 src/app/core/export/export-config.routes.ts delete mode 100644 src/app/core/export/export-config.schemas.ts delete mode 100644 src/app/core/feedback/feedback.components.yml delete mode 100644 src/app/core/feedback/feedback.routes.ts delete mode 100644 src/app/core/feedback/feedback.schemas.ts delete mode 100644 src/app/core/messages/messages.routes.ts delete mode 100644 src/app/core/metrics/metrics.routes.ts delete mode 100644 src/app/core/notifications/notification.routes.ts create mode 100644 src/app/core/teams/team-auth.middleware.ts delete mode 100644 src/app/core/teams/team.components.yml delete mode 100644 src/app/core/teams/teams.controller.spec.ts delete mode 100644 src/app/core/teams/teams.routes.ts delete mode 100644 src/app/core/teams/teams.schemas.ts delete mode 100644 src/app/core/user/admin/user-admin.controller.spec.ts delete mode 100644 src/app/core/user/admin/user-admin.routes.ts create mode 100644 src/app/core/user/auth/auth.controller.ts create mode 100644 src/app/core/user/auth/auth.middleware.ts delete mode 100644 src/app/core/user/auth/user-authentication.controller.spec.ts delete mode 100644 src/app/core/user/auth/user-authentication.controller.ts delete mode 100644 src/app/core/user/auth/user-authenticiation.routes.ts delete mode 100644 src/app/core/user/auth/user-password.controller.ts delete mode 100644 src/app/core/user/eua/eua.controller.spec.ts create mode 100644 src/app/core/user/eua/eua.middleware.ts delete mode 100644 src/app/core/user/eua/eua.routes.ts delete mode 100644 src/app/core/user/user-auth.middleware.ts delete mode 100644 src/app/core/user/user.components.yml delete mode 100644 src/app/core/user/user.controller.spec.ts delete mode 100644 src/app/core/user/user.routes.ts create mode 100644 src/app/site/test/test.controller.ts delete mode 100644 src/app/site/test/test.routes.ts delete mode 100644 src/lib/express.spec.ts delete mode 100644 src/lib/express.ts create mode 100644 src/lib/fastify.ts create mode 100644 src/spec/fastify.ts delete mode 100644 src/spec/helpers.ts create mode 100644 src/type-extensions.d.ts diff --git a/config/default.js b/config/default.js index 5a6d4725..b6206f71 100644 --- a/config/default.js +++ b/config/default.js @@ -66,10 +66,8 @@ module.exports = { assets: { models: ['src/**/*.model!(.spec).{js,ts}'], - routes: ['src/**/*.routes!(.spec).{js,ts}'], + controllers: ['src/**/*.controller!(.spec).{js,ts}'], sockets: ['src/**/*.socket!(.spec).{js,ts}'], - config: ['src/**/*.config!(.spec).js'], - docs: ['src/**/*/*.components.yml'], // Test specific source files tests: ['src/**/*.spec.{js,ts}'], e2e: ['e2e/**/*.spec.{js, ts}'] @@ -399,12 +397,14 @@ module.exports = { // Express route logging expressLogging: false, + fastifyLogging: false, /** * Logging Settings */ logger: { application: { + prettyPrint: false, silent: false, console: { enabled: true, @@ -422,6 +422,7 @@ module.exports = { } }, audit: { + prettyPrint: false, silent: false, console: { enabled: false, @@ -439,6 +440,7 @@ module.exports = { } }, metrics: { + prettyPrint: false, silent: false, console: { enabled: false, diff --git a/package-lock.json b/package-lock.json index cdaffc42..e53fca67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,30 +8,34 @@ "name": "node-rest-starter", "version": "0.0.0", "dependencies": { + "@fastify/compress": "^8.0.1", + "@fastify/cookie": "^11.0.1", + "@fastify/cors": "^10.0.1", + "@fastify/express": "^4.0.1", + "@fastify/formbody": "^8.0.1", + "@fastify/helmet": "^12.0.1", + "@fastify/passport": "^3.0.1", + "@fastify/session": "^11.0.1", + "@fastify/swagger": "^9.2.0", + "@fastify/swagger-ui": "^5.1.0", + "@fastify/type-provider-json-schema-to-ts": "^4.0.1", "agenda": "^4.3.0", "async": "^3.2.5", - "compression": "1.7", "config": "^3.3.11", "connect-mongo": "^5.1.0", "cors": "^2.8.5", "csv-stringify": "4.0", - "express": "4.21", - "express-actuator": "^1.8.4", - "express-async-errors": "^3.1.1", - "express-json-validator-middleware": "^3.0.1", "express-session": "^1.18.1", + "fastify": "^5.1.0", "glob": "10.3.10", "handlebars": "^4.7.7", - "helmet": "^7.1.0", "http-status-codes": "^2.3.0", "jsonpath": "^1.1.1", "lodash": "4.17.21", "luxon": "^1.28.1", - "method-override": "3.0", "migrate-mongo": "^11.0.0", "mongoose": "^8.4.4", "mongoose-unique-validator": "^5.0.1", - "morgan": "1.9", "multipipe": "^4.0.0", "nodemailer": "6.9.9", "passport": "0.6.0", @@ -40,8 +44,6 @@ "platform": "1.3", "socket.io": "^4.8.0", "socketio-sticky-session": "0.4", - "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1", "through2": "^4.0.2", "typescript": "^5.4.5", "uuid": "^10.0.0", @@ -54,23 +56,16 @@ "@swc/cli": "^0.3.12", "@swc/core": "^1.6.3", "@trivago/prettier-plugin-sort-imports": "^4.3.0", - "@types/compression": "^1.7.5", "@types/config": "^3.3.4", - "@types/express": "4.16", - "@types/express-actuator": "^1.8.3", "@types/express-session": "^1.18.0", "@types/jsonpath": "^0.2.0", "@types/lodash": "^4.14.176", "@types/luxon": "^1.27.1", - "@types/method-override": "^0.0.35", "@types/migrate-mongo": "^10.0.4", "@types/mocha": "^8.2.1", - "@types/morgan": "^1.9.9", "@types/multipipe": "^3.0.5", "@types/passport": "1.0.10", "@types/q": "1.5", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.6", "@types/through2": "^2.0.36", "@types/uuid": "^9.0.8", "@types/yargs": "^17.0.10", @@ -89,8 +84,7 @@ "nodemon": "^3.1.4", "nyc": "^17.0.0", "prettier": "^3.0.0", - "sinon": "^18.0.0", - "swagger-parser": "^10.0.3" + "sinon": "^18.0.0" }, "engines": { "node": ">=20.17.0", @@ -119,62 +113,6 @@ "node": ">=6.0.0" } }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", - "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", - "js-yaml": "^4.1.0" - } - }, - "node_modules/@apidevtools/json-schema-ref-parser/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@apidevtools/openapi-schemas": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", - "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/@apidevtools/swagger-methods": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", - "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" - }, - "node_modules/@apidevtools/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^9.0.6", - "@apidevtools/openapi-schemas": "^2.0.4", - "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "call-me-maybe": "^1.0.1", - "z-schema": "^5.0.1" - }, - "peerDependencies": { - "openapi-types": ">=7" - } - }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", @@ -1386,25 +1324,480 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.0.tgz", + "integrity": "sha512-/Sce/kBzuTxIq5tJh85nVNOq9wKD8s+viIgX0fFMDBdw95gnpf53qmF1oBgJym3cPFliWUuSloVg/1w/rH0FcQ==", + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.1.tgz", + "integrity": "sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==", + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@fastify/compress": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@fastify/compress/-/compress-8.0.1.tgz", + "integrity": "sha512-yWNfKhvL4orfN45LKCHCo8Fcsbj1kdNgwyShw2xpdHfzPf4A3MESmgSfUm3TCKQwgqDdrPnLfy1E+3I/DVP+BQ==", + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "fastify-plugin": "^5.0.0", + "mime-db": "^1.52.0", + "minipass": "^7.0.4", + "peek-stream": "^1.1.3", + "pump": "^3.0.0", + "pumpify": "^2.0.1", + "readable-stream": "^4.5.2" + } + }, + "node_modules/@fastify/compress/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/@fastify/compress/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fastify/compress/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/compress/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/@fastify/cookie": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.1.tgz", + "integrity": "sha512-n1Ooz4bgQ5LcOlJQboWPfsMNxIrGV0SgU85UkctdpTlCQE0mtA3rlspOPUdqk9ubiiZn053ucnia4DjTquI4/g==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.0", + "fastify-plugin": "^5.0.0" + } + }, + "node_modules/@fastify/cookie/node_modules/cookie": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.1.tgz", + "integrity": "sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/cors": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-10.0.1.tgz", + "integrity": "sha512-O8JIf6448uQbOgzSkCqhClw6gFTAqrdfeA6R3fc/3gwTJGUp7gl8/3tbNB+6INuu4RmgVOq99BmvdGbtu5pgOA==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "mnemonist": "0.39.8" + } + }, + "node_modules/@fastify/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz", + "integrity": "sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==", + "license": "MIT" + }, + "node_modules/@fastify/express": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@fastify/express/-/express-4.0.1.tgz", + "integrity": "sha512-mEQ6pawaENeZ3swqVtkxdLi8NQC5eKBkclE+7ma1qQMuB+yI6WxDyEp55pdbqPIqBQTN/cGgHv84qxVS7NKC2Q==", + "license": "MIT", + "dependencies": { + "express": "^4.18.3", + "fastify-plugin": "^5.0.0" + } + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.1.tgz", + "integrity": "sha512-f2d3JExJgFE3UbdFcpPwqNUEoHWmt8pAKf8f+9YuLESdefA0WgqxeT6DrGL4Yrf/9ihXNSKOqpjEmurV405meA==", + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/flash": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@fastify/flash/-/flash-6.0.2.tgz", + "integrity": "sha512-z/FZ5GeQO+o+rk8X3NjVwzmYJz6lnL+SYGEFxyZJZn6fkIj/M4U4FfvYwIWqEyr30ExJlX+d/5hX41Ld4ANM2g==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0" + } + }, + "node_modules/@fastify/formbody": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-8.0.1.tgz", + "integrity": "sha512-LPrcadSIK8TrQk510Zdj56fnw7cyHq0/PW0YHGGM8ycGL4X7XAex+FKcwpzB4i5lF9eykc71a4EtcO9AEoByqw==", + "license": "MIT", + "dependencies": { + "fast-querystring": "^1.1.2", + "fastify-plugin": "^5.0.0" + } + }, + "node_modules/@fastify/helmet": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@fastify/helmet/-/helmet-12.0.1.tgz", + "integrity": "sha512-kkjBcedWwdflRThovGuvN9jB2QQLytBqArCFPdMIb7o2Fp0l/H3xxYi/6x/SSRuH/FFt9qpTGIfJz2bfnMrLqA==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "helmet": "^7.1.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@fastify/passport": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/passport/-/passport-3.0.1.tgz", + "integrity": "sha512-23hMw7W2rJafea0uiWiWMpNyLFAORABNH2mFzlF4jmlimM8A1dtBuWoyQe/UsgDXtGeDKfyDMJvD5yzR4vY81w==", + "license": "MIT", + "dependencies": { + "@fastify/flash": "^6.0.0", + "fastify-plugin": "^5.0.0" + } + }, + "node_modules/@fastify/send": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-3.1.1.tgz", + "integrity": "sha512-LdiV2mle/2tH8vh6GwGl0ubfUAgvY+9yF9oGI1iiwVyNUVOQamvw5n+OFu6iCNNoyuCY80FFURBn4TZCbTe8LA==", + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/send/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@fastify/session": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@fastify/session/-/session-11.0.1.tgz", + "integrity": "sha512-I/CRrC6vWktZor9aF5eG9HZh/hxQQ4HY8GntIpeSjl97Q3ly0KnqD+BBGGNtRWOK369tclvL8FEQx3pWTgzSHQ==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^4.5.1", + "safe-stable-stringify": "^2.4.3" + } + }, + "node_modules/@fastify/session/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==", + "license": "MIT" + }, + "node_modules/@fastify/static": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.0.2.tgz", + "integrity": "sha512-xJ+XaZVl4Y+lKztx8jGi+BE73aByhOmjMgaTx98E4XtVZxUpiaYQIMBlwACsJz+xohm0kvzV34BZoiZ+bsJtBQ==", + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^3.1.0", + "content-disposition": "^0.5.4", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^11.0.0" + } + }, + "node_modules/@fastify/static/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@fastify/static/node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@fastify/static/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/swagger": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.2.0.tgz", + "integrity": "sha512-tuy78fW3G4z8EhTdopAu6gXbllFrQBkcYduOmPiEVESZNaLnxR8N80YVu7F6WuMwk7sd9rYGnGo9bxDQChCFjg==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "json-schema-resolver": "^2.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.2" + } + }, + "node_modules/@fastify/swagger-ui": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/swagger-ui/-/swagger-ui-5.1.0.tgz", + "integrity": "sha512-XWb+zWz0vlP4QIXbF2xo/n9XuOjNF5aRdQ+0AiBXY9nlIuoTYU1ZXCkXNStdnM/sOdnDy8Q1vsxZ2RsN7XivQA==", + "license": "MIT", "dependencies": { - "argparse": "^2.0.1" - }, + "@fastify/static": "^8.0.0", + "fastify-plugin": "^5.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.1" + } + }, + "node_modules/@fastify/swagger-ui/node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "license": "ISC", "bin": { - "js-yaml": "bin/js-yaml.js" + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" } }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, + "node_modules/@fastify/swagger/node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 14" + } + }, + "node_modules/@fastify/type-provider-json-schema-to-ts": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@fastify/type-provider-json-schema-to-ts/-/type-provider-json-schema-to-ts-4.0.1.tgz", + "integrity": "sha512-+QS1iiRZMAqCcWX7Ck8zAVXb1WbjpffC2gOYxDGvF1wtLviblz7HtnjAX04bW6ZsgreZP0RktRPlEAzmEafEQw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.0" } }, "node_modules/@humanwhocodes/config-array": { @@ -1658,10 +2051,14 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/@mole-inc/bin-wrapper": { "version": "8.0.1", @@ -2777,6 +3174,7 @@ }, "node_modules/@types/body-parser": { "version": "1.19.2", + "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -2795,15 +3193,6 @@ "@types/responselike": "^1.0.0" } }, - "node_modules/@types/compression": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", - "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", - "dev": true, - "dependencies": { - "@types/express": "*" - } - }, "node_modules/@types/config": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/@types/config/-/config-3.3.4.tgz", @@ -2812,6 +3201,7 @@ }, "node_modules/@types/connect": { "version": "3.4.35", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -2860,17 +3250,9 @@ "@types/serve-static": "*" } }, - "node_modules/@types/express-actuator": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@types/express-actuator/-/express-actuator-1.8.3.tgz", - "integrity": "sha512-Nhy+wtwkZagm0dYE5skOXPX8oclnFahkP1yRsxd+sW495tn0uA7kMvd5f8EExYRJv12/c5+yN9GYyBy9whWrkQ==", - "dev": true, - "dependencies": { - "@types/express": "*" - } - }, "node_modules/@types/express-serve-static-core": { "version": "4.17.30", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -2895,6 +3277,7 @@ }, "node_modules/@types/json-schema": { "version": "7.0.11", + "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -2928,15 +3311,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/method-override": { - "version": "0.0.35", - "resolved": "https://registry.npmjs.org/@types/method-override/-/method-override-0.0.35.tgz", - "integrity": "sha512-HdhM5xiIV8fwsZ3B8e9IKWJOqEgmXXBJ/qQzhs5Z8idjsszqEX4j/7/QAcso27ArZ1tSBXg2XMlI1cIHAsCTXA==", - "dev": true, - "dependencies": { - "@types/express": "*" - } - }, "node_modules/@types/migrate-mongo": { "version": "10.0.4", "resolved": "https://registry.npmjs.org/@types/migrate-mongo/-/migrate-mongo-10.0.4.tgz", @@ -2949,6 +3323,7 @@ }, "node_modules/@types/mime": { "version": "3.0.1", + "dev": true, "license": "MIT" }, "node_modules/@types/mocha": { @@ -2956,15 +3331,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/morgan": { - "version": "1.9.9", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", - "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/multipipe": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/multipipe/-/multipipe-3.0.5.tgz", @@ -2993,10 +3359,12 @@ }, "node_modules/@types/qs": { "version": "6.9.7", + "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.4", + "dev": true, "license": "MIT" }, "node_modules/@types/responselike": { @@ -3016,28 +3384,13 @@ }, "node_modules/@types/serve-static": { "version": "1.15.0", + "dev": true, "license": "MIT", "dependencies": { "@types/mime": "*", "@types/node": "*" } }, - "node_modules/@types/swagger-jsdoc": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", - "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", - "dev": true - }, - "node_modules/@types/swagger-ui-express": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", - "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", - "dev": true, - "dependencies": { - "@types/express": "*", - "@types/serve-static": "*" - } - }, "node_modules/@types/through2": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/@types/through2/-/through2-2.0.36.tgz", @@ -3304,6 +3657,24 @@ "dev": true, "license": "ISC" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "license": "MIT", @@ -3437,6 +3808,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-escapes": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", @@ -3684,6 +4094,15 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -3696,6 +4115,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/avvio": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", + "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "license": "MIT" @@ -3726,16 +4155,6 @@ "node": "^4.5.0 || >= 5.9" } }, - "node_modules/basic-auth": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -4081,6 +4500,7 @@ }, "node_modules/brace-expansion": { "version": "1.1.11", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -4172,8 +4592,7 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "node_modules/builtin-modules": { "version": "3.3.0", @@ -4202,13 +4621,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bytes": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -4283,11 +4695,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4566,45 +4973,9 @@ "dev": true, "license": "MIT" }, - "node_modules/compressible": { - "version": "2.0.18", - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "license": "MIT" - }, "node_modules/concat-map": { "version": "0.0.1", + "dev": true, "license": "MIT" }, "node_modules/config": { @@ -4769,11 +5140,6 @@ "version": "2.0.0", "license": "MIT" }, - "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" - }, "node_modules/debug": { "version": "4.3.4", "license": "MIT", @@ -4926,13 +5292,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/depd": { - "version": "1.1.2", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -4963,6 +5322,7 @@ }, "node_modules/doctrine": { "version": "3.0.0", + "dev": true, "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" @@ -4978,6 +5338,18 @@ "readable-stream": "^2.0.2" } }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -5014,7 +5386,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -5938,12 +6309,30 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "dev": true }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", @@ -6032,69 +6421,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express-actuator": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/express-actuator/-/express-actuator-1.8.4.tgz", - "integrity": "sha512-V0VbfdnxYTp2IPIDh78LVoxShW0CL83rbLQ/BtFansamEaX93DepIhy9v78zZysJoPvce8nmS3TumwYSV4XKiw==", - "dependencies": { - "dayjs": "^1.11.3", - "properties-reader": "^2.2.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/express-async-errors": { - "version": "3.1.1", - "license": "ISC", - "peerDependencies": { - "express": "^4.16.2" - } - }, - "node_modules/express-json-validator-middleware": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/express-json-validator-middleware/-/express-json-validator-middleware-3.0.1.tgz", - "integrity": "sha512-DkqrIwS4O1eCqshuBNG76dBV+BuoXhgsiUjW9Rh3aBerlIY6fwIrNQ+UZLqh6CLbqcQRQTbqiNA5AlneqlUflA==", - "dependencies": { - "@types/express": "^4.17.3", - "@types/express-serve-static-core": "^4.17.2", - "@types/json-schema": "^7.0.4", - "ajv": "^8.11.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/express-json-validator-middleware/node_modules/@types/express": { - "version": "4.17.13", - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/express-json-validator-middleware/node_modules/ajv": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", - "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/express-json-validator-middleware/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, "node_modules/express-session": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", @@ -6238,6 +6564,12 @@ "node": ">=4" } }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "license": "MIT" @@ -6263,17 +6595,90 @@ "node": ">=8.6.0" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-json-stringify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.0.tgz", + "integrity": "sha512-FGMKZwniMTgZh7zQp9b6XnBVxUmKVahQLQeRQHqwYmPDqDhcEKZ3BaQsxelFFI5PY7nN71OEeiL47/zUWcYe1A==", + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.1", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.3.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv/node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "license": "BSD-3-Clause" + }, + "node_modules/fast-json-stringify/node_modules/fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", + "license": "MIT" + }, + "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "license": "BSD-3-Clause" + }, "node_modules/fast-xml-parser": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", @@ -6296,9 +6701,49 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fastify": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.1.0.tgz", + "integrity": "sha512-0SdUC5AoiSgMSc2Vxwv3WyKzyGMDJRAW/PgNsK1kZrnkO6MeqUIW9ovVg9F2UGIqtIcclYMyeJa4rK6OZc7Jxg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.0", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.0.0", + "process-warning": "^4.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.1", + "secure-json-parse": "^2.7.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", + "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==", + "license": "MIT" + }, "node_modules/fastq": { - "version": "1.13.0", - "dev": true, + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -6443,6 +6888,20 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-my-way": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.1.0.tgz", + "integrity": "sha512-Y5jIsuYR4BwWDYYQ2A/RWWE6gD8a0FMgtU+HOq1WKku+Cwdz8M1v8wcAmRXXM1/iqtoqg06v+LjAxMYbCjViMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/find-up": { "version": "5.0.0", "dev": true, @@ -6581,6 +7040,7 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", + "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -7203,6 +7663,7 @@ }, "node_modules/inflight": { "version": "1.0.6", + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -7850,6 +8311,45 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-resolver": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/json-schema-resolver/-/json-schema-resolver-2.0.0.tgz", + "integrity": "sha512-pJ4XLQP4Q9HTxl6RVDLJ8Cyh1uitSs0CzDBAz1uoJ4sRD/Bk7cFSXL1FUXDW3zJ7YnfliJx6eu8Jn283bpZ4Yg==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "rfdc": "^1.1.4", + "uri-js": "^4.2.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/Eomm/json-schema-resolver?sponsor=1" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7957,6 +8457,26 @@ "node": ">= 0.8.0" } }, + "node_modules/light-my-request": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.3.0.tgz", + "integrity": "sha512-bWTAPJmeWQH5suJNYwG0f5cs0p6ho9e6f1Ppoxv5qMosY+s9Ir2+ZLvvHcgA7VTDop4zl/NCHhOVVqU+kd++Ow==", + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/cookie": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.1.tgz", + "integrity": "sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/lilconfig": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", @@ -8209,20 +8729,10 @@ "version": "4.4.2", "license": "MIT" }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, "node_modules/lodash.merge": { "version": "4.6.2", "license": "MIT" }, - "node_modules/lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" - }, "node_modules/log-symbols": { "version": "4.1.0", "dev": true, @@ -8529,30 +9039,6 @@ "node": ">= 8" } }, - "node_modules/method-override": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "debug": "3.1.0", - "methods": "~1.1.2", - "parseurl": "~1.3.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/method-override/node_modules/debug": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/method-override/node_modules/ms": { - "version": "2.0.0", - "license": "MIT" - }, "node_modules/methods": { "version": "1.1.2", "license": "MIT", @@ -8659,6 +9145,7 @@ }, "node_modules/minimatch": { "version": "3.1.2", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -8672,13 +9159,23 @@ "license": "MIT" }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/mnemonist": { + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", + "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.1" + } + }, "node_modules/mocha": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.4.0.tgz", @@ -9010,31 +9507,6 @@ "version": "2.1.3", "license": "MIT" }, - "node_modules/morgan": { - "version": "1.9.1", - "license": "MIT", - "dependencies": { - "basic-auth": "~2.0.0", - "debug": "2.6.9", - "depd": "~1.1.2", - "on-finished": "~2.3.0", - "on-headers": "~1.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/morgan/node_modules/debug": { - "version": "2.6.9", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/morgan/node_modules/ms": { - "version": "2.0.0", - "license": "MIT" - }, "node_modules/mpath": { "version": "0.9.0", "license": "MIT", @@ -9574,14 +10046,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/on-finished": { - "version": "2.3.0", + "node_modules/obliterator": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", + "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==", + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, "engines": { - "node": ">= 0.8" + "node": ">=14.0.0" } }, "node_modules/on-headers": { @@ -9642,8 +10119,7 @@ "node_modules/openapi-types": { "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "peer": true + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" }, "node_modules/optionator": { "version": "0.9.3", @@ -9753,6 +10229,12 @@ "node": ">=8" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9821,6 +10303,7 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9891,6 +10374,27 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/peek-stream": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", + "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "duplexify": "^3.5.0", + "through2": "^2.0.3" + } + }, + "node_modules/peek-stream/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -9929,6 +10433,43 @@ "node": ">=0.10.0" } }, + "node_modules/pino": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz", + "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -10067,31 +10608,11 @@ "node": ">=8" } }, - "node_modules/properties-reader": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-2.3.0.tgz", - "integrity": "sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==", - "dependencies": { - "mkdirp": "^1.0.4" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/steveukx/properties?sponsor=1" - } - }, - "node_modules/properties-reader/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } + "node_modules/process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", + "license": "MIT" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -10119,12 +10640,48 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, + "node_modules/pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, + "node_modules/pumpify/node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/pumpify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -10166,6 +10723,12 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -10277,6 +10840,15 @@ "node": ">=8.10.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -10423,9 +10995,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/reusify": { "version": "1.0.4", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -10435,8 +11015,7 @@ "node_modules/rfdc": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", - "dev": true + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" }, "node_modules/rimraf": { "version": "3.0.2", @@ -10628,6 +11207,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-regex2": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-4.0.0.tgz", + "integrity": "sha512-Hvjfv25jPDVr3U+4LDzBuZPPOymELG3PYcSk5hcevooo1yxxamQL/bHs/GrEPGmMoMEwRrHVGiCA1pXi97B8Ew==", + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } + }, "node_modules/safe-stable-stringify": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", @@ -10641,11 +11229,16 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -10775,6 +11368,12 @@ "dev": true, "license": "ISC" }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -11023,6 +11622,15 @@ "npm": ">= 3.0.0" } }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -11125,6 +11733,15 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "dev": true, @@ -11154,6 +11771,12 @@ "node": ">= 0.8" } }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.1.1", "license": "MIT", @@ -11361,89 +11984,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swagger-jsdoc": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", - "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", - "dependencies": { - "commander": "6.2.0", - "doctrine": "3.0.0", - "glob": "7.1.6", - "lodash.mergewith": "^4.6.2", - "swagger-parser": "^10.0.3", - "yaml": "2.0.0-1" - }, - "bin": { - "swagger-jsdoc": "bin/swagger-jsdoc.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/swagger-jsdoc/node_modules/commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/swagger-jsdoc/node_modules/glob": { - "version": "7.1.6", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/swagger-jsdoc/node_modules/yaml": { - "version": "2.0.0-1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", - "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", - "dependencies": { - "@apidevtools/swagger-parser": "10.0.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/swagger-ui-dist": { - "version": "5.17.14", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", - "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==" - }, - "node_modules/swagger-ui-express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", - "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "dependencies": { - "swagger-ui-dist": ">=5.0.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } - }, "node_modules/synckit": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", @@ -11512,6 +12052,15 @@ "dev": true, "license": "MIT" }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -11565,6 +12114,15 @@ "node": ">=8.0" } }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -11644,6 +12202,12 @@ "node": ">= 14.0.0" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -11986,14 +12550,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/vary": { "version": "1.1.2", "license": "MIT", @@ -12292,6 +12848,15 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "license": "ISC", @@ -12412,34 +12977,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/z-schema": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", - "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", - "dependencies": { - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "validator": "^13.7.0" - }, - "bin": { - "z-schema": "bin/z-schema" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "commander": "^9.4.1" - } - }, - "node_modules/z-schema/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "optional": true, - "engines": { - "node": "^12.20.0 || >=14" - } } }, "dependencies": { @@ -12459,55 +12996,6 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, - "@apidevtools/json-schema-ref-parser": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", - "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", - "requires": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", - "js-yaml": "^4.1.0" - }, - "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "requires": { - "argparse": "^2.0.1" - } - } - } - }, - "@apidevtools/openapi-schemas": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", - "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==" - }, - "@apidevtools/swagger-methods": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", - "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" - }, - "@apidevtools/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", - "requires": { - "@apidevtools/json-schema-ref-parser": "^9.0.6", - "@apidevtools/openapi-schemas": "^2.0.4", - "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "call-me-maybe": "^1.0.1", - "z-schema": "^5.0.1" - } - }, "@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", @@ -13446,71 +13934,406 @@ "kuler": "^2.0.0" } }, - "@es-joy/jsdoccomment": { - "version": "0.39.4", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.39.4.tgz", - "integrity": "sha512-Jvw915fjqQct445+yron7Dufix9A+m9j1fCJYlCo1FWlRvTxa3pjJelxdSTdaLWcTwRU6vbL+NYjO4YuNIS5Qg==", - "dev": true, + "@es-joy/jsdoccomment": { + "version": "0.39.4", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.39.4.tgz", + "integrity": "sha512-Jvw915fjqQct445+yron7Dufix9A+m9j1fCJYlCo1FWlRvTxa3pjJelxdSTdaLWcTwRU6vbL+NYjO4YuNIS5Qg==", + "dev": true, + "requires": { + "comment-parser": "1.3.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + } + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + } + } + }, + "@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true + }, + "@fastify/accept-negotiator": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.0.tgz", + "integrity": "sha512-/Sce/kBzuTxIq5tJh85nVNOq9wKD8s+viIgX0fFMDBdw95gnpf53qmF1oBgJym3cPFliWUuSloVg/1w/rH0FcQ==" + }, + "@fastify/ajv-compiler": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.1.tgz", + "integrity": "sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==", + "requires": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, + "@fastify/compress": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@fastify/compress/-/compress-8.0.1.tgz", + "integrity": "sha512-yWNfKhvL4orfN45LKCHCo8Fcsbj1kdNgwyShw2xpdHfzPf4A3MESmgSfUm3TCKQwgqDdrPnLfy1E+3I/DVP+BQ==", + "requires": { + "@fastify/accept-negotiator": "^2.0.0", + "fastify-plugin": "^5.0.0", + "mime-db": "^1.52.0", + "minipass": "^7.0.4", + "peek-stream": "^1.1.3", + "pump": "^3.0.0", + "pumpify": "^2.0.1", + "readable-stream": "^4.5.2" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "@fastify/cookie": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.1.tgz", + "integrity": "sha512-n1Ooz4bgQ5LcOlJQboWPfsMNxIrGV0SgU85UkctdpTlCQE0mtA3rlspOPUdqk9ubiiZn053ucnia4DjTquI4/g==", + "requires": { + "cookie": "^1.0.0", + "fastify-plugin": "^5.0.0" + }, + "dependencies": { + "cookie": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.1.tgz", + "integrity": "sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==" + } + } + }, + "@fastify/cors": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-10.0.1.tgz", + "integrity": "sha512-O8JIf6448uQbOgzSkCqhClw6gFTAqrdfeA6R3fc/3gwTJGUp7gl8/3tbNB+6INuu4RmgVOq99BmvdGbtu5pgOA==", + "requires": { + "fastify-plugin": "^5.0.0", + "mnemonist": "0.39.8" + } + }, + "@fastify/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz", + "integrity": "sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==" + }, + "@fastify/express": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@fastify/express/-/express-4.0.1.tgz", + "integrity": "sha512-mEQ6pawaENeZ3swqVtkxdLi8NQC5eKBkclE+7ma1qQMuB+yI6WxDyEp55pdbqPIqBQTN/cGgHv84qxVS7NKC2Q==", + "requires": { + "express": "^4.18.3", + "fastify-plugin": "^5.0.0" + } + }, + "@fastify/fast-json-stringify-compiler": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.1.tgz", + "integrity": "sha512-f2d3JExJgFE3UbdFcpPwqNUEoHWmt8pAKf8f+9YuLESdefA0WgqxeT6DrGL4Yrf/9ihXNSKOqpjEmurV405meA==", + "requires": { + "fast-json-stringify": "^6.0.0" + } + }, + "@fastify/flash": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@fastify/flash/-/flash-6.0.2.tgz", + "integrity": "sha512-z/FZ5GeQO+o+rk8X3NjVwzmYJz6lnL+SYGEFxyZJZn6fkIj/M4U4FfvYwIWqEyr30ExJlX+d/5hX41Ld4ANM2g==", + "requires": { + "fastify-plugin": "^5.0.0" + } + }, + "@fastify/formbody": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-8.0.1.tgz", + "integrity": "sha512-LPrcadSIK8TrQk510Zdj56fnw7cyHq0/PW0YHGGM8ycGL4X7XAex+FKcwpzB4i5lF9eykc71a4EtcO9AEoByqw==", + "requires": { + "fast-querystring": "^1.1.2", + "fastify-plugin": "^5.0.0" + } + }, + "@fastify/helmet": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@fastify/helmet/-/helmet-12.0.1.tgz", + "integrity": "sha512-kkjBcedWwdflRThovGuvN9jB2QQLytBqArCFPdMIb7o2Fp0l/H3xxYi/6x/SSRuH/FFt9qpTGIfJz2bfnMrLqA==", + "requires": { + "fastify-plugin": "^5.0.0", + "helmet": "^7.1.0" + } + }, + "@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "@fastify/passport": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/passport/-/passport-3.0.1.tgz", + "integrity": "sha512-23hMw7W2rJafea0uiWiWMpNyLFAORABNH2mFzlF4jmlimM8A1dtBuWoyQe/UsgDXtGeDKfyDMJvD5yzR4vY81w==", "requires": { - "comment-parser": "1.3.1", - "esquery": "^1.5.0", - "jsdoc-type-pratt-parser": "~4.0.0" + "@fastify/flash": "^6.0.0", + "fastify-plugin": "^5.0.0" } }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, + "@fastify/send": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-3.1.1.tgz", + "integrity": "sha512-LdiV2mle/2tH8vh6GwGl0ubfUAgvY+9yF9oGI1iiwVyNUVOQamvw5n+OFu6iCNNoyuCY80FFURBn4TZCbTe8LA==", "requires": { - "eslint-visitor-keys": "^3.3.0" + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + }, + "dependencies": { + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==" + } } }, - "@eslint-community/regexpp": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", - "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", - "dev": true + "@fastify/session": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@fastify/session/-/session-11.0.1.tgz", + "integrity": "sha512-I/CRrC6vWktZor9aF5eG9HZh/hxQQ4HY8GntIpeSjl97Q3ly0KnqD+BBGGNtRWOK369tclvL8FEQx3pWTgzSHQ==", + "requires": { + "fastify-plugin": "^4.5.1", + "safe-stable-stringify": "^2.4.3" + }, + "dependencies": { + "fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + } + } }, - "@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, + "@fastify/static": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.0.2.tgz", + "integrity": "sha512-xJ+XaZVl4Y+lKztx8jGi+BE73aByhOmjMgaTx98E4XtVZxUpiaYQIMBlwACsJz+xohm0kvzV34BZoiZ+bsJtBQ==", "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^3.1.0", + "content-disposition": "^0.5.4", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^11.0.0" }, "dependencies": { - "argparse": { + "brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, + "foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "requires": { - "argparse": "^2.0.1" + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + } + }, + "glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + } + }, + "jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "requires": { + "@isaacs/cliui": "^8.0.2" + } + }, + "lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==" + }, + "minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "requires": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" } + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" } } }, - "@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true + "@fastify/swagger": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.2.0.tgz", + "integrity": "sha512-tuy78fW3G4z8EhTdopAu6gXbllFrQBkcYduOmPiEVESZNaLnxR8N80YVu7F6WuMwk7sd9rYGnGo9bxDQChCFjg==", + "requires": { + "fastify-plugin": "^5.0.0", + "json-schema-resolver": "^2.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.2" + }, + "dependencies": { + "yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==" + } + } + }, + "@fastify/swagger-ui": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/swagger-ui/-/swagger-ui-5.1.0.tgz", + "integrity": "sha512-XWb+zWz0vlP4QIXbF2xo/n9XuOjNF5aRdQ+0AiBXY9nlIuoTYU1ZXCkXNStdnM/sOdnDy8Q1vsxZ2RsN7XivQA==", + "requires": { + "@fastify/static": "^8.0.0", + "fastify-plugin": "^5.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.1" + }, + "dependencies": { + "yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==" + } + } + }, + "@fastify/type-provider-json-schema-to-ts": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@fastify/type-provider-json-schema-to-ts/-/type-provider-json-schema-to-ts-4.0.1.tgz", + "integrity": "sha512-+QS1iiRZMAqCcWX7Ck8zAVXb1WbjpffC2gOYxDGvF1wtLviblz7HtnjAX04bW6ZsgreZP0RktRPlEAzmEafEQw==", + "requires": { + "json-schema-to-ts": "^3.1.0" + } }, "@humanwhocodes/config-array": { "version": "0.11.14", @@ -13682,10 +14505,10 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + "@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==" }, "@mole-inc/bin-wrapper": { "version": "8.0.1", @@ -14497,6 +15320,7 @@ }, "@types/body-parser": { "version": "1.19.2", + "dev": true, "requires": { "@types/connect": "*", "@types/node": "*" @@ -14514,15 +15338,6 @@ "@types/responselike": "^1.0.0" } }, - "@types/compression": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", - "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", - "dev": true, - "requires": { - "@types/express": "*" - } - }, "@types/config": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/@types/config/-/config-3.3.4.tgz", @@ -14531,6 +15346,7 @@ }, "@types/connect": { "version": "3.4.35", + "dev": true, "requires": { "@types/node": "*" } @@ -14577,17 +15393,9 @@ "@types/serve-static": "*" } }, - "@types/express-actuator": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@types/express-actuator/-/express-actuator-1.8.3.tgz", - "integrity": "sha512-Nhy+wtwkZagm0dYE5skOXPX8oclnFahkP1yRsxd+sW495tn0uA7kMvd5f8EExYRJv12/c5+yN9GYyBy9whWrkQ==", - "dev": true, - "requires": { - "@types/express": "*" - } - }, "@types/express-serve-static-core": { "version": "4.17.30", + "dev": true, "requires": { "@types/node": "*", "@types/qs": "*", @@ -14610,7 +15418,8 @@ "dev": true }, "@types/json-schema": { - "version": "7.0.11" + "version": "7.0.11", + "dev": true }, "@types/json5": { "version": "0.0.29", @@ -14641,15 +15450,6 @@ "version": "1.27.1", "dev": true }, - "@types/method-override": { - "version": "0.0.35", - "resolved": "https://registry.npmjs.org/@types/method-override/-/method-override-0.0.35.tgz", - "integrity": "sha512-HdhM5xiIV8fwsZ3B8e9IKWJOqEgmXXBJ/qQzhs5Z8idjsszqEX4j/7/QAcso27ArZ1tSBXg2XMlI1cIHAsCTXA==", - "dev": true, - "requires": { - "@types/express": "*" - } - }, "@types/migrate-mongo": { "version": "10.0.4", "resolved": "https://registry.npmjs.org/@types/migrate-mongo/-/migrate-mongo-10.0.4.tgz", @@ -14661,21 +15461,13 @@ } }, "@types/mime": { - "version": "3.0.1" + "version": "3.0.1", + "dev": true }, "@types/mocha": { "version": "8.2.3", "dev": true }, - "@types/morgan": { - "version": "1.9.9", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", - "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/multipipe": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/multipipe/-/multipipe-3.0.5.tgz", @@ -14700,10 +15492,12 @@ "dev": true }, "@types/qs": { - "version": "6.9.7" + "version": "6.9.7", + "dev": true }, "@types/range-parser": { - "version": "1.2.4" + "version": "1.2.4", + "dev": true }, "@types/responselike": { "version": "1.0.0", @@ -14722,27 +15516,12 @@ }, "@types/serve-static": { "version": "1.15.0", + "dev": true, "requires": { "@types/mime": "*", "@types/node": "*" } }, - "@types/swagger-jsdoc": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", - "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", - "dev": true - }, - "@types/swagger-ui-express": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", - "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", - "dev": true, - "requires": { - "@types/express": "*", - "@types/serve-static": "*" - } - }, "@types/through2": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/@types/through2/-/through2-2.0.36.tgz", @@ -14913,6 +15692,19 @@ "version": "1.1.1", "dev": true }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, "accepts": { "version": "1.3.8", "requires": { @@ -15013,6 +15805,32 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, "ansi-escapes": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", @@ -15179,12 +15997,26 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, + "atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" + }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true }, + "avvio": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", + "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", + "requires": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, "balanced-match": { "version": "1.0.2" }, @@ -15196,12 +16028,6 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, - "basic-auth": { - "version": "2.0.1", - "requires": { - "safe-buffer": "5.1.2" - } - }, "big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -15467,6 +16293,7 @@ }, "brace-expansion": { "version": "1.1.11", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -15515,8 +16342,7 @@ "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "builtin-modules": { "version": "3.3.0", @@ -15533,9 +16359,6 @@ "run-applescript": "^5.0.0" } }, - "bytes": { - "version": "3.0.0" - }, "cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -15590,11 +16413,6 @@ "set-function-length": "^1.2.1" } }, - "call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -15786,37 +16604,9 @@ "version": "1.0.1", "dev": true }, - "compressible": { - "version": "2.0.18", - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0" - } - } - }, "concat-map": { - "version": "0.0.1" + "version": "0.0.1", + "dev": true }, "config": { "version": "3.3.11", @@ -15927,11 +16717,6 @@ } } }, - "dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" - }, "debug": { "version": "4.3.4", "requires": { @@ -16024,9 +16809,6 @@ "object-keys": "^1.1.1" } }, - "depd": { - "version": "1.1.2" - }, "destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -16045,6 +16827,7 @@ }, "doctrine": { "version": "3.0.0", + "dev": true, "requires": { "esutils": "^2.0.2" } @@ -16055,6 +16838,17 @@ "readable-stream": "^2.0.2" } }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -16086,7 +16880,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -16735,12 +17528,22 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "dev": true }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, "execa": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", @@ -16835,61 +17638,10 @@ "ee-first": "1.1.1" } }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - } - } - }, - "express-actuator": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/express-actuator/-/express-actuator-1.8.4.tgz", - "integrity": "sha512-V0VbfdnxYTp2IPIDh78LVoxShW0CL83rbLQ/BtFansamEaX93DepIhy9v78zZysJoPvce8nmS3TumwYSV4XKiw==", - "requires": { - "dayjs": "^1.11.3", - "properties-reader": "^2.2.0" - } - }, - "express-async-errors": { - "version": "3.1.1", - "requires": {} - }, - "express-json-validator-middleware": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/express-json-validator-middleware/-/express-json-validator-middleware-3.0.1.tgz", - "integrity": "sha512-DkqrIwS4O1eCqshuBNG76dBV+BuoXhgsiUjW9Rh3aBerlIY6fwIrNQ+UZLqh6CLbqcQRQTbqiNA5AlneqlUflA==", - "requires": { - "@types/express": "^4.17.3", - "@types/express-serve-static-core": "^4.17.2", - "@types/json-schema": "^7.0.4", - "ajv": "^8.11.0" - }, - "dependencies": { - "@types/express": { - "version": "4.17.13", - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "ajv": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", - "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", - "requires": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" } } }, @@ -16958,6 +17710,11 @@ "sort-keys-length": "^1.0.0" } }, + "fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, "fast-deep-equal": { "version": "3.1.3" }, @@ -16984,11 +17741,73 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "fast-json-stringify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.0.tgz", + "integrity": "sha512-FGMKZwniMTgZh7zQp9b6XnBVxUmKVahQLQeRQHqwYmPDqDhcEKZ3BaQsxelFFI5PY7nN71OEeiL47/zUWcYe1A==", + "requires": { + "@fastify/merge-json-schemas": "^0.1.1", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.3.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "dependencies": { + "fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==" + } + } + }, + "fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==" + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "requires": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==" + }, + "fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==" + }, "fast-xml-parser": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", @@ -16998,9 +17817,37 @@ "strnum": "^1.0.5" } }, + "fastify": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.1.0.tgz", + "integrity": "sha512-0SdUC5AoiSgMSc2Vxwv3WyKzyGMDJRAW/PgNsK1kZrnkO6MeqUIW9ovVg9F2UGIqtIcclYMyeJa4rK6OZc7Jxg==", + "requires": { + "@fastify/ajv-compiler": "^4.0.0", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.0.0", + "process-warning": "^4.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.1", + "secure-json-parse": "^2.7.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "fastify-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", + "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==" + }, "fastq": { - "version": "1.13.0", - "dev": true, + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "requires": { "reusify": "^1.0.4" } @@ -17108,6 +17955,16 @@ "pkg-dir": "^4.1.0" } }, + "find-my-way": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.1.0.tgz", + "integrity": "sha512-Y5jIsuYR4BwWDYYQ2A/RWWE6gD8a0FMgtU+HOq1WKku+Cwdz8M1v8wcAmRXXM1/iqtoqg06v+LjAxMYbCjViMw==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^4.0.0" + } + }, "find-up": { "version": "5.0.0", "dev": true, @@ -17191,7 +18048,8 @@ } }, "fs.realpath": { - "version": "1.0.0" + "version": "1.0.0", + "dev": true }, "fsevents": { "version": "2.3.2", @@ -17582,6 +18440,7 @@ }, "inflight": { "version": "1.0.6", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -17999,6 +18858,33 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-resolver": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/json-schema-resolver/-/json-schema-resolver-2.0.0.tgz", + "integrity": "sha512-pJ4XLQP4Q9HTxl6RVDLJ8Cyh1uitSs0CzDBAz1uoJ4sRD/Bk7cFSXL1FUXDW3zJ7YnfliJx6eu8Jn283bpZ4Yg==", + "requires": { + "debug": "^4.1.1", + "rfdc": "^1.1.4", + "uri-js": "^4.2.2" + } + }, + "json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "requires": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + } + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -18083,6 +18969,23 @@ "type-check": "~0.4.0" } }, + "light-my-request": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.3.0.tgz", + "integrity": "sha512-bWTAPJmeWQH5suJNYwG0f5cs0p6ho9e6f1Ppoxv5qMosY+s9Ir2+ZLvvHcgA7VTDop4zl/NCHhOVVqU+kd++Ow==", + "requires": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + }, + "dependencies": { + "cookie": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.1.tgz", + "integrity": "sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==" + } + } + }, "lilconfig": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", @@ -18247,19 +19150,9 @@ "lodash.get": { "version": "4.4.2" }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, "lodash.merge": { "version": "4.6.2" }, - "lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" - }, "log-symbols": { "version": "4.1.0", "dev": true, @@ -18459,26 +19352,6 @@ "version": "1.4.1", "dev": true }, - "method-override": { - "version": "3.0.0", - "requires": { - "debug": "3.1.0", - "methods": "~1.1.2", - "parseurl": "~1.3.2", - "vary": "~1.1.2" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0" - } - } - }, "methods": { "version": "1.1.2" }, @@ -18544,6 +19417,7 @@ }, "minimatch": { "version": "3.1.2", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -18552,9 +19426,17 @@ "version": "1.2.6" }, "minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==" + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, + "mnemonist": { + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", + "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", + "requires": { + "obliterator": "^2.0.1" + } }, "mocha": { "version": "10.4.0", @@ -18745,27 +19627,6 @@ "lodash.merge": "^4.6.2" } }, - "morgan": { - "version": "1.9.1", - "requires": { - "basic-auth": "~2.0.0", - "debug": "2.6.9", - "depd": "~1.1.2", - "on-finished": "~2.3.0", - "on-headers": "~1.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0" - } - } - }, "mpath": { "version": "0.9.0" }, @@ -19140,11 +20001,15 @@ "es-abstract": "^1.20.4" } }, - "on-finished": { - "version": "2.3.0", - "requires": { - "ee-first": "1.1.1" - } + "obliterator": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", + "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" + }, + "on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==" }, "on-headers": { "version": "1.0.2" @@ -19187,8 +20052,7 @@ "openapi-types": { "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "peer": true + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" }, "optionator": { "version": "0.9.3", @@ -19258,6 +20122,11 @@ "release-zalgo": "^1.0.0" } }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -19299,7 +20168,8 @@ "dev": true }, "path-is-absolute": { - "version": "1.0.1" + "version": "1.0.1", + "dev": true }, "path-key": { "version": "3.1.1" @@ -19344,6 +20214,27 @@ "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", "dev": true }, + "peek-stream": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", + "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", + "requires": { + "buffer-from": "^1.0.0", + "duplexify": "^3.5.0", + "through2": "^2.0.3" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, "picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -19366,6 +20257,37 @@ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true }, + "pino": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz", + "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==", + "requires": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + } + }, + "pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "requires": { + "split2": "^4.0.0" + } + }, + "pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" + }, "pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -19454,20 +20376,10 @@ "fromentries": "^1.2.0" } }, - "properties-reader": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-2.3.0.tgz", - "integrity": "sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==", - "requires": { - "mkdirp": "^1.0.4" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - } - } + "process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==" }, "proxy-addr": { "version": "2.0.7", @@ -19490,12 +20402,44 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, + "pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "requires": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + }, + "dependencies": { + "duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -19513,6 +20457,11 @@ "version": "1.2.3", "dev": true }, + "quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, "quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -19593,6 +20542,11 @@ "picomatch": "^2.2.1" } }, + "real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" + }, "regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -19693,15 +20647,18 @@ } } }, + "ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==" + }, "reusify": { - "version": "1.0.4", - "dev": true + "version": "1.0.4" }, "rfdc": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", - "dev": true + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" }, "rimraf": { "version": "3.0.2", @@ -19831,6 +20788,14 @@ "is-regex": "^1.1.4" } }, + "safe-regex2": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-4.0.0.tgz", + "integrity": "sha512-Hvjfv25jPDVr3U+4LDzBuZPPOymELG3PYcSk5hcevooo1yxxamQL/bHs/GrEPGmMoMEwRrHVGiCA1pXi97B8Ew==", + "requires": { + "ret": "~0.5.0" + } + }, "safe-stable-stringify": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", @@ -19841,11 +20806,15 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, "semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==" }, "semver-regex": { "version": "4.0.5", @@ -19944,6 +20913,11 @@ "version": "2.0.0", "dev": true }, + "set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -20122,6 +21096,14 @@ "smart-buffer": "^4.2.0" } }, + "sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "requires": { + "atomic-sleep": "^1.0.0" + } + }, "sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -20204,6 +21186,11 @@ "version": "3.0.12", "dev": true }, + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + }, "sprintf-js": { "version": "1.0.3", "dev": true @@ -20226,6 +21213,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" + }, "string_decoder": { "version": "1.1.1", "requires": { @@ -20358,63 +21350,6 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, - "swagger-jsdoc": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", - "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", - "requires": { - "commander": "6.2.0", - "doctrine": "3.0.0", - "glob": "7.1.6", - "lodash.mergewith": "^4.6.2", - "swagger-parser": "^10.0.3", - "yaml": "2.0.0-1" - }, - "dependencies": { - "commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==" - }, - "glob": { - "version": "7.1.6", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "yaml": { - "version": "2.0.0-1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", - "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==" - } - } - }, - "swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", - "requires": { - "@apidevtools/swagger-parser": "10.0.3" - } - }, - "swagger-ui-dist": { - "version": "5.17.14", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", - "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==" - }, - "swagger-ui-express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", - "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "requires": { - "swagger-ui-dist": ">=5.0.0" - } - }, "synckit": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", @@ -20465,6 +21400,14 @@ "version": "0.2.0", "dev": true }, + "thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "requires": { + "real-require": "^0.2.0" + } + }, "through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -20504,6 +21447,11 @@ "is-number": "^7.0.0" } }, + "toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==" + }, "toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -20556,6 +21504,11 @@ "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==" }, + "ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==" + }, "ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -20785,11 +21738,6 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==" }, - "validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==" - }, "vary": { "version": "1.1.2" }, @@ -20995,6 +21943,11 @@ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "requires": {} }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "5.0.8" }, @@ -21069,25 +22022,6 @@ "yocto-queue": { "version": "0.1.0", "dev": true - }, - "z-schema": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", - "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", - "requires": { - "commander": "^9.4.1", - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "validator": "^13.7.0" - }, - "dependencies": { - "commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "optional": true - } - } } } } diff --git a/package.json b/package.json index f76cd4e1..1143708c 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "init:mongo:express": "docker compose -p node-rest-starter -f dev/mongo-express.yml up -d", "init:mongo:down": "docker compose -p node-rest-starter -f dev/mongo.yml down", "clean": "rm -rf dist", - "start": "nodemon ./src/server.ts --exec 'npm run lint && node --inspect -r @swc-node/register'", - "start:dev": "export NODE_ENV=development && npm run start", + "start": "export NODE_ENV=development && nodemon ./src/server.ts --exec 'node --inspect -r @swc-node/register'", + "start:brk": "export NODE_ENV=development && nodemon ./src/server.ts --exec 'node --inspect-brk -r @swc-node/register'", "start:prod": "cd dist && node ./src/server.js", "test": "NODE_ENV=test ALLOW_CONFIG_MUTATIONS=true nodemon -r @swc-node/register src/test.ts", "test:build": "cd dist && node -r @swc-node/register src/test.ts", @@ -36,7 +36,7 @@ "lint:eslint": "eslint \"./**/*.{js,ts}\"", "lint:eslint:fix": "eslint \"./**/*.{js,ts}\" --fix", "tsc:check": "tsc --project tsconfig.json --noEmit", - "tsc:watch": "tsc --watch --noEmit", + "tsc:watch": "tsc --project tsconfig.json --watch --noEmit", "prepare": "husky" }, "nyc": { @@ -48,30 +48,33 @@ ] }, "dependencies": { + "@fastify/compress": "^8.0.1", + "@fastify/cookie": "^11.0.1", + "@fastify/cors": "^10.0.1", + "@fastify/express": "^4.0.1", + "@fastify/formbody": "^8.0.1", + "@fastify/helmet": "^12.0.1", + "@fastify/passport": "^3.0.1", + "@fastify/session": "^11.0.1", + "@fastify/swagger": "^9.2.0", + "@fastify/swagger-ui": "^5.1.0", + "@fastify/type-provider-json-schema-to-ts": "^4.0.1", "agenda": "^4.3.0", - "async": "^3.2.5", - "compression": "1.7", "config": "^3.3.11", "connect-mongo": "^5.1.0", "cors": "^2.8.5", "csv-stringify": "4.0", - "express": "4.21", - "express-actuator": "^1.8.4", - "express-async-errors": "^3.1.1", - "express-json-validator-middleware": "^3.0.1", "express-session": "^1.18.1", + "fastify": "^5.1.0", "glob": "10.3.10", "handlebars": "^4.7.7", - "helmet": "^7.1.0", "http-status-codes": "^2.3.0", "jsonpath": "^1.1.1", "lodash": "4.17.21", "luxon": "^1.28.1", - "method-override": "3.0", "migrate-mongo": "^11.0.0", "mongoose": "^8.4.4", "mongoose-unique-validator": "^5.0.1", - "morgan": "1.9", "multipipe": "^4.0.0", "nodemailer": "6.9.9", "passport": "0.6.0", @@ -80,8 +83,6 @@ "platform": "1.3", "socket.io": "^4.8.0", "socketio-sticky-session": "0.4", - "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1", "through2": "^4.0.2", "typescript": "^5.4.5", "uuid": "^10.0.0", @@ -94,23 +95,16 @@ "@swc/cli": "^0.3.12", "@swc/core": "^1.6.3", "@trivago/prettier-plugin-sort-imports": "^4.3.0", - "@types/compression": "^1.7.5", "@types/config": "^3.3.4", - "@types/express": "4.16", - "@types/express-actuator": "^1.8.3", "@types/express-session": "^1.18.0", "@types/jsonpath": "^0.2.0", "@types/lodash": "^4.14.176", "@types/luxon": "^1.27.1", - "@types/method-override": "^0.0.35", "@types/migrate-mongo": "^10.0.4", "@types/mocha": "^8.2.1", - "@types/morgan": "^1.9.9", "@types/multipipe": "^3.0.5", "@types/passport": "1.0.10", "@types/q": "1.5", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.6", "@types/through2": "^2.0.36", "@types/uuid": "^9.0.8", "@types/yargs": "^17.0.10", @@ -129,8 +123,7 @@ "nodemon": "^3.1.4", "nyc": "^17.0.0", "prettier": "^3.0.0", - "sinon": "^18.0.0", - "swagger-parser": "^10.0.3" + "sinon": "^18.0.0" }, "lint-staged": { "*.{js,ts}": "eslint --fix" diff --git a/src/app/common/csv-stream.service.ts b/src/app/common/csv-stream.service.ts index 3dce4546..753a46fa 100644 --- a/src/app/common/csv-stream.service.ts +++ b/src/app/common/csv-stream.service.ts @@ -68,7 +68,7 @@ class CsvStreamService { // Create an output stream piping the parsing stream to the CSV stream const out = pipe(stream, csv); - out.on('error', (err) => logger.error(err, 'Failed to create CSV')); + out.on('error', (err) => logger.error('Failed to create CSV', err)); return out as Transform; } diff --git a/src/app/common/errors.ts b/src/app/common/errors.ts index de83cca4..4e5c4859 100644 --- a/src/app/common/errors.ts +++ b/src/app/common/errors.ts @@ -8,7 +8,7 @@ export class BaseError extends Error { } export class HttpError extends BaseError { constructor( - public readonly status: StatusCodes, + public readonly statusCode: StatusCodes, name: string, message: string ) { @@ -17,7 +17,7 @@ export class HttpError extends BaseError { toJSON(exposeServerErrors = false): Record { return { - status: this.status, + status: this.statusCode, message: this.message, type: this.name, stack: exposeServerErrors ? this.stack : undefined diff --git a/src/app/common/express/auth-middleware.ts b/src/app/common/express/auth-middleware.ts deleted file mode 100644 index bc298ef6..00000000 --- a/src/app/common/express/auth-middleware.ts +++ /dev/null @@ -1,93 +0,0 @@ -type RequirementFunction = ( - req: unknown, - res: unknown, - next: unknown -) => Promise; - -/** - * Apply the auth requirements as authorization middleware - * @param requirement The requirement function to invoke - */ -export const has = (requirement: RequirementFunction) => { - // Return a function that adapts the requirements to middleware - return (req, res, next) => { - Promise.resolve(requirement(req, req, next)) - .then(() => { - next(); - }) - .catch(next); - }; -}; - -/** - * Apply the array of auth functions in order, using AND logic - */ -export const hasAll = (...requirements: Array) => { - return (req, res, next) => { - Promise.resolve(requiresAll(requirements)(req, res, next)) - .then(() => { - next(); - }) - .catch(next); - }; -}; - -/** - * Apply the array of auth functions in order, using OR logic - */ -export const hasAny = (...requirements: Array) => { - return (req, res, next) => { - Promise.resolve(requiresAny(requirements)(req, res, next)) - .then(() => { - next(); - }) - .catch(next); - }; -}; - -export const requiresAll = (requirements: Array) => { - return (req, res, next) => { - // Apply the requirements - const applyRequirement = (i) => { - if (i < requirements.length) { - return requirements[i](req, res, next).then(() => { - // Success means try the next one - return applyRequirement(++i); - }); - } - // Once they all pass, we're good - return Promise.resolve(); - }; - - return applyRequirement(0); - }; -}; - -export const requiresAny = (requirements: Array) => { - return (req, res, next) => { - // Apply the requirements - let error; - const applyRequirement = (i) => { - if (i < requirements.length) { - return requirements[i](req, res, next) - .then(() => { - // Success means we're done - return Promise.resolve(); - }) - .catch((errorResult) => { - // Failure means keep going - error = errorResult; - return applyRequirement(++i); - }); - } - // If we run out of requirements, fail with the last error - return Promise.reject(error); - }; - - if (requirements.length > 0) { - return applyRequirement(0); - } - // Nothing to check passes - return Promise.resolve(); - }; -}; diff --git a/src/app/common/express/error-handlers.ts b/src/app/common/express/error-handlers.ts deleted file mode 100644 index da1fa76e..00000000 --- a/src/app/common/express/error-handlers.ts +++ /dev/null @@ -1,106 +0,0 @@ -import config from 'config'; -import { ErrorRequestHandler } from 'express'; -import { ValidationError } from 'express-json-validator-middleware'; -import _ from 'lodash'; -import { Error as MongooseError } from 'mongoose'; - -import { logger } from '../../../lib/logger'; -import { BadRequestError, HttpError } from '../errors'; - -export const mongooseValidationErrorHandler: ErrorRequestHandler = ( - err, - req, - res, - next -) => { - // Skip if not mongoose validation error - if (!(err instanceof MongooseError.ValidationError)) { - return next(err); - } - - // Map to format expected by default error handler and pass on - const errors = Object.entries(err.errors ?? {}) - .filter( - ([, innerError]) => innerError instanceof MongooseError.ValidatorError - ) - .map(([field, innerError]) => ({ field, message: innerError.message })); - - return next( - new BadRequestError(errors.map((e) => e.message).join(', '), errors) - ); -}; - -export const jsonSchemaValidationErrorHandler: ErrorRequestHandler = ( - err: Error, - req, - res, - next -) => { - // Skip if not json schema validation error - if (!(err instanceof ValidationError)) { - return next(err); - } - - return next( - new BadRequestError('Schema validation error', err.validationErrors) - ); -}; - -export const defaultErrorHandler: ErrorRequestHandler = ( - err, - req, - res, - next -) => { - if (res.headersSent) { - return next(err); - } - - const exposeServerErrors = config.get('exposeServerErrors'); - - if (err instanceof HttpError) { - logger.error(req.url, err); - - return res.status(err.status).json(err.toJSON(exposeServerErrors)); - } - - const errorResponse = { - status: getStatus(err), - type: err.type ?? 'server-error', - message: getMessage(err), - stack: err.stack - }; - - // Log the error - logger.error(req.url, errorResponse); - - if (errorResponse.status >= 500 && errorResponse.status < 600) { - // Swap the error message if `exposeServerErrors` is disabled - if (!exposeServerErrors) { - errorResponse.message = 'A server error has occurred.'; - delete errorResponse.stack; - } - } - - // Send the response - res.status(errorResponse.status).json(errorResponse); -}; - -const getStatus = (err: Parameters[0]) => { - if (!err.status || err.status < 400 || err.status >= 600) { - return 500; - } - return err.status; -}; - -const getMessage = (err: Parameters[0]) => { - if (_.isString(err)) { - return err; - } - - if (err?.message) { - return `${err.name ?? 'Error'}: ${err.message}`; - } - - return 'Error: Unknown error'; -}; diff --git a/src/app/common/sockets/base-socket.provider.ts b/src/app/common/sockets/base-socket.provider.ts index 32958351..2b132beb 100644 --- a/src/app/common/sockets/base-socket.provider.ts +++ b/src/app/common/sockets/base-socket.provider.ts @@ -1,6 +1,5 @@ -import * as async from 'async'; import config from 'config'; -import { Request, RequestHandler, Response } from 'express'; +import { FastifyRequest } from 'fastify'; import { Socket } from 'socket.io'; import { logger } from '../../../lib/logger'; @@ -35,13 +34,13 @@ export abstract class BaseSocket> { ); } - abstract onDisconnect(); + abstract onDisconnect(): void; - abstract onError(err); + abstract onError(err: Error): void; - abstract onSubscribe(message: MessageType); + abstract onSubscribe(message: MessageType): void; - abstract onUnsubscribe(message: MessageType); + abstract onUnsubscribe(message: MessageType): void; // eslint-disable-next-line @typescript-eslint/no-unused-vars subscribe(topic: string) { @@ -216,82 +215,7 @@ export abstract class BaseSocket> { user: req.user, isAuthenticated: () => req.isAuthenticated(), isUnauthenticated: () => req.isUnauthenticated() - } as Request; - } - - /** - * Gets a placeholder response object that can be used for middleware. It stubs out the status() and send() - * methods, and if there is an error, forwards it to the next handler. - * - * @param next A callback for the async handler. It will be called with an error if the middleware - * callback function passes any message to the UI. - */ - getResponse(next: (err?: unknown) => void) { - function send(data) { - const err = new Error(data?.message ?? 'Unauthorized'); - return next(err); - } - - function status() { - return { - send: send, - json: send - }; - } - - return { - status, - send, - json: send - } as unknown as Response; - } - - /** - * Applies a set of callbacks in series. Each function should accept a request and response object and - * a callback function, in the same format as the Express.js middleware. - * - * @param callbacks - An array of middleware callbacks to execute. - * @param [done] - Optionally, a function that will be called when all middleware has processed, either - * with an error or without. - * - * @returns A promise that will be resolved when all the middleware has run. You can either - * listen for this or pass in a callback. - */ - applyMiddleware( - callbacks: Array, - done?: (err, result) => void - ): Promise { - return new Promise((resolve, reject) => { - // Use the same request for all callbacks - const req = this.getRequest(); - - const tasks = callbacks.map((callback) => { - return (next) => { - // Create a new response for each next() callback - const res = this.getResponse(next); - - // Invoke the callback - callback(req, res, next); - }; - }); - async.series(tasks, (err, results) => { - // Get the result from the last task - const result = results[tasks.length - 1]; - - // Invoke the callback if there is one - if (null != done) { - done(err, result); - } - - // In addition to the optional callback, - // resolve or reject the promise - if (err) { - reject(err); - } else { - resolve(result); - } - }); - }); + } as FastifyRequest; } } diff --git a/src/app/core/access-checker/access-checker.components.yml b/src/app/core/access-checker/access-checker.components.yml deleted file mode 100644 index 0d050456..00000000 --- a/src/app/core/access-checker/access-checker.components.yml +++ /dev/null @@ -1,9 +0,0 @@ -components: - parameters: - keyParam: - in: path - name: key - required: true - schema: - type: string - description: the unique key of the cache entry diff --git a/src/app/core/access-checker/access-checker.controller.ts b/src/app/core/access-checker/access-checker.controller.ts index b9b06dc0..bbfa7201 100644 --- a/src/app/core/access-checker/access-checker.controller.ts +++ b/src/app/core/access-checker/access-checker.controller.ts @@ -1,42 +1,98 @@ -import { StatusCodes } from 'http-status-codes'; +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; +import { FastifyInstance } from 'fastify'; import accessCheckerService from './access-checker.service'; import cacheEntryService from './cache/cache-entry.service'; +import { PagingQueryStringSchema, SearchBodySchema } from '../core.schemas'; +import { requireAdminAccess, requireLogin } from '../user/auth/auth.middleware'; -/** - * Public methods - */ -// Match users given a search fragment -export const matchEntries = async (req, res) => { - const results = await cacheEntryService.search( - req.query, - req.body.s, - req.body.q - ); +export default function (_fastify: FastifyInstance) { + const fastify = _fastify.withTypeProvider(); + fastify.route({ + method: 'POST', + url: '/access-checker/entry/:key', + schema: { + description: 'Trigger cache entry refresh', + tags: ['Access Checker'], + params: { + type: 'object', + properties: { + key: { type: 'string' } + }, + required: ['key'] + } + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + await accessCheckerService.refreshEntry(req.params.key); + return reply.send(); + } + }); - // Create the return copy of the messages - const mappedResults = { - pageNumber: results.pageNumber, - pageSize: results.pageSize, - totalPages: results.totalPages, - totalSize: results.totalSize, - elements: results.elements.map((element) => element.fullCopy()) - }; + fastify.route({ + method: 'DELETE', + url: '/access-checker/entry/:key', + schema: { + description: 'Delete cache entry', + tags: ['Access Checker'], + params: { + type: 'object', + properties: { + key: { type: 'string' } + }, + required: ['key'] + } + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + await cacheEntryService.delete(req.params.key); + return reply.send(); + } + }); - res.status(StatusCodes.OK).json(mappedResults); -}; + fastify.route({ + method: 'POST', + url: '/access-checker/entries/match', + schema: { + description: 'Search cache entries', + tags: ['Access Checker'], + body: SearchBodySchema, + querystring: PagingQueryStringSchema + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + const results = await cacheEntryService.search( + req.query, + req.body.s, + req.body.q + ); -export const refreshEntry = async (req, res) => { - await accessCheckerService.refreshEntry(req.params.key); - res.status(StatusCodes.NO_CONTENT).end(); -}; + // Create the return copy of the messages + const mappedResults = { + pageNumber: results.pageNumber, + pageSize: results.pageSize, + totalPages: results.totalPages, + totalSize: results.totalSize, + elements: results.elements.map((element) => element.fullCopy()) + }; -export const deleteEntry = async (req, res) => { - await cacheEntryService.delete(req.params.key); - res.status(StatusCodes.NO_CONTENT).end(); -}; + return reply.send(mappedResults); + } + }); -export const refreshCurrentUser = async (req, res) => { - await accessCheckerService.refreshEntry(req.user?.providerData?.dnLower); - res.status(StatusCodes.NO_CONTENT).end(); -}; + fastify.route({ + method: 'POST', + url: '/access-checker/user', + schema: { + description: 'Refresh cache entry', + tags: ['Access Checker'] + }, + preValidation: requireLogin, + handler: async function (req, reply) { + await accessCheckerService.refreshEntry( + req.user.providerData?.dnLower as string + ); + return reply.send(); + } + }); +} diff --git a/src/app/core/access-checker/access-checker.routes.ts b/src/app/core/access-checker/access-checker.routes.ts deleted file mode 100644 index a1c1c5dd..00000000 --- a/src/app/core/access-checker/access-checker.routes.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Router } from 'express'; - -import * as accessChecker from './access-checker.controller'; -import { logger } from '../../../lib/logger'; -import { hasAdminAccess, hasLogin } from '../user/user-auth.middleware'; - -/** - * Routes that only apply to the 'proxy-pki' passport strategy - */ -logger.info('Configuring proxy-pki user authentication routes.'); - -const router = Router(); - -/** - * @swagger - * - * /access-checker/entry/{key}: - * post: - * tags: ['Access Checker'] - * description: Trigger cache entry refresh - * parameters: - * - $ref: '#/components/parameters/keyParam' - * responses: - * '204': - * description: Cache entry refresh was submitted successfully - * '400': - * $ref: '#/components/responses/NotAuthenticated' - * delete: - * tags: ['Access Checker'] - * description: Delete cache entry - * parameters: - * - $ref: '#/components/parameters/keyParam' - * responses: - * '204': - * description: Cache entry refresh was submitted deleted - * '400': - * $ref: '#/components/responses/NotAuthenticated' - */ -router - .route('/access-checker/entry/:key') - .post(hasAdminAccess, accessChecker.refreshEntry) - .delete(hasAdminAccess, accessChecker.deleteEntry); - -router - .route('/access-checker/entries/match') - .post(hasAdminAccess, accessChecker.matchEntries); - -// Refresh current user -router - .route('/access-checker/user') - .post(hasLogin, accessChecker.refreshCurrentUser); - -export = router; diff --git a/src/app/core/audit/audit.controller.ts b/src/app/core/audit/audit.controller.ts index f65417f8..ae502b9e 100644 --- a/src/app/core/audit/audit.controller.ts +++ b/src/app/core/audit/audit.controller.ts @@ -1,83 +1,126 @@ -import { StatusCodes } from 'http-status-codes'; +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; +import { FastifyInstance } from 'fastify'; import _ from 'lodash'; import { FilterQuery } from 'mongoose'; import { Audit, AuditDocument } from './audit.model'; import { config, utilService as util } from '../../../dependencies'; +import { PagingQueryStringSchema, SearchBodySchema } from '../core.schemas'; import { Callbacks } from '../export/callbacks'; import * as exportConfigController from '../export/export-config.controller'; -import { IExportConfig } from '../export/export-config.model'; +import { loadExportConfigById } from '../export/export-config.controller'; +import { requireAuditorAccess } from '../user/auth/auth.middleware'; -/** - * Retrieves the distinct values for a field in the Audit collection - */ -export const getDistinctValues = async (req, res) => { - const results = await Audit.distinct(req.query.field, {}); - res.status(StatusCodes.OK).json(results); -}; +export default function (_fastify: FastifyInstance) { + const fastify = _fastify.withTypeProvider(); + fastify.route({ + method: 'GET', + url: '/audit/distinctValues', + schema: { + description: + 'Retrieves the distinct values for a field in the Audit collection', + tags: ['Audit'], + querystring: { + type: 'object', + properties: { + field: { type: 'string' } + }, + required: ['field'] + } + }, + preValidation: requireAuditorAccess, + handler: async function (req, reply) { + const results = await Audit.distinct(req.query.field, {}); + return reply.send(results); + } + }); -export const search = async function (req, res) { - const search = req.body.s || null; - let query = req.body.q || {}; - query = util.toMongoose(query); + fastify.route({ + method: 'POST', + url: '/audit', + schema: { + description: 'Returns audit records matching search criteria', + tags: ['Audit'], + body: SearchBodySchema, + querystring: PagingQueryStringSchema + }, + preValidation: requireAuditorAccess, + handler: async function (req, reply) { + const search = req.body.s ?? null; + let query: Record = req.body.q ?? {}; + query = util.toMongoose(query) as Record; - const page = util.getPage(req.query); - const limit = util.getLimit(req.query); - const sort = util.getSortObj(req.query, 'DESC', '_id'); + const page = util.getPage(req.query); + const limit = util.getLimit(req.query); + const sort = util.getSortObj(req.query, 'DESC', '_id'); - const result = await Audit.find(query) - .containsSearch(search) - .sort(sort) - .paginate(limit, page); + const result = await Audit.find(query) + .containsSearch(search) + .sort(sort) + .paginate(limit, page); - // If any audit objects are strings, try to parse them as json. we may have stringified objects because mongo - // can't support keys with dots - result.elements = result.elements.map((doc) => { - if (_.isString(doc.audit.object)) { - try { - doc.audit.object = JSON.parse(doc.audit.object); + // If any audit objects are strings, try to parse them as json. we may have stringified objects because mongo + // can't support keys with dots + result.elements = result.elements.map((doc) => { + if (_.isString(doc.audit.object)) { + try { + doc.audit.object = JSON.parse(doc.audit.object); + return doc; + } catch (e) { + // ignore + return doc; + } + } return doc; - } catch (e) { - // ignore - return doc; - } + }); + + // Serialize the response + return reply.send(result); } - return doc; }); - // Serialize the response - res.status(StatusCodes.OK).json(result); -}; + fastify.route({ + method: 'GET', + url: '/audit/csv/:id', + schema: { + description: 'Export audit records as CSV file', + tags: ['Audit'] + }, + preValidation: requireAuditorAccess, + preHandler: loadExportConfigById, + handler: function (req, reply) { + const exportConfig = req.exportConfig; + const exportQuery = util.toMongoose( + req.exportQuery + ) as FilterQuery; -export const getCSV = (req, res) => { - const exportConfig = req.exportConfig as IExportConfig; - const exportQuery = util.toMongoose( - req.exportQuery - ) as FilterQuery; + const fileName = `${config.get('app.instanceName')}-${ + exportConfig.type + }.csv`; - const fileName = `${config.get('app.instanceName')}-${exportConfig.type}.csv`; + const columns = exportConfig.config.cols; - const columns = exportConfig.config.cols; + columns.forEach((col) => { + col.title = col.title ?? _.capitalize(col.key); - columns.forEach((col) => { - col.title = col.title ?? _.capitalize(col.key); + switch (col.key) { + case 'created': + col.callback = Callbacks.formatDate(`yyyy-LL-dd HH:mm:ss`); + break; + case 'audit.actor': + col.callback = Callbacks.getValueProperty('name'); + break; + } + }); - switch (col.key) { - case 'created': - col.callback = Callbacks.formatDate(`yyyy-LL-dd HH:mm:ss`); - break; - case 'audit.actor': - col.callback = Callbacks.getValueProperty('name'); - break; - } - }); - - const sort = util.getSortObj(exportConfig.config, 'DESC', '_id'); + const sort = util.getSortObj(exportConfig.config, 'DESC', '_id'); - const cursor = Audit.find(exportQuery) - .containsSearch(exportConfig.config.s) - .sort(sort) - .cursor(); + const cursor = Audit.find(exportQuery) + .containsSearch(exportConfig.config.s) + .sort(sort) + .cursor(); - exportConfigController.exportCSV(req, res, fileName, columns, cursor); -}; + exportConfigController.exportCSV(req, reply, fileName, columns, cursor); + } + }); +} diff --git a/src/app/core/audit/audit.routes.ts b/src/app/core/audit/audit.routes.ts deleted file mode 100644 index 49d6fdbc..00000000 --- a/src/app/core/audit/audit.routes.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Router } from 'express'; - -import * as audit from './audit.controller'; -import { exportConfigById } from '../export/export-config.controller'; -import { hasAuditorAccess } from '../user/user-auth.middleware'; - -const router = Router(); - -router.route('/audit').post(hasAuditorAccess, audit.search); - -router.route('/audit/csv/:exportId').get(audit.getCSV); - -router - .route('/audit/distinctValues') - .get(hasAuditorAccess, audit.getDistinctValues); - -router.param('exportId', exportConfigById); - -export = router; diff --git a/src/app/core/audit/audit.service.ts b/src/app/core/audit/audit.service.ts index 56d502f5..b7364b9a 100644 --- a/src/app/core/audit/audit.service.ts +++ b/src/app/core/audit/audit.service.ts @@ -1,5 +1,6 @@ import config from 'config'; import { Request } from 'express'; +import { FastifyRequest } from 'fastify'; import { Audit, AuditDocument } from './audit.model'; import { utilService } from '../../../dependencies'; @@ -26,7 +27,11 @@ class AuditService { message: string, eventType: string, eventAction: string, - requestOrEventActor: Request | Promise> | Partial, + requestOrEventActor: + | Request + | FastifyRequest + | Promise> + | Partial, eventObject: unknown, eventMetadata = null ): Promise { diff --git a/src/app/core/config/config.controller.ts b/src/app/core/config/config.controller.ts index 7f4c8a04..c5fae4b4 100644 --- a/src/app/core/config/config.controller.ts +++ b/src/app/core/config/config.controller.ts @@ -1,40 +1,17 @@ -import { config } from '../../../dependencies'; -import pkg from '../../../../package.json'; - -export const getSystemConfig = () => { - const toReturn = { - auth: config.get('auth.strategy'), - apiDocs: config.get('apiDocs'), - app: config.get('app'), - requiredRoles: config.get('auth.requiredRoles'), - - version: pkg.version, - banner: config.get('banner'), - copyright: config.get('copyright'), - - contactEmail: config.get('app.contactEmail'), - - feedback: config.get('feedback'), - teams: config.get('teams'), - help: config.get('help'), - - userPreferences: config.get('userPreferences'), - - masqueradeEnabled: - config.get('auth.strategy') === 'proxy-pki' && - config.get('auth.masquerade') === true, - masqueradeUserHeader: config.get('masqueradeUserHeader'), - - allowDeleteUser: config.get('allowDeleteUser') - }; - - return toReturn; -}; - -// Read -export const read = function (req, res) { - /** - * Add unsecured configuration data - */ - res.json(getSystemConfig()); -}; +import { FastifyInstance } from 'fastify'; + +import configService from './config.service'; + +export default function (fastify: FastifyInstance) { + // For now, just a single get for the global client configuration + fastify.route({ + method: 'GET', + url: '/config', + schema: { + hide: true + }, + handler: function (request, reply) { + return reply.send(configService.getSystemConfig()); + } + }); +} diff --git a/src/app/core/config/config.routes.ts b/src/app/core/config/config.routes.ts deleted file mode 100644 index 33ddcdf1..00000000 --- a/src/app/core/config/config.routes.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Router } from 'express'; - -import * as config from './config.controller'; - -const router = Router(); - -// For now, just a single get for the global client configuration -router.route('/config').get(config.read); - -export = router; diff --git a/src/app/core/config/config.controller.spec.ts b/src/app/core/config/config.service.spec.ts similarity index 66% rename from src/app/core/config/config.controller.spec.ts rename to src/app/core/config/config.service.spec.ts index 19b433dd..21874cc6 100644 --- a/src/app/core/config/config.controller.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -1,22 +1,22 @@ -import * as configController from './config.controller'; +import configService from './config.service'; import assert from 'node:assert/strict'; describe('Config Server Controller', () => { describe('#getSystemConfig', () => { it('should not include the mailer configuration', () => { - const systemConfig = configController.getSystemConfig() as any; + const systemConfig = configService.getSystemConfig() as any; assert.equal(systemConfig.mailer, undefined); }); it('should only include a contact email address', () => { - const systemConfig = configController.getSystemConfig(); + const systemConfig = configService.getSystemConfig(); assert(systemConfig.contactEmail); assert(typeof systemConfig.contactEmail, 'string'); }); it('should include apiDocs', () => { - const systemConfig = configController.getSystemConfig(); + const systemConfig = configService.getSystemConfig(); assert(systemConfig.apiDocs); }); }); diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts new file mode 100644 index 00000000..b1e6a1f5 --- /dev/null +++ b/src/app/core/config/config.service.ts @@ -0,0 +1,34 @@ +import { config } from '../../../dependencies'; +import pkg from '../../../../package.json'; + +class ConfigService { + getSystemConfig() { + return { + auth: config.get('auth.strategy'), + apiDocs: config.get('apiDocs'), + app: config.get('app'), + requiredRoles: config.get('auth.requiredRoles'), + + version: pkg.version, + banner: config.get('banner'), + copyright: config.get('copyright'), + + contactEmail: config.get('app.contactEmail'), + + feedback: config.get('feedback'), + teams: config.get('teams'), + help: config.get('help'), + + userPreferences: config.get('userPreferences'), + + masqueradeEnabled: + config.get('auth.strategy') === 'proxy-pki' && + config.get('auth.masquerade') === true, + masqueradeUserHeader: config.get('masqueradeUserHeader'), + + allowDeleteUser: config.get('allowDeleteUser') + }; + } +} + +export default new ConfigService(); diff --git a/src/app/core/core.components.yml b/src/app/core/core.components.yml deleted file mode 100644 index ebcb00e7..00000000 --- a/src/app/core/core.components.yml +++ /dev/null @@ -1,83 +0,0 @@ -components: - schemas: - Error: - type: object - properties: - status: - type: number - type: - type: string - message: - type: string - ResultsPage: - type: object - properties: - pageNumber: - type: integer - pageSize: - type: integer - totalPages: - type: integer - totalSize: - type: integer - example: - pageNumber: 0 - pageSize: 20 - totalPages: 10 - totalSize: 200 - elements: [] - parameters: - pageParam: - in: query - name: page - schema: - type: integer - description: Page number - sizeParam: - in: query - name: size - schema: - type: integer - description: Number of results to return (result per page) - sortParam: - in: query - name: sort - schema: - type: string - description: Field name to sort by - dirParam: - in: query - name: dir - schema: - type: string - description: Sort direction (ASC or DESC) - requestBodies: - SearchCriteria: - description: > - Criteria used for searching records, including a structured query (q) and text search (s) - content: - application/json: - schema: - type: object - properties: - s: - type: string - description: Optional value to search against any fields in a text index for those records. - q: - type: object - description: Structured search object for matching database records. Typically supports MongoDB queries. - example: - s: 'search string' - q: { } - responses: - NotAuthenticated: - description: User not authenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - example: - status: 400 - type: 'missing-credentials' - message: 'Missing credentials' - diff --git a/src/app/core/core.schemas.ts b/src/app/core/core.schemas.ts new file mode 100644 index 00000000..c6d52b76 --- /dev/null +++ b/src/app/core/core.schemas.ts @@ -0,0 +1,35 @@ +export const SearchBodySchema = { + type: 'object', + properties: { + s: { + type: 'string', + description: 'Optional value to search against selected fields.' + }, + q: { + type: 'object', + description: + 'Structured search object for matching database records. Typically supports MongoDB queries.' + } + }, + description: 'Criteria used for searching records' +} as const; + +export const PagingQueryStringSchema = { + type: 'object', + properties: { + page: { type: 'integer', description: 'Page number', examples: [0] }, + size: { + type: 'integer', + description: 'Number of results to return (results per page)', + examples: [20] + }, + sort: { type: 'string', description: 'Field name to sort by' }, + dir: { + anyOf: [ + { type: 'string', enum: ['ASC', 'DESC'] }, + { type: 'integer', enum: [-1, 1] } + ], + description: 'Sort direction' + } + } +} as const; diff --git a/src/app/core/email/email.service.spec.ts b/src/app/core/email/email.service.spec.ts index f4861acc..bddcb86a 100644 --- a/src/app/core/email/email.service.spec.ts +++ b/src/app/core/email/email.service.spec.ts @@ -212,11 +212,7 @@ ${footer} 'src/app/core/user/templates/user-welcome-with-access-email.server.view.html' }; - const options = await emailService.generateMailOptions( - user, - {}, - emailConfig - ); + const options = await emailService.generateMailOptions(user, emailConfig); assert(options); assert.equal(options.header, header); @@ -231,9 +227,7 @@ ${footer} 'src/app/core/user/templates/file-that-doesnt-exist.view.html' }; - await assert.rejects( - emailService.generateMailOptions(user, {}, emailConfig) - ); + await assert.rejects(emailService.generateMailOptions(user, emailConfig)); }); }); }); diff --git a/src/app/core/email/email.service.ts b/src/app/core/email/email.service.ts index 6cc5d58f..5b704b91 100644 --- a/src/app/core/email/email.service.ts +++ b/src/app/core/email/email.service.ts @@ -63,7 +63,6 @@ class EmailService { async generateMailOptions( user, - req, emailTemplateConfig, emailContentData = {}, emailSubjectData = {}, @@ -86,7 +85,7 @@ class EmailService { emailSubjectData ); } catch (error) { - logger.error('Failure rendering template.', { err: error, req: req }); + logger.error('Failure rendering template.', { err: error }); return Promise.reject(error); } diff --git a/src/app/core/export/export-config.controller.ts b/src/app/core/export/export-config.controller.ts index ae3fab8f..ab617cf4 100644 --- a/src/app/core/export/export-config.controller.ts +++ b/src/app/core/export/export-config.controller.ts @@ -1,55 +1,111 @@ import os from 'os'; import { Readable, Transform } from 'stream'; -import { StatusCodes } from 'http-status-codes'; +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; +import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import { ExportColumnDef } from './export-config.model'; import exportConfigService from './export-config.service'; import { auditService, csvStream } from '../../../dependencies'; -import { logger } from '../../../lib/logger'; import { NotFoundError } from '../../common/errors'; -/** - * Request to generate an export configuration in preparation to serve a CSV download soon. The config document will - * expire after a number of minutes (see export-config.server.service). - */ -export const requestExport = async (req, res) => { - if (req.body.config.q) { - // Stringify the query JSON because '$' is reserved in Mongo. - req.body.config.q = JSON.stringify(req.body.config.q); - } +export default function (_fastify: FastifyInstance) { + const fastify = _fastify.withTypeProvider(); + fastify.route({ + method: 'POST', + url: '/requestExport', + schema: { + description: + 'Request to generate an export configuration in preparation to serve a file download soon.', + tags: ['Export'], + body: { + type: 'object', + properties: { + type: { type: 'string' }, + config: { + type: 'object', + properties: { + col: { + type: 'object', + properties: { + key: { type: 'string' }, + title: { type: 'string' } + }, + required: ['key'] + }, + s: { type: 'string' }, + q: { type: 'object' }, + sort: { type: 'string' }, + dir: { + anyOf: [ + { type: 'string', enum: ['ASC', 'DESC'] }, + { type: 'integer', enum: [-1, 1] } + ] + } + } + } + }, + required: ['type'] + }, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + _id: { type: 'string' } + } + } + } + }, + handler: async function (req, reply) { + const { q, ...config } = req.body.config; + if (q) { + // Stringify the query JSON because '$' is reserved in Mongo. + config.q = JSON.stringify(req.body.config.q); + } + req.body.config = config; - const generatedConfig = await exportConfigService.create(req.body); + const generatedConfig = await exportConfigService.create(req.body); - auditService.audit( - `${req.body.type} config created`, - 'export', - 'create', - req, - generatedConfig.auditCopy() - ); + auditService + .audit( + `${req.body.type} config created`, + 'export', + 'create', + req, + generatedConfig.auditCopy() + ) + .then(); - res.status(StatusCodes.OK).json({ _id: generatedConfig._id }); -}; + return reply.send({ _id: generatedConfig._id.toString() }); + } + }); +} /** * Export a CSV file with rows derived from an array of objects or a readable stream * * @param req - * @param res + * @param reply * @param filename the name of the exported file * @param columns the columns to include in the exported CSV file * @param data an array of objects containing data for rows, or an instance of readable */ export const exportCSV = ( - req, - res, + req: FastifyRequest, + reply: FastifyReply, filename: string, columns: ExportColumnDef[], data: Array | Readable ) => { if (null !== data) { - exportStream(req, res, filename, 'text/csv', buildCSVStream(data, columns)); + exportStream( + req, + reply, + filename, + 'text/csv', + buildCSVStream(data, columns) + ); } }; @@ -169,27 +225,28 @@ const buildExportStream = ( * @param stream */ export const exportStream = ( - req, - res, + req: FastifyRequest, + res: FastifyReply, fileName: string, contentType: string, stream: Readable ) => { - res.set('Content-Type', `${contentType};charset=utf-8`); - res.set('Content-Disposition', `attachment;filename="${fileName}"`); - res.set('Transfer-Encoding', 'chunked'); + const reply = res; + reply.type(`${contentType};charset=utf-8`); + reply.header('Content-Disposition', `attachment;filename="${fileName}"`); + reply.header('Transfer-Encoding', 'chunked'); // Pipe each row to the response - stream.pipe(res); + reply.send(stream); // If an error occurs, close the stream stream.on('error', (err) => { - logger.error(`${contentType} export error occurred`, err); + req.log.error(`${contentType} export error occurred`, err); stream.destroy(); // End the download - res.end(); + reply.raw.end(); }); stream.on('end', () => { @@ -197,46 +254,41 @@ export const exportStream = ( }); // If the client drops the connection, stop processing the stream - req.on('close', () => { + req.raw.on('close', () => { if (!stream.destroyed) { - logger.info( + req.log.info( `${contentType} export aborted because client dropped the connection` ); - stream.destroy(); } // End the download. - res.end(); + reply.raw.end(); }); }; -/** - * export middleware - */ -const loadExportConfigById = async (req, res, id) => { - const exportConfig = await exportConfigService.read(id); +export async function loadExportConfigById(req: FastifyRequest) { + const id = req.params['id']; + req.exportConfig = await exportConfigService.read(id); - if (exportConfig == null) { + if (!req.exportConfig) { throw new NotFoundError( 'Export configuration not found. Document may have expired.' ); } - req.exportConfig = exportConfig; - // Parse query from JSON string - req.exportQuery = exportConfig.config.q - ? JSON.parse(exportConfig.config.q) + req.exportQuery = req.exportConfig.config.q + ? JSON.parse(req.exportConfig.config.q) : {}; - auditService.audit( - `${exportConfig.type} CSV config retrieved`, - 'export', - 'export', - req, - exportConfig.auditCopy() - ); -}; -export const exportConfigById = (req, res, next, id) => - loadExportConfigById(req, res, id).then(next).catch(next); + auditService + .audit( + `${req.exportConfig.type} export config retrieved`, + 'export', + 'export', + req, + req.exportConfig.auditCopy() + ) + .then(); +} diff --git a/src/app/core/export/export-config.routes.ts b/src/app/core/export/export-config.routes.ts deleted file mode 100644 index 5a58dbe9..00000000 --- a/src/app/core/export/export-config.routes.ts +++ /dev/null @@ -1,21 +0,0 @@ -import express from 'express'; -import { Validator } from 'express-json-validator-middleware'; - -import * as exportConfig from './export-config.controller'; -import { exportConfigSchema } from './export-config.schemas'; -import { hasAccess } from '../user/user-auth.middleware'; - -const { validate } = new Validator({}); - -const router = express.Router(); - -// Admin post CSV config parameters -router - .route('/requestExport') - .post( - hasAccess, - validate({ body: exportConfigSchema }), - exportConfig.requestExport - ); - -export = router; diff --git a/src/app/core/export/export-config.schemas.ts b/src/app/core/export/export-config.schemas.ts deleted file mode 100644 index 2b9c136c..00000000 --- a/src/app/core/export/export-config.schemas.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { JSONSchema7 } from 'json-schema'; - -export const exportConfigSchema: JSONSchema7 = { - $schema: 'http://json-schema.org/draft-07/schema', - $id: 'node-rest-server/src/app/core/export-config/request', - type: 'object', - title: 'Export Config Request Schema', - description: 'Schema for export config request', - required: ['type'], - properties: { - type: { - $id: '#/properties/type', - type: 'string', - title: 'Type', - description: 'type of the export request', - default: '', - examples: ['user'] - } - } -}; diff --git a/src/app/core/feedback/feedback.components.yml b/src/app/core/feedback/feedback.components.yml deleted file mode 100644 index 5d89e554..00000000 --- a/src/app/core/feedback/feedback.components.yml +++ /dev/null @@ -1,108 +0,0 @@ -components: - schemas: - FeedbackCreate: - type: object - properties: - body: - type: string - url: - type: string - type: - type: string - classification: - type: string - example: - body: 'This is a great tool! Thanks for building it.' - url: 'http://localhost:3000/#/path/to/page' - type: 'Bug' - classification: 'CLASS 1' - Feedback: - allOf: - - $ref: '#/components/schemas/FeedbackCreate' - - type: object - properties: - _id: - type: string - status: - type: string - enum: [New, Open, Closed] - browser: - type: string - os: - type: string - created: - type: number - updated: - type: number - creator: - type: object - properties: - _id: - type: string - username: - type: string - name: - type: string - email: - type: string - organization: - type: string - example: - _id: '12312312312312313' - status: 'New' - browser: 'Chrome 89.0.4389.90' - os: 'Windows' - created: 1617738797588 - updated: 1617738797588 - body: 'This is a great tool! Thanks for building it.' - url: 'http://localhost:3000/#/path/to/page' - type: 'Bug' - classification: 'CLASS 1' - FeedbackPage: - allOf: - - $ref: '#/components/schemas/ResultsPage' - - type: object - properties: - elements: - type: array - items: - $ref: '#/components/schemas/Feedback' - parameters: - feedbackIdParam: - in: path - name: feedbackId - required: true - schema: - type: string - description: the unique id of the feedback - responses: - FeedbackNotFound: - description: Unable to find feedback with the supplied ID - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - example: - status: 404 - type: 'not-found' - message: 'Could not find feedback' - FeedbackUpdateInvalidId: - description: User attempted to update feedback with invalid ID - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - example: - status: 400 - type: 'validation' - message: 'Invalid feedback ID' - FeedbackUpdateAnonymousUser: - description: Anonymous user attempted to update feedback - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - example: - status: 401 - type: 'no-login' - message: 'User is not logged in' diff --git a/src/app/core/feedback/feedback.controller.spec.ts b/src/app/core/feedback/feedback.controller.spec.ts index 9b02a7d4..062713a1 100644 --- a/src/app/core/feedback/feedback.controller.spec.ts +++ b/src/app/core/feedback/feedback.controller.spec.ts @@ -1,19 +1,36 @@ -import { assert, createSandbox } from 'sinon'; +import assert from 'node:assert/strict'; -import * as feedbackController from './feedback.controller'; +import { FastifyInstance } from 'fastify'; +import { assert as sinonAssert, createSandbox } from 'sinon'; + +import feedbackController from './feedback.controller'; import { Feedback } from './feedback.model'; import feedbackService from './feedback.service'; import { auditService } from '../../../dependencies'; -import { getResponseSpy } from '../../../spec/helpers'; -import { User } from '../user/user.model'; +import { fastifyTest } from '../../../spec/fastify'; -describe('Feedback Controller2', () => { - let res; +describe('Feedback Controller', () => { let sandbox; + let app: FastifyInstance; + + before(() => { + app = fastifyTest(feedbackController, { + logger: { level: 'debug' }, + user: { + roles: { + user: true, + admin: true + } + } + }); + }); + after(() => { + app.close(); + }); + beforeEach(() => { sandbox = createSandbox(); - res = getResponseSpy(); }); afterEach(() => { @@ -22,76 +39,86 @@ describe('Feedback Controller2', () => { describe('submitFeedback', () => { it(`should submit feedback successfully`, async () => { - const req = { - body: { - body: 'This is a test', - type: 'Bug', - url: 'http://localhost:3000/some-page?with=param' - }, - user: new User({}) - }; - sandbox.stub(auditService, 'audit').resolves({ audit: {} }); sandbox.stub(feedbackService, 'create').resolves(new Feedback()); sandbox.stub(feedbackService, 'sendFeedbackEmail').resolves(); - await feedbackController.submitFeedback(req, res); + const reply = await app.inject({ + method: 'POST', + url: '/feedback', + payload: { + body: 'This is a test', + type: 'Bug', + url: 'http://localhost:3000/some-page?with=param' + } + }); - assert.calledOnce(feedbackService.create); - assert.calledOnce(auditService.audit); + sinonAssert.calledOnce(feedbackService.create); + sinonAssert.calledOnce(auditService.audit); - assert.calledWith(res.status, 200); - assert.called(res.json); + assert.equal(reply.statusCode, 200); + assert(reply.body); }); - }); - describe('searchFeedback', () => { - it('search returns feedback', async () => { - const req = { body: {} }; + describe('searchFeedback', () => { + it('search returns feedback', async () => { + sandbox.stub(feedbackService, 'search').resolves({}); - sandbox.stub(feedbackService, 'search').resolves(); - await feedbackController.search(req, res); + const reply = await app.inject({ + method: 'POST', + url: '/admin/feedback', + payload: {} + }); - assert.calledOnce(feedbackService.search); + sinonAssert.calledOnce(feedbackService.search); - assert.calledWith(res.status, 200); - assert.called(res.json); + assert.equal(reply.statusCode, 200); + assert(reply.body); + }); }); - }); - - describe('updateFeedbackAssignee', () => { - it('assignee is updated', async () => { - const req = { body: { assignee: 'user' } }; - - sandbox.stub(feedbackService, 'updateFeedbackAssignee').resolves(); - await feedbackController.updateFeedbackAssignee(req, res); - assert.calledOnceWithExactly( - feedbackService.updateFeedbackAssignee, - undefined, - req.body.assignee - ); - - assert.calledWith(res.status, 200); - assert.called(res.json); + describe('updateFeedbackAssignee', () => { + it('assignee is updated', async () => { + sandbox.stub(feedbackService, 'read').resolves({}); + sandbox.stub(feedbackService, 'updateFeedbackAssignee').resolves({}); + + const reply = await app.inject({ + method: 'PATCH', + url: '/admin/feedback/1/assignee', + payload: { assignee: 'user' } + }); + + sinonAssert.calledOnceWithExactly( + feedbackService.updateFeedbackAssignee, + {}, + 'user' + ); + + assert.equal(reply.statusCode, 200); + assert(reply.body); + }); }); - }); - - describe('updateFeedbackStatus', () => { - it('status is updated', async () => { - const req = { body: { status: 'closed' } }; - - sandbox.stub(feedbackService, 'updateFeedbackStatus').resolves(); - await feedbackController.updateFeedbackStatus(req, res); - - assert.calledOnceWithExactly( - feedbackService.updateFeedbackStatus, - undefined, - req.body.status - ); - assert.calledWith(res.status, 200); - assert.called(res.json); + describe('updateFeedbackStatus', () => { + it('status is updated', async () => { + sandbox.stub(feedbackService, 'read').resolves({}); + sandbox.stub(feedbackService, 'updateFeedbackStatus').resolves({}); + + const reply = await app.inject({ + method: 'PATCH', + url: '/admin/feedback/1/status', + payload: { status: 'Closed' } + }); + + sinonAssert.calledOnceWithExactly( + feedbackService.updateFeedbackStatus, + {}, + 'Closed' + ); + + assert.equal(reply.statusCode, 200); + assert(reply.body); + }); }); }); }); diff --git a/src/app/core/feedback/feedback.controller.ts b/src/app/core/feedback/feedback.controller.ts index 67661b2e..2b73578b 100644 --- a/src/app/core/feedback/feedback.controller.ts +++ b/src/app/core/feedback/feedback.controller.ts @@ -1,129 +1,248 @@ -import { StatusCodes } from 'http-status-codes'; +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; +import { FastifyInstance } from 'fastify'; import _ from 'lodash'; import { FilterQuery } from 'mongoose'; -import { FeedbackDocument } from './feedback.model'; +import { FeedbackDocument, Statuses } from './feedback.model'; import feedbackService from './feedback.service'; import { auditService, config } from '../../../dependencies'; import { NotFoundError } from '../../common/errors'; +import { PagingQueryStringSchema, SearchBodySchema } from '../core.schemas'; import { Callbacks } from '../export/callbacks'; import * as exportConfigController from '../export/export-config.controller'; +import { loadExportConfigById } from '../export/export-config.controller'; import { IExportConfig } from '../export/export-config.model'; +import { requireLogin, requireAdminAccess } from '../user/auth/auth.middleware'; + +export default function (_fastify: FastifyInstance) { + const fastify = _fastify.withTypeProvider(); + fastify.route({ + method: 'POST', + url: '/feedback', + schema: { + description: 'Submit feedback to the system', + tags: ['Feedback'], + body: { + type: 'object', + properties: { + body: { + type: 'string', + title: 'Body', + description: 'Body of the feedback', + examples: ['This application is great!'] + }, + type: { + type: 'string', + title: 'Type', + description: 'type/category of the feedback', + examples: ['general feedback'] + }, + url: { + type: 'string', + title: 'URL', + description: 'url from which the feedback was submitted', + examples: ['http://localhost/#/home'] + }, + classification: { + type: 'string', + title: 'Classification', + description: 'Classification level of the feedback', + examples: ['class1'] + } + }, + required: ['body', 'type', 'url'] + } + }, + preValidation: requireLogin, + handler: async function (req, reply) { + const audit = await auditService.audit( + 'Feedback submitted', + 'feedback', + 'create', + req, + req.body + ); + const feedback = await feedbackService.create( + req.user, + req.body, + audit.audit.userSpec + ); + await feedbackService.sendFeedbackEmail(req.user, feedback, req); + + return reply.send(feedback); + } + }); -export const submitFeedback = async function (req, res) { - const audit = await auditService.audit( - 'Feedback submitted', - 'feedback', - 'create', - req, - req.body - ); - const feedback = await feedbackService.create( - req.user, - req.body, - audit.audit.userSpec - ); - await feedbackService.sendFeedbackEmail(req.user, feedback, req); - - res.status(StatusCodes.OK).json(feedback); -}; - -export const adminGetFeedbackCSV = function (req, res) { - const exportConfig = req.exportConfig as IExportConfig; - const exportQuery = req.exportQuery as FilterQuery; - - const fileName = `${config.get('app.instanceName')}-${exportConfig.type}.csv`; - - const columns = exportConfig.config.cols; - // Based on which columns are requested, handle property-specific behavior (ex. callbacks for the - // CSV service to make booleans and dates more human-readable) - columns.forEach((col) => { - col.title = col.title ?? _.capitalize(col.key); - - switch (col.key) { - case 'created': - case 'updated': - col.callback = Callbacks.isoDateString; - break; + fastify.route({ + method: 'POST', + url: '/admin/feedback', + schema: { + description: 'returns feedback matching search criteria', + tags: ['Feedback'], + body: SearchBodySchema, + querystring: PagingQueryStringSchema + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + const results = await feedbackService.search( + req.query, + req.body.s, + req.body.q, + { + path: 'creator', + select: ['username', 'organization', 'name', 'email'] + } + ); + return reply.send(results); } }); - const cursor = feedbackService.cursorSearch( - exportConfig.config, - exportConfig.config.s, - exportQuery, - { - path: 'creator', - select: ['username', 'organization', 'name', 'email'] + fastify.route({ + method: 'PATCH', + url: '/admin/feedback/:id/status', + schema: { + description: 'Updates the status of the feedback with the supplied ID', + tags: ['Feedback'], + body: { + type: 'object', + properties: { + status: { type: 'string', enum: ['New', 'Open', 'Closed'] } + }, + required: ['status'] + }, + params: { + type: 'object', + properties: { + id: { type: 'string' } + }, + required: ['id'] + } + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + const populate = [ + { + path: 'creator', + select: ['username', 'organization', 'name', 'email'] + } + ]; + + const feedback = await feedbackService.read(req.params.id, populate); + if (!feedback) { + throw new NotFoundError('Could not find feedback'); + } + + // Audit feedback status update + await auditService.audit( + 'Feedback status updated', + 'feedback', + 'update', + req, + req.body + ); + + const updatedFeedback = await feedbackService.updateFeedbackStatus( + feedback, + req.body.status as Statuses + ); + return reply.send(updatedFeedback); } - ); - - exportConfigController.exportCSV(req, res, fileName, columns, cursor); -}; - -export const search = async (req, res) => { - const results = await feedbackService.search( - req.query, - req.body.s, - req.body.q, - { - path: 'creator', - select: ['username', 'organization', 'name', 'email'] + }); + + fastify.route({ + method: 'PATCH', + url: '/admin/feedback/:id/assignee', + schema: { + description: ' Updates the assignee of the feedback with the supplied ID', + tags: ['Feedback'], + body: { + type: 'object', + properties: { + assignee: { type: 'string' } + }, + required: ['assignee'] + }, + params: { + type: 'object', + properties: { + id: { type: 'string' } + }, + required: ['id'] + } + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + const populate = [ + { + path: 'creator', + select: ['username', 'organization', 'name', 'email'] + } + ]; + + const feedback = await feedbackService.read(req.params.id, populate); + if (!feedback) { + throw new NotFoundError('Could not find feedback'); + } + + // Audit feedback assignee update + await auditService.audit( + 'Feedback assignee updated', + 'feedback', + 'update', + req, + req.body + ); + + const updatedFeedback = await feedbackService.updateFeedbackAssignee( + feedback, + req.body.assignee + ); + return reply.send(updatedFeedback); } - ); - res.status(StatusCodes.OK).json(results); -}; - -export const updateFeedbackAssignee = async (req, res) => { - // Audit feedback assignee update - await auditService.audit( - 'Feedback assignee updated', - 'feedback', - 'update', - req, - req.body - ); - - const updateFeedbackAssigneePromise = feedbackService.updateFeedbackAssignee( - req.feedback, - req.body.assignee - ); - const updatedFeedback = await updateFeedbackAssigneePromise; - res.status(StatusCodes.OK).json(updatedFeedback); -}; - -export const updateFeedbackStatus = async (req, res) => { - // Audit feedback status update - await auditService.audit( - 'Feedback status updated', - 'feedback', - 'update', - req, - req.body - ); - - const updateFeedbackStatusPromise = feedbackService.updateFeedbackStatus( - req.feedback, - req.body.status - ); - const updatedFeedback = await updateFeedbackStatusPromise; - res.status(StatusCodes.OK).json(updatedFeedback); -}; - -/** - * Feedback middleware - */ -export const feedbackById = async (req, res, next, id) => { - const populate = [ - { - path: 'creator', - select: ['username', 'organization', 'name', 'email'] + }); + + fastify.route({ + method: 'GET', + url: '/admin/feedback/csv/:id', + schema: { + description: 'Export feedback as CSV file', + tags: ['Feedback'] + }, + preValidation: requireAdminAccess, + preHandler: loadExportConfigById, + handler: function (req, reply) { + const exportConfig = req.exportConfig as IExportConfig; + const exportQuery = req.exportQuery as FilterQuery; + + const fileName = `${config.get('app.instanceName')}-${ + exportConfig.type + }.csv`; + + const columns = exportConfig.config.cols; + // Based on which columns are requested, handle property-specific behavior (ex. callbacks for the + // CSV service to make booleans and dates more human-readable) + columns.forEach((col) => { + col.title = col.title ?? _.capitalize(col.key); + + switch (col.key) { + case 'created': + case 'updated': + col.callback = Callbacks.isoDateString; + break; + } + }); + + const cursor = feedbackService.cursorSearch( + exportConfig.config, + exportConfig.config.s, + exportQuery, + { + path: 'creator', + select: ['username', 'organization', 'name', 'email'] + } + ); + + exportConfigController.exportCSV(req, reply, fileName, columns, cursor); } - ]; - - req.feedback = await feedbackService.read(id, populate); - if (!req.feedback) { - throw new NotFoundError('Could not find feedback'); - } - return next(); -}; + }); +} diff --git a/src/app/core/feedback/feedback.routes.ts b/src/app/core/feedback/feedback.routes.ts deleted file mode 100644 index ce8c0446..00000000 --- a/src/app/core/feedback/feedback.routes.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { Router } from 'express'; -import { Validator } from 'express-json-validator-middleware'; - -import * as feedback from './feedback.controller'; -import { createFeedbackSchema } from './feedback.schemas'; -import { exportConfigById } from '../export/export-config.controller'; -import { hasAdminAccess, hasLogin } from '../user/user-auth.middleware'; - -const { validate } = new Validator({}); - -const router = Router(); - -/** - * @swagger - * /feedback: - * post: - * tags: [Feedback] - * description: > - * Echoes the feedback submitted, including the appended timestamp and user ID - * requestBody: - * description: > - * The Feedback that is submitted by the user from some part - * of the application - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/FeedbackCreate' - * responses: - * '200': - * description: Feedback was submitted successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Feedback' - * '401': - * description: Anonymous user attempted to submit feedback - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * example: - * status: 401 - * type: 'no-login' - * message: 'User is not logged in' - */ -router - .route('/feedback') - .post( - hasLogin, - validate({ body: createFeedbackSchema }), - feedback.submitFeedback - ); - -/** - * @swagger - * /admin/feedback: - * post: - * tags: [Feedback] - * description: > - * returns feedback matching search criteria - * requestBody: - * $ref: '#/components/requestBodies/SearchCriteria' - * parameters: - * - $ref: '#/components/parameters/pageParam' - * - $ref: '#/components/parameters/sizeParam' - * - $ref: '#/components/parameters/sortParam' - * - $ref: '#/components/parameters/dirParam' - * responses: - * '200': - * description: Feedback returned successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/FeedbackPage' - * '400': - * $ref: '#/components/responses/NotAuthenticated' - */ -router.route('/admin/feedback').post(hasAdminAccess, feedback.search); - -/** - * @swagger - * /admin/feedback/{feedbackId}/status: - * patch: - * tags: [Feedback] - * description: > - * Updates the status of the feedback with the supplied ID - * parameters: - * - $ref: '#/components/parameters/feedbackIdParam' - * requestBody: - * description: > - * The value to update the Feedback status to - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * enum: [New, Open, Closed] - * responses: - * '200': - * description: Feedback status was updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Feedback' - * '400': - * $ref: '#/components/responses/FeedbackUpdateInvalidId' - * '401': - * $ref: '#/components/responses/FeedbackUpdateAnonymousUser' - * '404': - * $ref: '#/components/responses/FeedbackNotFound' - */ -router - .route('/admin/feedback/:feedbackId/status') - .patch(hasAdminAccess, feedback.updateFeedbackStatus); - -/** - * @swagger - * /admin/feedback/{feedbackId}/assignee: - * patch: - * tags: [Feedback] - * description: > - * Updates the assignee of the feedback with the supplied ID - * parameters: - * - $ref: '#/components/parameters/feedbackIdParam' - * requestBody: - * description: > - * The username to update the Feedback assignee to - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * assignee: - * type: string - * responses: - * '200': - * description: Feedback assignee was updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Feedback' - * '400': - * $ref: '#/components/responses/FeedbackUpdateInvalidId' - * '401': - * $ref: '#/components/responses/FeedbackUpdateAnonymousUser' - * '404': - * $ref: '#/components/responses/FeedbackNotFound' - */ -router - .route('/admin/feedback/:feedbackId/assignee') - .patch(hasAdminAccess, feedback.updateFeedbackAssignee); - -router - .route('/admin/feedback/csv/:exportId') - .get(hasAdminAccess, feedback.adminGetFeedbackCSV); - -router.param('feedbackId', feedback.feedbackById); - -router.param('exportId', exportConfigById); - -export = router; diff --git a/src/app/core/feedback/feedback.schemas.ts b/src/app/core/feedback/feedback.schemas.ts deleted file mode 100644 index 466a01b3..00000000 --- a/src/app/core/feedback/feedback.schemas.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { JSONSchema7 } from 'json-schema'; - -export const createFeedbackSchema: JSONSchema7 = { - $schema: 'http://json-schema.org/draft-07/schema', - $id: 'node-rest-server/src/app/core/feedback/create', - type: 'object', - title: 'Feedback Schema', - description: 'Schema for feedback creation', - required: ['body', 'type', 'url'], - properties: { - body: { - $id: '#/properties/body', - type: 'string', - title: 'Body', - description: 'Body of the feedback', - default: '', - examples: ['This application is great!'] - }, - type: { - $id: '#/properties/type', - type: 'string', - title: 'Type', - description: 'type/category of the feedback', - default: '', - examples: ['general feedback'] - }, - url: { - $id: '#/properties/url', - type: 'string', - title: 'URL', - description: 'url from which the feedback was submitted', - default: '', - examples: ['http://localhost/#/home'] - }, - classification: { - $id: '#/properties/classification', - type: 'string', - title: 'Classification', - description: 'Classification level of the feedback', - default: '', - examples: ['class1'] - } - } -}; diff --git a/src/app/core/feedback/feedback.service.ts b/src/app/core/feedback/feedback.service.ts index 10700ef7..f9c90103 100644 --- a/src/app/core/feedback/feedback.service.ts +++ b/src/app/core/feedback/feedback.service.ts @@ -121,7 +121,6 @@ class FeedbackService { try { const mailOptions = await emailService.generateMailOptions( user, - req, config.get('coreEmails.feedbackEmail'), { url: feedback.url, @@ -130,7 +129,7 @@ class FeedbackService { } ); await emailService.sendMail(mailOptions); - logger.debug(`Sent approved user (${user.username}) alert email`); + logger.debug(`Sent feedback email`); } catch (error) { // Log the error but this shouldn't block logger.error('Failure sending email.', { err: error, req: req }); diff --git a/src/app/core/messages/message.controller.ts b/src/app/core/messages/message.controller.ts index 75c5181f..627c9269 100644 --- a/src/app/core/messages/message.controller.ts +++ b/src/app/core/messages/message.controller.ts @@ -1,126 +1,223 @@ -import { StatusCodes } from 'http-status-codes'; +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; +import { FastifyInstance } from 'fastify'; import messageService from './messages.service'; import { auditService } from '../../../dependencies'; import { NotFoundError } from '../../common/errors'; +import { PagingQueryStringSchema, SearchBodySchema } from '../core.schemas'; +import { + requireAccess, + requireAdminAccess +} from '../user/auth/auth.middleware'; + +export default function (_fastify: FastifyInstance) { + const fastify = _fastify.withTypeProvider(); + fastify.route({ + method: 'POST', + url: '/messages', + schema: { + description: '', + tags: ['Messages'], + body: SearchBodySchema, + querystring: PagingQueryStringSchema + }, + preValidation: requireAccess, + handler: async function (req, reply) { + const results = await messageService.search( + req.query, + req.body.s, + req.body.q + ); + + // Create the return copy of the messages + const mappedResults = { + pageNumber: results.pageNumber, + pageSize: results.pageSize, + totalPages: results.totalPages, + totalSize: results.totalSize, + elements: results.elements.map((element) => element.fullCopy()) + }; + + return reply.send(mappedResults); + } + }); + + fastify.route({ + method: 'POST', + url: '/messages/recent', + schema: { + description: 'Retrieve list of recent messages', + tags: ['Messages'] + }, + preValidation: requireAccess, + handler: async function (req, reply) { + const result = await messageService.getRecentMessages(req.user._id); + return reply.send(result); + } + }); + + fastify.route({ + method: 'POST', + url: '/messages/dismiss', + schema: { + description: 'Dismiss messages', + tags: ['Messages'], + body: { + type: 'object', + properties: { + messageIds: { + type: 'array', + items: { + type: 'string' + } + } + } + } + }, + preValidation: requireAccess, + handler: async function (req, reply) { + const dismissedMessages = await messageService.dismissMessages( + req.body.messageIds, + req.user + ); + + // Audit dismissal of messages + for (const dismissedMessage of dismissedMessages) { + auditService + .audit( + 'message dismissed', + 'message', + 'dismissed', + req, + dismissedMessage.auditCopy() + ) + .then(); + } + + return reply.send(dismissedMessages); + } + }); -// Create -export const create = async (req, res) => { - const message = await messageService.create(req.user, req.body); - - // Publish message - messageService.publishMessage(message); - - // Audit creation of messages - await auditService.audit( - 'message created', - 'message', - 'create', - req, - message.auditCopy() - ); - - res.status(StatusCodes.OK).json(message); -}; - -// Read -export const read = (req, res) => { - res.status(StatusCodes.OK).json(req.message); -}; - -// Update -export const update = async (req, res) => { - // Make a copy of the original message for auditing purposes - const originalMessage = req.message.auditCopy(); - - const updatedMessage = await messageService.update(req.message, req.body); - - // Audit the save action - await auditService.audit('message updated', 'message', 'update', req, { - before: originalMessage, - after: updatedMessage.auditCopy() + fastify.route({ + method: 'POST', + url: '/admin/message', + schema: { + description: 'Create a new message', + tags: ['Messages'] + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + const message = await messageService.create(req.user, req.body); + + // Publish message + messageService.publishMessage(message).then(); + + // Audit creation of messages + await auditService.audit( + 'message created', + 'message', + 'create', + req, + message.auditCopy() + ); + + return reply.send(message); + } }); - res.status(StatusCodes.OK).json(updatedMessage); -}; - -// Delete -export const deleteMessage = async (req, res) => { - await messageService.delete(req.message); - - // Audit the message delete attempt - await auditService.audit( - 'message deleted', - 'message', - 'delete', - req, - req.message.auditCopy() - ); - - res.status(StatusCodes.OK).json(req.message); -}; - -// Search - with paging and sorting -export const search = async (req, res) => { - const results = await messageService.search( - req.query, - req.body.s, - req.body.q - ); - - // Create the return copy of the messages - const mappedResults = { - pageNumber: results.pageNumber, - pageSize: results.pageSize, - totalPages: results.totalPages, - totalSize: results.totalSize, - elements: results.elements.map((element) => element.fullCopy()) - }; - - res.status(StatusCodes.OK).json(mappedResults); -}; - -/** - * Message middleware - */ -export const messageById = async (req, res, next, id) => { - const message = await messageService.read(id); - if (!message) { - return next(new NotFoundError(`Failed to load message: ${id}`)); - } - req.message = message; - return next(); -}; - -/** - * Gets recent messages from the past week that have not been dismissed - */ -export const getRecentMessages = async (req, res) => { - const result = await messageService.getRecentMessages(req.user._id); - res.status(StatusCodes.OK).json(result); -}; - -/** - * When a user dismisses a message, add it to the DismissedMessage collection - * @param req - * @param res - */ -export const dismissMessage = async (req, res) => { - const dismissedMessages = await messageService.dismissMessages( - req.body['messageIds'], - req.user - ); - - // Audit dismissal of messages - for (const dismissedMessage of dismissedMessages) { - auditService.audit( - 'message dismissed', - 'message', - 'dismissed', - req, - dismissedMessage.auditCopy() - ); - } - - res.status(StatusCodes.OK).json(dismissedMessages); -}; + fastify.route({ + method: 'GET', + url: '/admin/message/:id', + schema: { + description: '', + tags: ['Messages'], + params: { + type: 'object', + properties: { + id: { type: 'string' } + }, + required: ['id'] + } + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + const message = await messageService.read(req.params.id); + if (!message) { + throw new NotFoundError(`Failed to load message: ${req.params.id}`); + } + return reply.send(message); + } + }); + + fastify.route({ + method: 'POST', + url: '/admin/message/:id', + schema: { + description: 'Update message details', + tags: ['Messages'], + params: { + type: 'object', + properties: { + id: { type: 'string' } + }, + required: ['id'] + } + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + const message = await messageService.read(req.params.id); + if (!message) { + throw new NotFoundError(`Failed to load message: ${req.params.id}`); + } + + // Make a copy of the original message for auditing purposes + const originalMessage = message.auditCopy(); + + const updatedMessage = await messageService.update(message, req.body); + + // Audit the save action + await auditService.audit('message updated', 'message', 'update', req, { + before: originalMessage, + after: updatedMessage.auditCopy() + }); + + return reply.send(updatedMessage); + } + }); + + fastify.route({ + method: 'DELETE', + url: '/admin/message/:id', + schema: { + description: '', + tags: ['Messages'], + params: { + type: 'object', + properties: { + id: { type: 'string' } + }, + required: ['id'] + } + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + const message = await messageService.read(req.params.id); + if (!message) { + throw new NotFoundError(`Failed to load message: ${req.params.id}`); + } + + await messageService.delete(message); + + // Audit the message delete attempt + await auditService.audit( + 'message deleted', + 'message', + 'delete', + req, + message.auditCopy() + ); + + return reply.send(message); + } + }); +} diff --git a/src/app/core/messages/message.socket.ts b/src/app/core/messages/message.socket.ts index 9ffbb0fc..83be5a97 100644 --- a/src/app/core/messages/message.socket.ts +++ b/src/app/core/messages/message.socket.ts @@ -3,7 +3,7 @@ import { Socket } from 'socket.io'; import { config, socketIO } from '../../../dependencies'; import { logger } from '../../../lib/logger'; import { SocketConfig } from '../../common/sockets/base-socket.provider'; -import { hasAccess } from '../user/user-auth.middleware'; +import { requireAccess } from '../user/auth/auth.middleware'; const emitName = 'message'; @@ -38,8 +38,8 @@ export class MessageSocket extends socketIO.SocketProvider { /** * Handle socket errors */ - override onError(err) { - logger.error(err, 'MessageSocket: Client connection error'); + override onError(err: Error) { + logger.error('MessageSocket: Client connection error', err); this.unsubscribe(this.getTopic()); } @@ -47,17 +47,13 @@ export class MessageSocket extends socketIO.SocketProvider { /** * */ - onSubscribe(payload) { + onSubscribe(payload: unknown) { if (logger.isDebugEnabled()) { - logger.debug( - `MessageSocket: ${emitName}: subscribe event with payload: ${JSON.stringify( - payload - )}` - ); + logger.debug(`MessageSocket: ${emitName}: subscribe event`, { payload }); } // Check that the user account has access - this.applyMiddleware([hasAccess]) + requireAccess(this.getRequest(), null) .then(() => { // Subscribe to the user's message topic const topic = this.getTopic(); @@ -65,22 +61,21 @@ export class MessageSocket extends socketIO.SocketProvider { this._subscriptionCount++; }) .catch((err) => { - logger.warn( - `Unauthorized access to messages by inactive user ${this.getUserId()}: ${err}` - ); + logger.warn('Unauthorized access to messages by inactive user', { + user: this.getUserId(), + err + }); }); } /** * */ - onUnsubscribe(payload) { + onUnsubscribe(payload: unknown) { if (logger.isDebugEnabled()) { - logger.debug( - `MessageSocket: ${emitName}: unsubscribe event with payload: ${JSON.stringify( - payload - )}` - ); + logger.debug(`MessageSocket: ${emitName}: unsubscribe event`, { + payload + }); } const topic = this.getTopic(); diff --git a/src/app/core/messages/messages.routes.ts b/src/app/core/messages/messages.routes.ts deleted file mode 100644 index 3754f0f9..00000000 --- a/src/app/core/messages/messages.routes.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Router } from 'express'; - -import * as messages from './message.controller'; -import { hasAccess, hasAdminAccess } from '../user/user-auth.middleware'; - -const router = Router(); - -// Create Message -router.route('/admin/message').post(hasAdminAccess, messages.create); - -// Search messages -router.route('/messages').post(hasAccess, messages.search); - -router.route('/messages/recent').post(hasAccess, messages.getRecentMessages); - -// Dismiss a message -router.route('/messages/dismiss').post(hasAccess, messages.dismissMessage); - -// Admin retrieve/update/delete -router - .route('/admin/message/:msgId') - .get(hasAccess, messages.read) - .post(hasAdminAccess, messages.update) - .delete(hasAdminAccess, messages.deleteMessage); - -// Bind the message middleware -router.param('msgId', messages.messageById); - -export = router; diff --git a/src/app/core/metrics/metrics.controller.ts b/src/app/core/metrics/metrics.controller.ts index b0a1059c..837789df 100644 --- a/src/app/core/metrics/metrics.controller.ts +++ b/src/app/core/metrics/metrics.controller.ts @@ -1,9 +1,19 @@ -import { StatusCodes } from 'http-status-codes'; +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; +import { FastifyInstance } from 'fastify'; import { metricsLogger } from '../../../lib/logger'; -// handle a generic client metrics event -export const handleEvent = (req, res) => { - metricsLogger.log('', { metricsEvent: req.body }); - res.status(StatusCodes.OK).send(); -}; +export default function (_fastify: FastifyInstance) { + const fastify = _fastify.withTypeProvider(); + fastify.route({ + method: 'GET', + url: '/client-metrics', + schema: { + hide: true + }, + handler: function (req, reply) { + metricsLogger.log('', { metricsEvent: req.body }); + return reply.send(); + } + }); +} diff --git a/src/app/core/metrics/metrics.routes.ts b/src/app/core/metrics/metrics.routes.ts deleted file mode 100644 index f1910cfa..00000000 --- a/src/app/core/metrics/metrics.routes.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Router } from 'express'; - -import * as metrics from './metrics.controller'; - -const router = Router(); - -// For now, just a single get for the global client configuration -router.route('/client-metrics').post(metrics.handleEvent); - -export = router; diff --git a/src/app/core/notifications/notification.controller.ts b/src/app/core/notifications/notification.controller.ts index 139e0281..d863ce67 100644 --- a/src/app/core/notifications/notification.controller.ts +++ b/src/app/core/notifications/notification.controller.ts @@ -1,14 +1,32 @@ -import { StatusCodes } from 'http-status-codes'; +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; +import { FastifyInstance } from 'fastify'; import notificationsService from './notification.service'; +import { PagingQueryStringSchema, SearchBodySchema } from '../core.schemas'; +import { requireAccess } from '../user/auth/auth.middleware'; -export const search = async (req, res) => { - // Get search and query parameters - const query = req.body.q ?? {}; +export default function (_fastify: FastifyInstance) { + const fastify = _fastify.withTypeProvider(); + fastify.route({ + method: 'POST', + url: '/notifications', + schema: { + hide: true, + description: '', + tags: ['Notifications'], + body: SearchBodySchema, + querystring: PagingQueryStringSchema + }, + preValidation: requireAccess, + handler: async function (req, reply) { + // Get search and query parameters + const query = req.body.q ?? {}; - // Always need to filter by user making the service call - query.user = req.user._id; + // Always need to filter by user making the service call + query.user = req.user._id; - const result = await notificationsService.search(req.query, query); - res.status(StatusCodes.OK).json(result); -}; + const result = await notificationsService.search(req.query, query); + return reply.send(result); + } + }); +} diff --git a/src/app/core/notifications/notification.routes.ts b/src/app/core/notifications/notification.routes.ts deleted file mode 100644 index a1ac8e23..00000000 --- a/src/app/core/notifications/notification.routes.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Router } from 'express'; - -import * as notifications from './notification.controller'; -import { hasAccess } from '../user/user-auth.middleware'; - -const router = Router(); - -router.route('/notifications').post(hasAccess, notifications.search); - -export = router; diff --git a/src/app/core/notifications/notification.socket.ts b/src/app/core/notifications/notification.socket.ts index f55e47e8..b87cfd44 100644 --- a/src/app/core/notifications/notification.socket.ts +++ b/src/app/core/notifications/notification.socket.ts @@ -3,7 +3,7 @@ import { Socket } from 'socket.io'; import { config, socketIO } from '../../../dependencies'; import { logger } from '../../../lib/logger'; import { SocketConfig } from '../../common/sockets/base-socket.provider'; -import { hasAccess } from '../user/user-auth.middleware'; +import { requireAccess } from '../user/auth/auth.middleware'; const emitName = 'alert'; @@ -66,7 +66,7 @@ export class NotificationSocket extends socketIO.SocketProvider { } // Check that the user account has access - this.applyMiddleware([hasAccess]) + requireAccess(this.getRequest(), null) .then(() => { // Subscribe to the user's notification topic const topic = this.getTopic(); diff --git a/src/app/core/teams/team-auth.middleware.ts b/src/app/core/teams/team-auth.middleware.ts new file mode 100644 index 00000000..29a96ed2 --- /dev/null +++ b/src/app/core/teams/team-auth.middleware.ts @@ -0,0 +1,29 @@ +import { FastifyReply, FastifyRequest } from 'fastify'; + +import { TeamRoles } from './team-role.model'; +import teamsService from './teams.service'; +import { BadRequestError } from '../../common/errors'; +import { AuthRequirementFunction } from '../user/auth/auth.middleware'; + +export function requireTeamRole(role: TeamRoles): AuthRequirementFunction { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + return function (req: FastifyRequest, rep: FastifyReply): Promise { + req.log.debug('Executing auth middleware: requireTeamRoles'); + + // Verify that the user and team are on the request + const user = req.user; + if (null == user) { + return Promise.reject(new BadRequestError('No user for request')); + } + const team = req.team; + if (null == team) { + return Promise.reject(new BadRequestError('No team for request')); + } + + return teamsService.meetsRoleRequirement(user, team, role); + }; +} + +export const requireTeamAdminRole = requireTeamRole(TeamRoles.Admin); +export const requireTeamEditorRole = requireTeamRole(TeamRoles.Editor); +export const requireTeamMemberRole = requireTeamRole(TeamRoles.Member); diff --git a/src/app/core/teams/team.components.yml b/src/app/core/teams/team.components.yml deleted file mode 100644 index 32f46f5c..00000000 --- a/src/app/core/teams/team.components.yml +++ /dev/null @@ -1,265 +0,0 @@ -components: - schemas: - NewTeam: - type: object - properties: - description: - type: string - description: The longer description of the team, as provided by team admins - implicitMembers: - type: boolean - description: If true, automatically adds members to this team based on their roles - name: - type: string - description: Shorter name for the team - requiresExternalRoles: - type: array - items: - type: string - description: External roles that are required for a user to access this team - requiresExternalTeams: - type: array - items: - type: string - description: External teams that are required for a user to access this team - Team: - allOf: - - $ref: '#/components/schemas/NewTeam' - - type: object - properties: - _id: - type: string - description: Unique ID of the Team - ancestors: - type: array - description: Nested teams provide access to resources in a top-down manner (i.e. members of a parent team have the same accesses to resources owned by any child teams). - items: - type: string - description: Team IDs - created: - type: number - description: Timestamp of when the team was created, in milliseconds since the unix epoch. - creator: - type: string - description: Unique ID of the user who created the team - creatorName: - type: string - description: Full display name of the user who created the team - isMember: - type: boolean - description: Whether the user making the request is a member of the team or not - TeamMember: - type: object - properties: - _id: - type: string - description: Unique ID of the Team Member, matching their unique User ID - name: - type: string - description: Full name of the Team Member - username: - type: string - description: Unique name for the User that can be shared across systems and/or used to login. - teams: - type: array - items: - type: object - properties: - _id: - type: string - description: Unique ID of the Team - role: - type: string - enum: [requester, member, editor, admin] - description: The role that this member has on this team - parameters: - teamIdParam: - in: path - name: teamId - required: true - schema: - type: string - description: the unique id of the team - memberIdParam: - in: path - name: memberId - required: true - schema: - type: string - description: the unique id of the member - requestBodies: - CreateTeam: - description: Values used to create a new team - content: - application/json: - schema: - type: object - properties: - firstAdmin: - type: string - description: User ID of the first Team Admin who can grant access to other members - team: - $ref: '#/components/schemas/NewTeam' - UpdateTeam: - description: Values used to update an existing team - content: - application/json: - schema: - $ref: '#/components/schemas/NewTeam' - RequestNewTeam: - description: Request a new team - content: - application/json: - schema: - type: object - properties: - org: - type: string - description: The team organization - aoi: - type: string - description: The team AOI - description: - type: string - description: The description of the request - UpdateMemberRole: - description: Update a member's role - content: - application/json: - schema: - type: object - properties: - role: - type: string - enum: [requester, member, editor, admin] - description: The role to give to this new member in the team - GetAncestorTeams: - description: From a set of Team IDs, requests a list of Ancestor Team IDs - content: - application/json: - schema: - type: object - properties: - teamIds: - type: array - description: Listing of Team IDs whose Ancestor Teams are requested - items: - type: string - description: Unique Team ID - SearchTeamMembers: - description: Searches for team members - content: - application/json: - schema: - type: object - properties: - teamIds: - type: array - description: Listing of user IDs who should be added to the team - items: - type: string - description: Unique User ID - AddTeamMembers: - description: Adds members to a Team by their user IDs - content: - application/json: - schema: - type: object - properties: - newMembers: - type: array - description: The members to add - items: - type: object - properties: - _id: - type: string - description: Unique user ID of the member to add - role: - type: string - enum: [requester, member, editor, admin] - description: The role to give to this new member in the team - AddTeamMember: - description: Adds a member to a Team - content: - application/json: - schema: - type: object - properties: - role: - type: string - enum: [requester, member, editor, admin] - description: The role to give to this new member in the team - responses: - CreatedTeam: - description: Team was created - content: - application/json: - schema: - $ref: '#/components/schemas/Team' - GetTeam: - description: Returns the details of the Team - content: - application/json: - schema: - $ref: '#/components/schemas/Team' - UpdateTeam: - description: The details of the Team that was updated - content: - application/json: - schema: - $ref: '#/components/schemas/Team' - DeleteTeam: - description: The details of the Team that was deleted - content: - application/json: - schema: - $ref: '#/components/schemas/Team' - RequestTeamAccess: - description: The request to join a team was submitted - RequestNewTeam: - description: The request for a new team was successfully submitted - TeamIds: - description: List of unique Team IDs - content: - application/json: - schema: - type: array - description: List of unique Team IDs - items: - type: string - description: Unique Team ID - AddedTeamMembers: - description: The members were successfully added to the team - AddedTeamMember: - description: The new member was successfully added to the team - RemovedTeamMember: - description: A member was successfully removed from the team - UpdatedTeamMemberRole: - description: The member's role in the team has been successfully updated - TeamListing: - description: The Teams that match the search criteria - content: - application/json: - schema: - allOf: - - $ref: '#/components/schemas/ResultsPage' - - type: object - properties: - elements: - type: array - items: - $ref: '#/components/schemas/Team' - TeamMembers: - description: The Team Members that match the search criteria - content: - application/json: - schema: - allOf: - - $ref: '#/components/schemas/ResultsPage' - - type: object - properties: - elements: - type: array - items: - $ref: '#/components/schemas/TeamMember' diff --git a/src/app/core/teams/team.model.ts b/src/app/core/teams/team.model.ts index 2a9bc215..d6f4ff32 100644 --- a/src/app/core/teams/team.model.ts +++ b/src/app/core/teams/team.model.ts @@ -34,7 +34,10 @@ export interface ITeam { export interface ITeamMethods { auditCopy(): Record; - auditCopyTeamMember(user: UserDocument): Record; + auditCopyTeamMember( + user: UserDocument, + role?: string + ): Record; } export type TeamDocument = HydratedDocument< @@ -141,7 +144,7 @@ TeamSchema.methods.auditCopy = function (): Record { // Copy a team role for audit logging TeamSchema.methods.auditCopyTeamMember = function ( user: UserDocument, - role = null + role?: string ): Record { const toReturn: Record = {}; diff --git a/src/app/core/teams/teams.controller.spec.ts b/src/app/core/teams/teams.controller.spec.ts deleted file mode 100644 index 4e5bf360..00000000 --- a/src/app/core/teams/teams.controller.spec.ts +++ /dev/null @@ -1,410 +0,0 @@ -import assert from 'node:assert'; - -import { Request } from 'express'; -import { assert as sinonAssert, createSandbox, match, stub } from 'sinon'; - -import { Team, TeamDocument } from './team.model'; -import * as teamsController from './teams.controller'; -import teamsService from './teams.service'; -import { auditService } from '../../../dependencies'; -import { getResponseSpy } from '../../../spec/helpers'; -import { User, UserDocument } from '../user/user.model'; -import userService from '../user/user.service'; - -/** - * Unit tests - */ -describe('Teams Controller:', () => { - let res; - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - res = getResponseSpy(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe('create', () => { - it('create successful', async () => { - const req = { - body: {}, - user: new User() - }; - sandbox.stub(auditService, 'audit').resolves(); - sandbox.stub(teamsService, 'create').resolves(new Team()); - - await teamsController.create(req, res); - - sinonAssert.calledOnce(teamsService.create); - sinonAssert.calledOnce(auditService.audit); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('read', () => { - it('persona found', async () => { - const req = { - team: new Team(), - user: new User() - }; - - await teamsController.read(req, res); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('update', () => { - it('team found', async () => { - const req = { - team: new Team(), - user: new User() - }; - - sandbox.stub(auditService, 'audit').resolves(); - sandbox.stub(teamsService, 'update').resolves(req.team); - - await teamsController.update(req, res); - - sinonAssert.calledOnce(auditService.audit); - sinonAssert.calledOnce(teamsService.update); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('delete', () => { - it('team found', async () => { - const req = { - team: new Team(), - user: new User() - }; - - sandbox.stub(auditService, 'audit').resolves(); - sandbox.stub(teamsService, 'delete').resolves(); - - await teamsController.deleteTeam(req, res); - - sinonAssert.calledOnce(auditService.audit); - sinonAssert.calledOnce(teamsService.delete); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('search', () => { - it('search returns teams', async () => { - const req = { - body: {} - }; - - sandbox.stub(teamsService, 'search').resolves(); - - await teamsController.search(req, res); - - sinonAssert.calledOnce(teamsService.search); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('requestNewTeam', () => { - it('request handled', async () => { - const req = { - team: new Team(), - user: new User(), - body: {} - }; - - sandbox.stub(auditService, 'audit').resolves(); - sandbox.stub(teamsService, 'requestNewTeam').resolves(); - - await teamsController.requestNewTeam(req, res); - - sinonAssert.calledOnce(teamsService.requestNewTeam); - sinonAssert.calledOnce(auditService.audit); - - sinonAssert.calledWith(res.status, 204); - sinonAssert.called(res.end); - sinonAssert.notCalled(res.json); - }); - }); - - describe('requestAccess', () => { - it('request handled', async () => { - const req = { - team: new Team(), - user: new User(), - body: {} - }; - - sandbox.stub(teamsService, 'requestAccessToTeam').resolves(); - - await teamsController.requestAccess(req, res); - - sinonAssert.calledOnce(teamsService.requestAccessToTeam); - - sinonAssert.calledWith(res.status, 204); - sinonAssert.called(res.end); - sinonAssert.notCalled(res.json); - }); - }); - - describe('searchMembers', () => { - it('search returns team members', async () => { - const req = { - body: {} - }; - - sandbox.stub(teamsService, 'updateMemberFilter').returns({}); - sandbox.stub(userService, 'searchUsers').resolves({ elements: [] }); - - await teamsController.searchMembers(req, res); - - sinonAssert.calledOnce(userService.searchUsers); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('addMember', () => { - it('request handled', async () => { - const req = { - team: new Team(), - user: new User(), - userParam: new User(), - body: {} - }; - - sandbox.stub(auditService, 'audit').resolves(); - sandbox.stub(teamsService, 'addMemberToTeam').resolves(); - - await teamsController.addMember(req, res); - - sinonAssert.calledOnce(auditService.audit); - sinonAssert.calledOnce(teamsService.addMemberToTeam); - - sinonAssert.calledWith(res.status, 204); - sinonAssert.called(res.end); - sinonAssert.notCalled(res.json); - }); - }); - - describe('addMembers', () => { - it('request handled', async () => { - const req = { - team: new Team(), - user: new User(), - body: { - newMembers: [ - { - _id: '12345', - role: 'admin' - }, - { - _id: '11111', - role: 'admin' - } - ] - } - }; - - sandbox.stub(auditService, 'audit').resolves(); - - const readUserStub = sandbox.stub(userService, 'read'); - readUserStub.onCall(0).resolves({}); - readUserStub.onCall(1).resolves(); - - sandbox.stub(teamsService, 'addMemberToTeam').resolves(); - - await teamsController.addMembers(req, res); - - sinonAssert.calledOnce(auditService.audit); - sinonAssert.calledOnce(teamsService.addMemberToTeam); - - sinonAssert.calledWith(res.status, 204); - sinonAssert.called(res.end); - sinonAssert.notCalled(res.json); - }); - }); - - describe('removeMember', () => { - it('request handled', async () => { - const req = { - team: new Team(), - user: new User(), - userParam: new User(), - body: {} - }; - - sandbox.stub(auditService, 'audit').resolves(); - sandbox.stub(teamsService, 'removeMemberFromTeam').resolves(); - - await teamsController.removeMember(req, res); - - sinonAssert.calledOnce(auditService.audit); - sinonAssert.calledOnce(teamsService.removeMemberFromTeam); - - sinonAssert.calledWith(res.status, 204); - sinonAssert.called(res.end); - sinonAssert.notCalled(res.json); - }); - }); - - describe('updateMemberRole', () => { - it('request handled', async () => { - const req = { - team: new Team(), - user: new User(), - userParam: new User(), - body: {} - }; - - sandbox.stub(auditService, 'audit').resolves(); - sandbox.stub(teamsService, 'updateMemberRole').resolves(); - - await teamsController.updateMemberRole(req, res); - - sinonAssert.calledOnce(auditService.audit); - sinonAssert.calledOnce(teamsService.updateMemberRole); - - sinonAssert.calledWith(res.status, 204); - sinonAssert.called(res.end); - sinonAssert.notCalled(res.json); - }); - }); - - describe('teamById', () => { - it('team found', async () => { - sandbox.stub(teamsService, 'read').resolves({ - toObject: () => { - return { - type: 'type', - owner: {} - }; - } - }); - - const nextFn = stub(); - const req = { user: {}, body: {} } as Request & { team: TeamDocument }; - - await teamsController.teamById(req, {}, nextFn, 'id'); - - assert(req.team); - sinonAssert.calledWith(nextFn); - }); - - it('team not found', async () => { - sandbox.stub(teamsService, 'read').resolves(); - - const nextFn = stub(); - const req = { user: {} } as Request & { team: TeamDocument }; - - await teamsController.teamById(req, {}, nextFn, 'id'); - - assert.equal(req.team, undefined); - sinonAssert.calledWith( - nextFn, - match.instanceOf(Error).and(match.has('message', 'Could not find team')) - ); - }); - }); - - describe('teamMemberById', () => { - it('team found', async () => { - sandbox.stub(userService, 'read').resolves({ - toObject: () => { - return { - type: 'type', - owner: {} - }; - } - }); - - const nextFn = stub(); - const req = { user: {}, body: {} } as Request & { - userParam: UserDocument; - }; - - await teamsController.teamMemberById(req, {}, nextFn, 'id'); - - assert(req.userParam); - sinonAssert.calledWith(nextFn); - }); - - it('team not found', async () => { - sandbox.stub(userService, 'read').resolves(); - - const nextFn = stub(); - const req = { user: {} } as Request & { userParam: UserDocument }; - - await teamsController.teamMemberById(req, {}, nextFn, 'id'); - - assert.equal(req.userParam, undefined); - sinonAssert.calledWith( - nextFn, - match - .instanceOf(Error) - .and(match.has('message', 'Failed to load team member')) - ); - }); - }); - - describe('requiresRole', () => { - const requiresRoleHelper = (method, testFunction) => { - describe(method, () => { - it('user not found', async () => { - sandbox.stub(teamsService, 'meetsRoleRequirement').resolves(); - - const req = {}; - - await assert.rejects(testFunction(req), { - status: 400, - message: 'No user for request' - }); - - sinonAssert.notCalled(teamsService.meetsRoleRequirement); - }); - - it('team not found', async () => { - sandbox.stub(teamsService, 'meetsRoleRequirement').resolves(); - - const req = { user: {} }; - - await assert.rejects(testFunction(req), { - status: 400, - message: 'No team for request' - }); - - sinonAssert.notCalled(teamsService.meetsRoleRequirement); - }); - - it('team not found', async () => { - sandbox.stub(teamsService, 'meetsRoleRequirement').resolves(); - - const req = { user: {}, team: {} }; - - await assert.doesNotReject(testFunction(req)); - - sinonAssert.calledOnce(teamsService.meetsRoleRequirement); - }); - }); - }; - - requiresRoleHelper('requiresAdmin', teamsController.requiresAdmin); - - requiresRoleHelper('requiresEditor', teamsController.requiresEditor); - - requiresRoleHelper('requiresMember', teamsController.requiresMember); - }); -}); diff --git a/src/app/core/teams/teams.controller.ts b/src/app/core/teams/teams.controller.ts index c4ce4fdc..e7ae37fd 100644 --- a/src/app/core/teams/teams.controller.ts +++ b/src/app/core/teams/teams.controller.ts @@ -1,227 +1,435 @@ -import { StatusCodes } from 'http-status-codes'; +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; +import { FastifyInstance, FastifyRequest } from 'fastify'; +import { + requireTeamAdminRole, + requireTeamMemberRole +} from './team-auth.middleware'; import { TeamRoles } from './team-role.model'; import teamsService from './teams.service'; import { utilService, auditService } from '../../../dependencies'; -import { BadRequestError, NotFoundError } from '../../common/errors'; +import { NotFoundError } from '../../common/errors'; +import { PagingQueryStringSchema, SearchBodySchema } from '../core.schemas'; +import { + requireAccess, + requireAdminRole, + requireAny, + requireEditorAccess +} from '../user/auth/auth.middleware'; import userService from '../user/user.service'; -/** - * Create a new team. The team creator is automatically added as an admin - */ -export const create = async (req, res) => { - const result = await teamsService.create( - req.body.team, - req.user, - req.body.firstAdmin - ); - - // Audit the creation action - await auditService.audit( - 'team created', - 'team', - 'create', - req, - result.auditCopy() - ); - - res.status(StatusCodes.OK).json(result); -}; - -/** - * Read the team - */ -export const read = (req, res) => { - res.status(StatusCodes.OK).json(req.team); -}; - -/** - * Update the team metadata - */ -export const update = async (req, res) => { - // Make a copy of the original team for auditing purposes - const originalTeam = req.team.auditCopy(); - - const result = await teamsService.update(req.team, req.body); - - await auditService.audit('team updated', 'team', 'update', req, { - before: originalTeam, - after: result.auditCopy() +export default function (_fastify: FastifyInstance) { + const fastify = _fastify.withTypeProvider(); + fastify.route({ + method: 'POST', + url: '/team', + schema: { + description: 'Creates a new Team', + tags: ['Team'], + body: { + type: 'object', + properties: { + team: { type: 'object' }, + firstAdmin: { type: 'string' } + }, + required: ['team'] + } + }, + preValidation: requireEditorAccess, + handler: async function (req, reply) { + const result = await teamsService.create( + req.body.team, + req.user, + req.body.firstAdmin + ); + + // Audit the creation action + await auditService.audit( + 'team created', + 'team', + 'create', + req, + result.auditCopy() + ); + + return reply.send(result); + } }); - res.status(StatusCodes.OK).json(result); -}; - -/** - * Delete the team - */ -export const deleteTeam = async (req, res) => { - await teamsService.delete(req.team); - - // Audit the team delete attempt - await auditService.audit( - 'team deleted', - 'team', - 'delete', - req, - req.team.auditCopy() - ); - - res.status(StatusCodes.OK).json(req.team); -}; - -/** - * Search the teams, includes paging and sorting - */ -export const search = async (req, res) => { - // Get search and query parameters - const search = req.body.s ?? null; - const query = utilService.toMongoose(req.body.q ?? {}); - - const result = await teamsService.search(req.query, query, search, req.user); - res.status(StatusCodes.OK).json(result); -}; - -export const getAncestorTeamIds = async (req, res) => { - const result = await teamsService.getAncestorTeamIds(req.body.teamIds); - res.status(StatusCodes.OK).json(result); -}; - -export const requestNewTeam = async (req, res) => { - const user = req.user; - const org = req.body.org ?? null; - const aoi = req.body.aoi ?? null; - const description = req.body.description ?? null; - - await teamsService.requestNewTeam(org, aoi, description, user, req); - - await auditService.audit('new team requested', 'team', 'request', req, { - org, - aoi, - description + fastify.route({ + method: 'POST', + url: '/teams', + schema: { + description: 'Returns teams that match the search criteria', + tags: ['Team'], + body: SearchBodySchema, + querystring: PagingQueryStringSchema + }, + preValidation: requireAccess, + handler: async function (req, reply) { + // Get search and query parameters + const search = req.body.s ?? null; + const query = utilService.toMongoose(req.body.q ?? {}); + + const result = await teamsService.search( + req.query, + query, + search, + req.user + ); + return reply.send(result); + } }); - res.status(StatusCodes.NO_CONTENT).end(); -}; - -export const requestAccess = async (req, res) => { - await teamsService.requestAccessToTeam(req.user, req.team, req); - res.status(StatusCodes.NO_CONTENT).end(); -}; - -/** - * Search the members of the team, includes paging and sorting - */ -export const searchMembers = async (req, res) => { - // Get search and query parameters - const search = req.body.s ?? ''; - const query = teamsService.updateMemberFilter( - utilService.toMongoose(req.body.q ?? {}), - req.team - ); - - const results = await userService.searchUsers(req.query, query, search); - - // Create the return copy of the messages - const mappedResults = { - pageNumber: results.pageNumber, - pageSize: results.pageSize, - totalPages: results.totalPages, - totalSize: results.totalSize, - elements: results.elements.map((element) => { - return { - ...element.filteredCopy(), - teams: element.teams.filter((team) => team._id.equals(req.team._id)) - }; - }) - }; - - res.status(StatusCodes.OK).json(mappedResults); -}; - -/** - * Add a member to a team, defaulting to read-only access - */ -export const addMember = async (req, res) => { - const role: TeamRoles = req.body.role ?? TeamRoles.Member; - - await teamsService.addMemberToTeam(req.userParam, req.team, role); - - // Audit the member add request - await auditService.audit( - `team ${role} added`, - 'team-role', - 'user add', - req, - req.team.auditCopyTeamMember(req.userParam, role) - ); - - res.status(StatusCodes.NO_CONTENT).end(); -}; - -/** - * Add specified members with specified roles to a team - */ -export const addMembers = async (req, res) => { - await Promise.all( - req.body.newMembers - .filter((member) => null != member._id) - .map(async (member) => { - const user = await userService.read(member._id); - if (null != user) { - await teamsService.addMemberToTeam(user, req.team, member.role); - return auditService.audit( - `team ${member.role} added`, - 'team-role', - 'user add', - req, - req.team.auditCopyTeamMember(user, member.role) - ); + fastify.route({ + method: 'GET', + url: '/team/:id', + schema: { + description: 'Gets the details of a Team', + tags: ['Team'], + params: { + type: 'object', + properties: { + id: { type: 'string' } + }, + required: ['id'] + } + }, + preValidation: [ + requireAccess, + requireAny(requireAdminRole, requireTeamMemberRole) + ], + preHandler: loadTeamById, + handler: function (req, reply) { + return reply.send(req.team); + } + }); + + fastify.route({ + method: 'POST', + url: '/team/:id', + schema: { + description: 'Updates the details of a Team', + tags: ['Team'], + params: { + type: 'object', + properties: { + id: { type: 'string' } + }, + required: ['id'] + } + }, + preValidation: [ + requireAccess, + requireAny(requireAdminRole, requireTeamAdminRole) + ], + preHandler: loadTeamById, + handler: async function (req, reply) { + // Make a copy of the original team for auditing purposes + const originalTeam = req.team.auditCopy(); + + const result = await teamsService.update(req.team, req.body); + + await auditService.audit('team updated', 'team', 'update', req, { + before: originalTeam, + after: result.auditCopy() + }); + + return reply.send(result); + } + }); + + fastify.route({ + method: 'DELETE', + url: '/team/:id', + schema: { + description: 'Deletes a Team', + tags: ['Team'], + params: { + type: 'object', + properties: { + id: { type: 'string' } + }, + required: ['id'] + } + }, + preValidation: [ + requireAccess, + requireAny(requireAdminRole, requireTeamAdminRole) + ], + preHandler: loadTeamById, + handler: async function (req, reply) { + await teamsService.delete(req.team); + + // Audit the team delete attempt + await auditService.audit( + 'team deleted', + 'team', + 'delete', + req, + req.team.auditCopy() + ); + + return reply.send(req.team); + } + }); + + fastify.route({ + method: 'POST', + url: '/team/:id/request', + schema: { + hide: true, + description: + 'Requests access to a Team. Notifies team admins of the request', + tags: ['Team'] + }, + preValidation: requireAccess, + preHandler: loadTeamById, + handler: async function (req, reply) { + await teamsService.requestAccessToTeam(req.user, req.team); + return reply.send(); + } + }); + + fastify.route({ + method: 'POST', + url: '/team-request', + schema: { + hide: true, + description: + 'Requests a new Team. Notifies the team organization admin of the request.', + tags: ['Team'], + body: { + type: 'object', + properties: { + org: { type: 'string' }, + aoi: { type: 'string' }, + description: { type: 'string' } } - }) - ); - res.status(StatusCodes.NO_CONTENT).end(); -}; - -/** - * Remove a member from a team - */ -export const removeMember = async (req, res) => { - await teamsService.removeMemberFromTeam(req.userParam, req.team); - - // Audit the user remove - await auditService.audit( - 'team member removed', - 'team-role', - 'user remove', - req, - req.team.auditCopyTeamMember(req.userParam) - ); - - res.status(StatusCodes.NO_CONTENT).end(); -}; - -export const updateMemberRole = async (req, res) => { - const role: TeamRoles = req.body.role || TeamRoles.Member; - - await teamsService.updateMemberRole(req.userParam, req.team, role); - - // Audit the member update request - await auditService.audit( - `team role changed to ${role}`, - 'team-role', - 'user add', - req, - req.team.auditCopyTeamMember(req.userParam, role) - ); - - res.status(StatusCodes.NO_CONTENT).end(); -}; - -/** - * Team middleware - */ -export const teamById = async (req, res, next, id: string) => { + } + }, + preValidation: requireAccess, + handler: async function (req, reply) { + const org = req.body.org ?? null; + const aoi = req.body.aoi ?? null; + const description = req.body.description ?? null; + + await teamsService.requestNewTeam(org, aoi, description, req.user); + + await auditService.audit('new team requested', 'team', 'request', req, { + org, + aoi, + description + }); + + return reply.send(); + } + }); + + fastify.route({ + method: 'PUT', + url: '/team/:id/members', + schema: { + description: 'Adds members to a Team', + tags: ['Team'], + body: { + type: 'object', + properties: { + newMembers: { + type: 'array', + items: { + type: 'object', + properties: { + _id: { type: 'string' }, + role: { type: 'string', enum: Object.values(TeamRoles) } + }, + required: ['_id'] + } + } + }, + required: ['newMembers'] + } + }, + preValidation: [ + requireAccess, + requireAny(requireAdminRole, requireTeamAdminRole) + ], + preHandler: loadTeamById, + handler: async function (req, reply) { + await Promise.all( + req.body.newMembers + .filter((member) => null != member._id) + .map(async (member) => { + const user = await userService.read(member._id); + if (null != user) { + await teamsService.addMemberToTeam(user, req.team, member.role); + return auditService.audit( + `team ${member.role} added`, + 'team-role', + 'user add', + req, + req.team.auditCopyTeamMember(user, member.role) + ); + } + }) + ); + return reply.send(); + } + }); + + fastify.route({ + method: 'POST', + url: '/team/:id/members', + schema: { + description: 'Searches for members of a Team', + tags: ['Team'], + body: SearchBodySchema, + querystring: PagingQueryStringSchema + }, + preValidation: [ + requireAccess, + requireAny(requireAdminRole, requireTeamMemberRole) + ], + preHandler: loadTeamById, + handler: async function (req, reply) { + // Get search and query parameters + const search = req.body.s ?? ''; + const query = teamsService.updateMemberFilter( + utilService.toMongoose(req.body.q ?? {}), + req.team + ); + + const results = await userService.searchUsers(req.query, query, search); + + // Create the return copy of the messages + const mappedResults = { + pageNumber: results.pageNumber, + pageSize: results.pageSize, + totalPages: results.totalPages, + totalSize: results.totalSize, + elements: results.elements.map((element) => { + return { + ...element.filteredCopy(), + teams: element.teams.filter((team) => team._id.equals(req.team._id)) + }; + }) + }; + + return reply.send(mappedResults); + } + }); + + fastify.route({ + method: 'POST', + url: '/team/:id/member/:memberId', + schema: { + description: 'Adds a member to a Team', + tags: ['Team'], + body: { + type: 'object', + properties: { + role: { + type: 'string', + enum: Object.values(TeamRoles) + } + }, + required: ['role'] + } + }, + preValidation: [ + requireAccess, + requireAny(requireAdminRole, requireTeamAdminRole) + ], + preHandler: [loadTeamById, loadTeamMemberById], + handler: async function (req, reply) { + const role: TeamRoles = req.body.role ?? TeamRoles.Member; + + await teamsService.addMemberToTeam(req.userParam, req.team, role); + + // Audit the member add request + await auditService.audit( + `team ${role} added`, + 'team-role', + 'user add', + req, + req.team.auditCopyTeamMember(req.userParam, role) + ); + + return reply.send(); + } + }); + + fastify.route({ + method: 'DELETE', + url: '/team/:id/member/:memberId', + schema: { + description: 'Deletes a member from a Team', + tags: ['Team'] + }, + preValidation: [ + requireAccess, + requireAny(requireAdminRole, requireTeamAdminRole) + ], + preHandler: [loadTeamById, loadTeamMemberById], + handler: async function (req, reply) { + await teamsService.removeMemberFromTeam(req.userParam, req.team); + + // Audit the user remove + await auditService.audit( + 'team member removed', + 'team-role', + 'user remove', + req, + req.team.auditCopyTeamMember(req.userParam) + ); + + return reply.send(); + } + }); + + fastify.route({ + method: 'POST', + url: '/team/:id/member/:memberId/role', + schema: { + description: `Updates a member's role in a team`, + tags: ['Team'], + body: { + type: 'object', + properties: { + role: { + type: 'string', + enum: Object.values(TeamRoles) + } + }, + required: ['role'] + } + }, + preValidation: [ + requireAccess, + requireAny(requireAdminRole, requireTeamAdminRole) + ], + preHandler: [loadTeamById, loadTeamMemberById], + handler: async function (req, reply) { + const role: TeamRoles = req.body.role || TeamRoles.Member; + + await teamsService.updateMemberRole(req.userParam, req.team, role); + + // Audit the member update request + await auditService.audit( + `team role changed to ${role}`, + 'team-role', + 'user add', + req, + req.team.auditCopyTeamMember(req.userParam, role) + ); + + return reply.send(); + } + }); +} + +async function loadTeamById(req: FastifyRequest) { + const id = req.params['id']; const populate = [ { path: 'parent', @@ -233,51 +441,17 @@ export const teamById = async (req, res, next, id: string) => { } ]; - const team = await teamsService.read(id, populate); - if (!team) { - return next(new NotFoundError('Could not find team')); + req.team = await teamsService.read(id, populate); + if (!req.team) { + throw new NotFoundError('Could not find team'); } - req.team = team; - return next(); -}; +} -export const teamMemberById = async (req, res, next, id: string) => { - const user = await userService.read(id); +async function loadTeamMemberById(req: FastifyRequest) { + const id = req.params['memberId']; + req.userParam = await userService.read(id); - if (null == user) { - return next(new Error('Failed to load team member')); + if (!req.userParam) { + throw new Error('Failed to load team member'); } - req.userParam = user; - return next(); -}; - -/** - * Does the user have the referenced role in the team - */ -function requiresRole(role: TeamRoles): (req) => Promise { - return function (req) { - // Verify that the user and team are on the request - const user = req.user; - if (null == user) { - return Promise.reject(new BadRequestError('No user for request')); - } - const team = req.team; - if (null == team) { - return Promise.reject(new BadRequestError('No team for request')); - } - - return teamsService.meetsRoleRequirement(user, team, role); - }; } - -export const requiresAdmin = (req) => { - return requiresRole(TeamRoles.Admin)(req); -}; - -export const requiresEditor = (req) => { - return requiresRole(TeamRoles.Editor)(req); -}; - -export const requiresMember = (req) => { - return requiresRole(TeamRoles.Member)(req); -}; diff --git a/src/app/core/teams/teams.routes.ts b/src/app/core/teams/teams.routes.ts deleted file mode 100644 index 91ff57b2..00000000 --- a/src/app/core/teams/teams.routes.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { Router } from 'express'; -import { Validator } from 'express-json-validator-middleware'; - -import * as teams from './teams.controller'; -import * as teamSchemas from './teams.schemas'; -import { hasAny } from '../../common/express/auth-middleware'; -import { - hasAccess, - hasEditorAccess, - requiresAdminRole -} from '../user/user-auth.middleware'; - -const { validate } = new Validator({}); - -const router = Router(); - -/** - * @swagger - * /team: - * post: - * tags: [Team] - * description: Creates a new Team - * requestBody: - * $ref: '#/components/requestBodies/CreateTeam' - * responses: - * '200': - * $ref: '#/components/responses/CreatedTeam' - */ -router.route('/team').post(hasEditorAccess, teams.create); - -/** - * @swagger - * /teams: - * post: - * tags: [Team] - * description: Returns Teams that match the search criteria - * requestBody: - * $ref: '#/components/requestBodies/SearchCriteria' - * responses: - * '200': - * $ref: '#/components/responses/TeamListing' - */ -router.route('/teams').post(hasAccess, teams.search); - -/** - * @swagger - * /team/ancestors: - * post: - * tags: [Team] - * description: Returns IDs of Ancestor Teams to any of the input teams - * requestBody: - * $ref: '#/components/requestBodies/GetAncestorTeams' - * responses: - * '200': - * $ref: '#/components/responses/TeamIds' - */ -router.route('/team/ancestors').post(hasAccess, teams.getAncestorTeamIds); - -/** - * @swagger - * /team/{teamId}: - * get: - * tags: [Team] - * description: Gets the details of a Team - * parameters: - * - $ref: '#/components/parameters/teamIdParam' - * responses: - * '200': - * $ref: '#/components/responses/GetTeam' - * post: - * tags: [Team] - * description: Updates the details of a Team - * parameters: - * - $ref: '#/components/parameters/teamIdParam' - * requestBody: - * $ref: '#/components/requestBodies/UpdateTeam' - * responses: - * '200': - * $ref: '#/components/responses/UpdateTeam' - * '400': - * description: Update unsuccessful. Could not find team. - * delete: - * tags: [Team] - * description: Deletes a Team - * parameters: - * - $ref: '#/components/parameters/teamIdParam' - * responses: - * '200': - * $ref: '#/components/responses/DeleteTeam' - * '400': - * description: Deletion unsuccessful. Could not find team. - */ -router - .route('/team/:teamId') - .get(hasAccess, hasAny(requiresAdminRole, teams.requiresMember), teams.read) - .post(hasAccess, hasAny(requiresAdminRole, teams.requiresAdmin), teams.update) - .delete( - hasAccess, - hasAny(requiresAdminRole, teams.requiresAdmin), - teams.deleteTeam - ); - -/** - * @swagger - * /team/{teamId}/request: - * post: - * tags: [Team] - * description: Requests access to a Team. Notifies team admins of the request - * parameters: - * - $ref: '#/components/parameters/teamIdParam' - * responses: - * '204': - * $ref: '#/components/responses/RequestTeamAccess' - * '400': - * description: Request for team access unsuccessful. Could not find team. - */ -router.route('/team/:teamId/request').post(hasAccess, teams.requestAccess); - -/** - * @swagger - * /team-request: - * post: - * tags: [Team] - * description: Requests a new Team. Notifies the team organization admin of the request. - * requestBody: - * $ref: '#/components/requestBodies/RequestNewTeam' - * responses: - * '204': - * $ref: '#/components/responses/RequestNewTeam' - */ -router.route('/team-request').post(hasAccess, teams.requestNewTeam); - -/** - * Team editors Routes (requires team admin role) - */ -/** - * @swagger - * /team/{teamId}/members: - * put: - * tags: [Team] - * description: Adds members to a Team - * parameters: - * - $ref: '#/components/parameters/teamIdParam' - * requestBody: - * $ref: '#/components/requestBodies/AddTeamMembers' - * responses: - * '204': - * $ref: '#/components/responses/AddedTeamMembers' - * post: - * tags: [Team] - * description: Searches for members of team - * parameters: - * - $ref: '#/components/parameters/teamIdParam' - * requestBody: - * $ref: '#/components/requestBodies/SearchCriteria' - * responses: - * '200': - * $ref: '#/components/responses/TeamMembers' - * '400': - * description: Add unsuccessful. Could not find team or new members not specified. - */ -router - .route('/team/:teamId/members') - .put( - hasAccess, - hasAny(requiresAdminRole, teams.requiresAdmin), - validate({ body: teamSchemas.addMembers }), - teams.addMembers - ) - .post( - hasAccess, - hasAny(requiresAdminRole, teams.requiresMember), - teams.searchMembers - ); - -/** - * @swagger - * /team/{teamId}/member/{memberId}: - * post: - * tags: [Team] - * description: Adds a member to a Team - * parameters: - * - $ref: '#/components/parameters/teamIdParam' - * - $ref: '#/components/parameters/memberIdParam' - * requestBody: - * $ref: '#/components/requestBodies/AddTeamMember' - * responses: - * '204': - * $ref: '#/components/responses/AddedTeamMember' - * '400': - * description: Add unsuccessful. Could not find team. - * delete: - * tags: [Team] - * description: Deletes a member from a team - * parameters: - * - $ref: '#/components/parameters/teamIdParam' - * - $ref: '#/components/parameters/memberIdParam' - * responses: - * '204': - * $ref: '#/components/responses/RemovedTeamMember' - * '400': - * description: Deletion unsuccessful. Could not find team. - */ -router - .route('/team/:teamId/member/:memberId') - .post( - hasAccess, - hasAny(requiresAdminRole, teams.requiresAdmin), - validate({ body: teamSchemas.addUpdateMemberRole }), - teams.addMember - ) - .delete( - hasAccess, - hasAny(requiresAdminRole, teams.requiresAdmin), - teams.removeMember - ); - -/** - * @swagger - * /team/{teamId}/member/{memberId}/role: - * post: - * tags: [Team] - * description: Updates a member's role in a team. - * parameters: - * - $ref: '#/components/parameters/teamIdParam' - * - $ref: '#/components/parameters/memberIdParam' - * requestBody: - * $ref: '#/components/requestBodies/UpdateMemberRole' - * responses: - * '204': - * $ref: '#/components/responses/UpdatedTeamMemberRole' - * '400': - * description: Update unsuccessful. Could not find team. - */ -router - .route('/team/:teamId/member/:memberId/role') - .post( - hasAccess, - hasAny(requiresAdminRole, teams.requiresAdmin), - validate({ body: teamSchemas.addUpdateMemberRole }), - teams.updateMemberRole - ); - -// Finish by binding the team middleware -router.param('teamId', teams.teamById); -router.param('memberId', teams.teamMemberById); - -export = router; diff --git a/src/app/core/teams/teams.schemas.ts b/src/app/core/teams/teams.schemas.ts deleted file mode 100644 index f410213c..00000000 --- a/src/app/core/teams/teams.schemas.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { JSONSchema7 } from 'json-schema'; - -import { TeamRoles } from './team-role.model'; - -export const addMembers: JSONSchema7 = { - $schema: 'http://json-schema.org/draft-07/schema', - $id: 'node-rest-server/src/app/core/team/addMembers', - type: 'object', - title: 'Team Add Members Schema', - description: 'Schema for adding members to a team', - required: ['newMembers'], - properties: { - newMembers: { - $id: '#/properties/newMembers', - type: 'array', - title: 'Type', - description: 'type of the export request', - items: { - type: 'object', - required: ['_id', 'role'], - properties: { - _id: { - type: 'string' - }, - role: { - type: 'string', - enum: Object.values(TeamRoles) - } - } - }, - minItems: 1 - } - } -}; - -export const addUpdateMemberRole: JSONSchema7 = { - $schema: 'http://json-schema.org/draft-07/schema', - $id: 'node-rest-server/src/app/core/team/addUpdateMemberRole', - type: 'object', - title: 'Team Add/Update Member Role Schema', - description: 'Schema for adding or updating a members role to a team', - required: ['role'], - properties: { - role: { - type: 'string', - enum: Object.values(TeamRoles) - } - } -}; diff --git a/src/app/core/teams/teams.service.spec.ts b/src/app/core/teams/teams.service.spec.ts index 58265a49..60a2952b 100644 --- a/src/app/core/teams/teams.service.spec.ts +++ b/src/app/core/teams/teams.service.spec.ts @@ -975,7 +975,7 @@ describe('Team Service:', () => { FOOTER `; - await teamsService.sendRequestEmail(toEmails, _user, _team, {}); + await teamsService.sendRequestEmail(toEmails, _user, _team); sinonAssert.called(sendMailStub); const [mailOptions] = sendMailStub.getCall(0).args; @@ -1008,8 +1008,7 @@ FOOTER await teamsService.sendRequestEmail( toEmails, user.user1, - team.teamWithNoExternalTeam, - {} + team.teamWithNoExternalTeam ); sinonAssert.calledOnce(logStub); @@ -1025,8 +1024,7 @@ FOOTER await assert.rejects( teamsService.requestAccessToTeam( user.admin, - team.teamWithNoExternalTeam2, - {} + team.teamWithNoExternalTeam2 ), new InternalServerError('Error retrieving team admins') ); @@ -1046,8 +1044,7 @@ FOOTER await assert.doesNotReject( teamsService.requestAccessToTeam( user.admin, - team.teamWithNoExternalTeam, - {} + team.teamWithNoExternalTeam ) ); const requesterCount = await User.countDocuments({ @@ -1071,22 +1068,22 @@ FOOTER it('should properly reject invalid parameters', async () => { await assert.rejects( - teamsService.requestNewTeam(null, null, null, null, null), + teamsService.requestNewTeam(null, null, null, null), new BadRequestError('Organization cannot be empty') ); await assert.rejects( - teamsService.requestNewTeam('org', null, null, null, null), + teamsService.requestNewTeam('org', null, null, null), new BadRequestError('AOI cannot be empty') ); await assert.rejects( - teamsService.requestNewTeam('org', 'aoi', null, null, null), + teamsService.requestNewTeam('org', 'aoi', null, null), new BadRequestError('Description cannot be empty') ); await assert.rejects( - teamsService.requestNewTeam('org', 'aoi', 'description', null, null), + teamsService.requestNewTeam('org', 'aoi', 'description', null), new BadRequestError('Invalid requester') ); }); @@ -1110,9 +1107,7 @@ FOOTER FOOTER `; - await teamsService.requestNewTeam('org', 'aoi', 'description', _user, { - headers: {} - }); + await teamsService.requestNewTeam('org', 'aoi', 'description', _user); sinonAssert.called(sendMailStub); const [mailOptions] = sendMailStub.getCall(0).args; @@ -1144,8 +1139,7 @@ FOOTER 'org', 'aoi', 'description', - user.user1, - { headers: {} } + user.user1 ); sinonAssert.calledOnce(logStub); @@ -1445,30 +1439,6 @@ FOOTER }); }); - describe('getAncestorTeamIds', () => { - it('should return team ancestors', async () => { - const ancestors = await teamsService.getAncestorTeamIds([ - team.nestedTeam2_1._id - ]); - assert.deepStrictEqual(ancestors, [ - team.teamWithNoExternalTeam._id, - team.nestedTeam2._id - ]); - }); - - it('should return empty array for team without ancestors', async () => { - const ancestors = await teamsService.getAncestorTeamIds([ - team.teamWithNoExternalTeam._id - ]); - assert.deepStrictEqual(ancestors, []); - }); - - it('should return empty array when no teams are passed in', async () => { - const ancestors = await teamsService.getAncestorTeamIds(); - assert.deepStrictEqual(ancestors, []); - }); - }); - describe('updateTeams', () => { it('implicit members disabled; nested teams disabled', async () => { const configGetStub = sandbox.stub(config, 'get'); diff --git a/src/app/core/teams/teams.service.ts b/src/app/core/teams/teams.service.ts index 0ab179c8..f74b9121 100644 --- a/src/app/core/teams/teams.service.ts +++ b/src/app/core/teams/teams.service.ts @@ -568,13 +568,11 @@ class TeamsService { async sendRequestEmail( toEmail: string[], requester: UserDocument, - team: TeamDocument, - req + team: TeamDocument ): Promise { try { const mailOptions = await emailService.generateMailOptions( requester, - null, config.get('coreEmails.teamAccessRequestEmail'), { team: team.toJSON() @@ -590,14 +588,13 @@ class TeamsService { logger.debug(`Sent approved user (${requester.username}) alert email`); } catch (error) { // Log the error but this shouldn't block - logger.error('Failure sending email.', { err: error, req: req }); + logger.error('Failure sending email.', { err: error }); } } async requestAccessToTeam( requester: UserDocument, - team: TeamDocument, - req + team: TeamDocument ): Promise { // Lookup the emails of all team admins const admins = await this.userModel @@ -623,15 +620,14 @@ class TeamsService { await this.addMemberToTeam(requester, team, TeamRoles.Requester); // Email template rendering requires simple objects and not Mongo classes - return this.sendRequestEmail(adminEmails, requester, team, req); + return this.sendRequestEmail(adminEmails, requester, team); } async requestNewTeam( org: string, aoi: string, description: string, - requester: UserDocument, - req + requester: UserDocument ): Promise { if (null == org) { return Promise.reject( @@ -651,7 +647,6 @@ class TeamsService { try { const mailOptions = await emailService.generateMailOptions( requester, - req, config.get('coreEmails.newTeamRequest'), { org: org, @@ -663,7 +658,7 @@ class TeamsService { logger.debug('Sent team request email'); } catch (error) { // Log the error but this shouldn't block - logger.error('Failure sending email.', { err: error, req: req }); + logger.error('Failure sending email.', { err: error }); } } @@ -786,12 +781,6 @@ class TeamsService { .exec(); } - getAncestorTeamIds( - teamIds: Types.ObjectId[] = [] - ): Promise { - return this.model.distinct('ancestors', { _id: { $in: teamIds } }).exec(); - } - async getTeamIds( user: IUser, ...roles: TeamRoles[] diff --git a/src/app/core/user/admin/user-admin.controller.spec.ts b/src/app/core/user/admin/user-admin.controller.spec.ts deleted file mode 100644 index ae79e189..00000000 --- a/src/app/core/user/admin/user-admin.controller.spec.ts +++ /dev/null @@ -1,246 +0,0 @@ -import assert from 'node:assert/strict'; - -import { assert as sinonAssert, createSandbox } from 'sinon'; - -import * as userAdminController from './user-admin.controller'; -import { auditService, config } from '../../../../dependencies'; -import { getResponseSpy } from '../../../../spec/helpers'; -import { BadRequestError } from '../../../common/errors'; -import userEmailService from '../user-email.service'; -import { User } from '../user.model'; -import userService from '../user.service'; - -/** - * Helpers - */ -function userSpec(key) { - return new User({ - name: `${key} Name`, - email: `${key}@mail.com`, - username: `${key}_username`, - password: 'password', - provider: 'local', - organization: `${key} Organization` - }); -} - -/** - * Unit tests - */ -describe('User Admin Controller:', () => { - let res; - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - res = getResponseSpy(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe('adminGetUser', () => { - it('user is found', async () => { - const req = { - body: {}, - userParam: new User() - }; - - await userAdminController.adminGetUser(req, res); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('adminGetAll', () => { - it('returns successfully w/ no results', async () => { - const req = { - body: { field: 'name' } - }; - - sandbox.stub(User, 'find').returns({ - exec: () => Promise.resolve([]) - }); - - await userAdminController.adminGetAll(req, res); - - sinonAssert.calledOnce(User.find); - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledWithMatch(res.json, []); - }); - - it('returns successfully w/ results', async () => { - const req = { - body: { field: 'name' } - }; - - sandbox.stub(User, 'find').returns({ - exec: () => Promise.resolve([userSpec('user1'), userSpec('user2')]) - }); - - await userAdminController.adminGetAll(req, res); - - sinonAssert.calledOnce(User.find); - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledWithMatch(res.json, ['user1 Name', 'user2 Name']); - }); - - it('query field undefined; returns error', async () => { - const req = { - body: {} - }; - - sandbox.stub(User, 'find').returns({ - exec: () => Promise.resolve([]) - }); - - await assert.rejects( - userAdminController.adminGetAll(req, res), - new BadRequestError('Query field must be provided') - ); - - sinonAssert.notCalled(User.find); - }); - - it('query field is empty string; returns error', async () => { - const req = { - body: { field: '' } - }; - - sandbox.stub(User, 'find').returns({ - exec: () => Promise.resolve([]) - }); - - await assert.rejects( - userAdminController.adminGetAll(req, res), - new BadRequestError('Query field must be provided') - ); - - sinonAssert.notCalled(User.find); - }); - }); - - describe('adminUpdateUser', () => { - let req; - beforeEach(() => { - req = { - body: {}, - user: userSpec('currentUser'), - userParam: userSpec('user1') - }; - - sandbox.stub(auditService, 'audit').resolves(); - sandbox.stub(userEmailService, 'emailApprovedUser').resolves(); - }); - - it('user is found', async () => { - sandbox.stub(userService, 'update').resolves(req.user); - - sandbox - .stub(config, 'coreEmails') - .value({ approvedUserEmail: { enabled: true } }); - await userAdminController.adminUpdateUser(req, res); - - sinonAssert.calledWithMatch(auditService.audit, 'admin user updated'); - sinonAssert.notCalled(userEmailService.emailApprovedUser); - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - - it('user is found; password is updated', async () => { - req.body.password = 'newPassword'; - - sandbox.stub(userService, 'update').resolves(req.user); - - await userAdminController.adminUpdateUser(req, res); - - sinonAssert.calledWithMatch(auditService.audit, 'admin user updated'); - sinonAssert.notCalled(userEmailService.emailApprovedUser); - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - - it('user is found; approved user email sent', async () => { - req.body.roles = { user: true }; - - sandbox.stub(userService, 'update').resolves(); - - await userAdminController.adminUpdateUser(req, res); - - sinonAssert.calledWithMatch(auditService.audit, 'admin user updated'); - sinonAssert.calledOnce(userEmailService.emailApprovedUser); - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('adminDeleteUser', () => { - let req; - beforeEach(() => { - req = { - body: {}, - user: userSpec('currentUser'), - userParam: userSpec('user1') - }; - - sandbox.stub(auditService, 'audit').resolves(); - }); - - it('user is found', async () => { - sandbox.stub(userService, 'remove').resolves(); - - await userAdminController.adminDeleteUser(req, res); - - sinonAssert.calledWithMatch(auditService.audit, 'admin user deleted'); - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('adminSearchUsers', () => { - const req = { - body: {} as Record, - user: userSpec('user1') - }; - - it('search returns successfully', async () => { - sandbox.stub(userService, 'searchUsers').resolves({ - elements: [userSpec('user1'), userSpec('user2')] - }); - - await userAdminController.adminSearchUsers(req, res); - - sinonAssert.calledOnce(userService.searchUsers); - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - }); - - describe('search returns successfully; filters updated', () => { - beforeEach(() => { - req.body.q = { $or: [{ 'roles.user': true }] }; - - sandbox.stub(userService, 'searchUsers').resolves({ - elements: [] - }); - }); - - ['external', 'hybrid'].forEach((strategy) => { - it(`strategy = ${strategy}`, () => { - const configGetStub = sandbox.stub(config, 'get'); - configGetStub.withArgs('auth.roleStrategy').returns(strategy); - configGetStub.callThrough(); - }); - }); - - afterEach(async () => { - await userAdminController.adminSearchUsers(req, res); - - sinonAssert.calledOnce(userService.searchUsers); - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - }); - }); - }); -}); diff --git a/src/app/core/user/admin/user-admin.controller.ts b/src/app/core/user/admin/user-admin.controller.ts index 6863211c..78bc1d65 100644 --- a/src/app/core/user/admin/user-admin.controller.ts +++ b/src/app/core/user/admin/user-admin.controller.ts @@ -1,243 +1,333 @@ -import { StatusCodes } from 'http-status-codes'; +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; +import { FastifyInstance } from 'fastify'; import _ from 'lodash'; import { FilterQuery } from 'mongoose'; import { auditService, config, utilService } from '../../../../dependencies'; import { logger } from '../../../../lib/logger'; -import { BadRequestError, ForbiddenError } from '../../../common/errors'; +import { PagingQueryStringSchema, SearchBodySchema } from '../../core.schemas'; import { Callbacks } from '../../export/callbacks'; import * as exportConfigController from '../../export/export-config.controller'; +import { loadExportConfigById } from '../../export/export-config.controller'; import { IExportConfig } from '../../export/export-config.model'; -import userAuthService from '../auth/user-authentication.service'; +import { requireAdminAccess } from '../auth/auth.middleware'; import userAuthorizationService from '../auth/user-authorization.service'; import userEmailService from '../user-email.service'; +import { loadUserById } from '../user.controller'; import { Roles, User, UserDocument } from '../user.model'; import userService from '../user.service'; -/** - * Standard User Operations - */ - -export const adminGetUser = (req, res) => { - // The user that is a parameter of the request is stored in 'userParam' - const user = req.userParam; - - res.status(StatusCodes.OK).json(user.fullCopy()); -}; - -export const adminGetAll = async (req, res) => { - // The field that the admin is requesting is a query parameter - const field = req.body.field; - if (null == field || field.length === 0) { - throw new BadRequestError('Query field must be provided'); - } - - const query = req.body.query; - - logger.debug('Querying Users for %s', field); - const proj = { [field]: 1 }; - - const results = await User.find(utilService.toMongoose(query), proj).exec(); - - res.status(StatusCodes.OK).json( - results.map((r) => { - return r[field]; - }) - ); -}; - -// Admin Update a User -export const adminUpdateUser = async (req, res) => { - // The persistence user - const user: UserDocument = req.userParam; - - // A copy of the original user for auditing - const originalUser = user.auditCopy(); - - // Copy over the new user properties - user.name = req.body.name; - user.organization = req.body.organization; - user.email = req.body.email; - user.phone = req.body.phone; - user.username = req.body.username; - user.roles = req.body.roles; - user.bypassAccessCheck = req.body.bypassAccessCheck; - - if (_.isString(req.body.password) && !_.isEmpty(req.body.password)) { - user.password = req.body.password; - } - - // Save the user - await userService.update(user); - - // Audit user update - auditService.audit('admin user updated', 'user', 'admin update', req, { - before: originalUser, - after: user.auditCopy() +export default function (_fastify: FastifyInstance) { + const fastify = _fastify.withTypeProvider(); + fastify.route({ + method: 'POST', + url: '/admin/users', + schema: { + description: 'Returns users that match the search criteria', + tags: ['User'], + body: SearchBodySchema, + querystring: PagingQueryStringSchema + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + // Handle the query/search/page + const query = userAuthorizationService.updateUserFilter(req.body.q); + const search = req.body.s; + + const results = await userService.searchUsers( + req.query, + query, + search, + [], + { + path: 'teams.team', + options: { select: { name: 1 } } + } + ); + const mappedResults = { + pageNumber: results.pageNumber, + pageSize: results.pageSize, + totalPages: results.totalPages, + totalSize: results.totalSize, + elements: results.elements.map((user) => { + const userCopy = user.fullCopy(); + userAuthorizationService.updateRoles(userCopy); + return userCopy; + }) + }; + return reply.send(mappedResults); + } }); - const originalUserRole = - (originalUser?.roles as Record)?.user ?? null; - const newUserRole = user?.roles?.user ?? null; - - if (originalUserRole !== newUserRole && newUserRole) { - await userEmailService.emailApprovedUser(user, req); - } - - res.status(StatusCodes.OK).json(user.fullCopy()); -}; + fastify.route({ + method: 'GET', + url: '/admin/user/:id', + schema: { + description: '', + tags: ['User'] + }, + preValidation: requireAdminAccess, + preHandler: loadUserById, + handler: function (req, reply) { + return reply.send(req.userParam.fullCopy()); + } + }); -// Admin Delete a User -export const adminDeleteUser = async (req, res) => { - // Init Variables - const user = req.userParam; + fastify.route({ + method: 'POST', + url: '/admin/user/:id', + schema: { + description: '', + tags: ['User'], + body: { + type: 'object', + properties: { + name: { type: 'string' }, + organization: { type: 'string' }, + email: { type: 'string' }, + phone: { type: 'string' }, + username: { type: 'string' }, + password: { type: 'string' }, + roles: { type: 'object' }, + bypassAccessCheck: { type: 'boolean' } + } + } + }, + preValidation: requireAdminAccess, + preHandler: loadUserById, + handler: async function (req, reply) { + // The persistence user + const user = req.userParam; + + // A copy of the original user for auditing + const originalUser = user.auditCopy(); + const originalUserRole = user.roles?.user ?? null; + + // Copy over the new user properties + user.name = req.body.name; + user.organization = req.body.organization; + user.email = req.body.email; + user.phone = req.body.phone; + user.username = req.body.username; + user.roles = req.body.roles; + user.bypassAccessCheck = req.body.bypassAccessCheck; + + if (_.isString(req.body.password) && !_.isEmpty(req.body.password)) { + user.password = req.body.password; + } + + // Save the user + await userService.update(user); + + // Audit user update + auditService + .audit('admin user updated', 'user', 'admin update', req, { + before: originalUser, + after: user.auditCopy() + }) + .then(); + + const newUserRole = user.roles?.user ?? null; + + if (originalUserRole !== newUserRole && newUserRole) { + await userEmailService.emailApprovedUser(user, req); + } + + return reply.send(user.fullCopy()); + } + }); - if (!config.get('allowDeleteUser')) { - throw new ForbiddenError('User deletion is disabled'); + if (config.get('allowDeleteUser')) { + fastify.route({ + method: 'DELETE', + url: '/admin/user/:id', + schema: { + description: '', + tags: ['User'] + }, + preValidation: requireAdminAccess, + preHandler: loadUserById, + handler: async function (req, reply) { + // Init Variables + const user = req.userParam; + + await auditService.audit( + 'admin user deleted', + 'user', + 'admin delete', + req, + user.auditCopy() + ); + await userService.remove(user); + return reply.send(user.fullCopy()); + } + }); } - await auditService.audit( - 'admin user deleted', - 'user', - 'admin delete', - req, - user.auditCopy() - ); - await userService.remove(user); - res.status(StatusCodes.OK).json(user.fullCopy()); -}; - -// Admin Search for Users -export const adminSearchUsers = async (req, res) => { - // Handle the query/search/page - const query = userAuthorizationService.updateUserFilter(req.body.q); - const search = req.body.s; - - const results = await userService.searchUsers(req.query, query, search, [], { - path: 'teams.team', - options: { select: { name: 1 } } - }); - const mappedResults = { - pageNumber: results.pageNumber, - pageSize: results.pageSize, - totalPages: results.totalPages, - totalSize: results.totalSize, - elements: results.elements.map((user) => { - const userCopy = user.fullCopy(); - userAuthorizationService.updateRoles(userCopy); - return userCopy; - }) - }; - res.status(StatusCodes.OK).json(mappedResults); -}; - -// GET the requested CSV using a special configuration from the export config collection -export const adminGetCSV = (req, res) => { - const exportConfig = req.exportConfig as IExportConfig; - const exportQuery = req.exportQuery as FilterQuery; - - const fileName = `${config.get('app.instanceName')}-${exportConfig.type}.csv`; - - // Replace `roles` column with individual columns for each role - const columns = exportConfig.config.cols.filter( - (col) => ['roles'].indexOf(col.key) === -1 - ); - if (columns.length !== exportConfig.config.cols.length) { - for (const role of Roles) { - columns.push({ - key: `roles.${role}`, - title: `${role} Role`, - callback: Callbacks.trueFalse + fastify.route({ + method: 'POST', + url: '/admin/users/getAll', + schema: { + description: '', + tags: ['User'], + body: { + type: 'object', + properties: { + field: { type: 'string' }, + query: { type: 'object' } + }, + required: ['field'] + } + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + const field = req.body.field; + const query = req.body.query; + + logger.debug('Querying Users for %s', field); + const proj = { [field]: 1 }; + + const results = await User.find( + utilService.toMongoose(query), + proj + ).exec(); + + const mappedResults = results.map((r) => { + return r[field]; }); - } - } - const populate = []; - - // Based on which columns are requested, handle property-specific behavior (ex. callbacks for the - // CSV service to make booleans and dates more human-readable) - columns.forEach((col) => { - col.title = col.title ?? _.capitalize(col.key); - - switch (col.key) { - case 'bypassAccessCheck': - col.callback = Callbacks.trueFalse; - break; - case 'lastLogin': - case 'created': - case 'updated': - case 'acceptedEua': - col.callback = Callbacks.isoDateString; - break; - case 'teams': - populate.push({ path: 'teams.team', select: 'name' }); - col.callback = Callbacks.mapAndJoinArray( - (team: { team: { name: string } }) => team.team.name - ); - break; + return reply.send(mappedResults); } }); - const cursor = userService.cursorSearch( - exportConfig.config, - exportConfig.config.s, - exportQuery, - [], - populate - ); - - exportConfigController.exportCSV(req, res, fileName, columns, cursor); -}; - -// Admin creates a user -async function _adminCreateUser(user, req, res) { - // Initialize the user - const result = await userAuthService.initializeNewUser(user); - await result.save(); - - auditService.audit( - 'admin user create', - 'user', - 'admin user create', - req, - result.auditCopy() - ); - res.status(StatusCodes.OK).json(result.fullCopy()); -} + fastify.route({ + method: 'POST', + url: '/admin/user', + schema: { + description: 'Create a new user', + tags: ['User'], + body: { + type: 'object', + properties: { + name: { type: 'string' }, + organization: { type: 'string' }, + email: { type: 'string' }, + phone: { type: 'string' }, + username: { type: 'string' }, + password: { type: 'string' }, + roles: { type: 'object' }, + bypassAccessCheck: { type: 'boolean' } + } + } + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + const user = new User(User.createCopy(req.body)); + user.bypassAccessCheck = req.body.bypassAccessCheck; + user.roles = req.body.roles; + + if (config.get('auth.strategy') === 'local') { + user.provider = 'local'; + + // Need to set null passwords to empty string for mongoose validation to work + if (null == user.password) { + user.password = ''; + } + } else if (config.get('auth.strategy') === 'proxy-pki') { + user.provider = 'pki'; + + if (req.body.username) { + user.username = req.body.username; + user.providerData = { + dn: req.body.username, + dnLower: req.body.username.toLowerCase() + }; + } + } + + // Initialize the user + await user.save(); + + auditService + .audit( + 'admin user create', + 'user', + 'admin user create', + req, + user.auditCopy() + ) + .then(); + + return reply.send(user.fullCopy()); + } + }); -/** - * Admin Create a User (Local Strategy) - */ -export const adminCreateUser = async (req, res) => { - const user = new User(User.createCopy(req.body)); - user.bypassAccessCheck = req.body.bypassAccessCheck; - user.roles = req.body.roles; - user.provider = 'local'; - - // Need to set null passwords to empty string for mongoose validation to work - if (null == user.password) { - user.password = ''; - } + fastify.route({ + method: 'GET', + url: '/admin/users/csv/:id', + schema: { + description: 'Export users as CSV file', + tags: ['User'] + }, + preValidation: requireAdminAccess, + preHandler: loadExportConfigById, + handler: function (req, reply) { + const exportConfig = req.exportConfig as IExportConfig; + const exportQuery = req.exportQuery as FilterQuery; + + const fileName = `${config.get('app.instanceName')}-${ + exportConfig.type + }.csv`; + + // Replace `roles` column with individual columns for each role + const columns = exportConfig.config.cols.filter( + (col) => ['roles'].indexOf(col.key) === -1 + ); + if (columns.length !== exportConfig.config.cols.length) { + for (const role of Roles) { + columns.push({ + key: `roles.${role}`, + title: `${role} Role`, + callback: Callbacks.trueFalse + }); + } + } + + const populate = []; + + // Based on which columns are requested, handle property-specific behavior (ex. callbacks for the + // CSV service to make booleans and dates more human-readable) + columns.forEach((col) => { + col.title = col.title ?? _.capitalize(col.key); + + switch (col.key) { + case 'bypassAccessCheck': + col.callback = Callbacks.trueFalse; + break; + case 'lastLogin': + case 'created': + case 'updated': + case 'acceptedEua': + col.callback = Callbacks.isoDateString; + break; + case 'teams': + populate.push({ path: 'teams.team', select: 'name' }); + col.callback = Callbacks.mapAndJoinArray( + (team: { team: { name: string } }) => team.team.name + ); + break; + } + }); - await _adminCreateUser(user, req, res); -}; - -/** - * Admin Create a User (Pki Strategy) - */ -export const adminCreateUserPki = async (req, res) => { - const user = new User(User.createCopy(req.body)); - user.bypassAccessCheck = req.body.bypassAccessCheck; - user.roles = req.body.roles; - - if (null != req.body.username) { - user.username = req.body.username; - user.providerData = { - dn: req.body.username, - dnLower: req.body.username.toLowerCase() - }; - } - user.provider = 'pki'; + const cursor = userService.cursorSearch( + exportConfig.config, + exportConfig.config.s, + exportQuery, + [], + populate + ); - await _adminCreateUser(user, req, res); -}; + exportConfigController.exportCSV(req, reply, fileName, columns, cursor); + } + }); +} diff --git a/src/app/core/user/admin/user-admin.routes.ts b/src/app/core/user/admin/user-admin.routes.ts deleted file mode 100644 index d0cf2027..00000000 --- a/src/app/core/user/admin/user-admin.routes.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Router } from 'express'; - -import * as userAdminController from './user-admin.controller'; -import { config } from '../../../../dependencies'; -import { exportConfigById } from '../../export/export-config.controller'; -import { hasAdminAccess } from '../user-auth.middleware'; -import * as users from '../user.controller'; - -const router = Router(); - -/** - * Admin User Routes (require admin) - */ - -// Admin search users -router - .route('/admin/users') - .post(hasAdminAccess, userAdminController.adminSearchUsers); - -// Admin retrieve/update/delete -router - .route('/admin/user/:userId') - .get(hasAdminAccess, userAdminController.adminGetUser) - .post(hasAdminAccess, userAdminController.adminUpdateUser) - .delete(hasAdminAccess, userAdminController.adminDeleteUser); - -// Get user CSV using the specifies config id -router - .route('/admin/users/csv/:exportId') - .get(hasAdminAccess, userAdminController.adminGetCSV); - -// Admin retrieving a User field for all users in the system -router - .route('/admin/users/getAll') - .post(hasAdminAccess, userAdminController.adminGetAll); - -/** - * Routes that only apply to the 'local' passport strategy - */ -if (config.get('auth.strategy') === 'local') { - // Admin Create User - router - .route('/admin/user') - .post(hasAdminAccess, userAdminController.adminCreateUser); -} else if (config.get('auth.strategy') === 'proxy-pki') { - // Admin Create User - router - .route('/admin/user') - .post(hasAdminAccess, userAdminController.adminCreateUserPki); -} - -// Finish by binding the user middleware -router.param('userId', users.userById); - -router.param('exportId', exportConfigById); - -export = router; diff --git a/src/app/core/user/auth/auth.controller.ts b/src/app/core/user/auth/auth.controller.ts new file mode 100644 index 00000000..bb1f82a2 --- /dev/null +++ b/src/app/core/user/auth/auth.controller.ts @@ -0,0 +1,182 @@ +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; +import { FastifyInstance } from 'fastify'; + +import userAuthService from './user-authentication.service'; +import userAuthorizationService from './user-authorization.service'; +import userPasswordService from './user-password.service'; +import { auditService, config } from '../../../../dependencies'; +import { BadRequestError } from '../../../common/errors'; +import teamService from '../../teams/teams.service'; +import userEmailService from '../user-email.service'; +import { User } from '../user.model'; + +export default function (_fastify: FastifyInstance) { + const fastify = _fastify.withTypeProvider(); + fastify.route({ + method: 'POST', + url: '/auth/signin', + schema: { + tags: ['Auth'], + description: 'authenticates the user', + body: { + type: 'object', + properties: { + username: { type: 'string' }, + password: { type: 'string' } + }, + required: ['username', 'password'] + } + }, + handler: async function (req, reply) { + const user = await userAuthService.authenticateAndLogin(req, reply); + if (user) { + userAuthorizationService.updateRoles(user); + await teamService.updateTeams(user); + return reply.send(user); + } + return reply; + } + }); + + fastify.route({ + method: 'GET', + url: '/auth/signout', + schema: { + tags: ['Auth'], + description: 'Signs out the user.' + }, + handler: async function (req, reply) { + await req.logout(); + return reply.redirect('/'); + } + }); + + /** + * Routes that only apply to the 'local' passport strategy + */ + if (config.get('auth.strategy') === 'local') { + fastify.route({ + method: 'POST', + url: '/auth/signup', + schema: { + tags: ['Auth'], + description: 'Signs out the user.', + body: { + type: 'object', + properties: { + name: { type: 'string' }, + username: { type: 'string' }, + organization: { type: 'string' }, + email: { type: 'string' }, + password: { type: 'string' } + }, + required: ['name', 'username', 'password'] + } + }, + handler: async function (req, reply) { + const newUser = new User(req.body); + newUser.provider = 'local'; + + await newUser.save(); + + auditService + .audit('user signup', 'user', 'user signup', req, newUser.auditCopy()) + .then(); + + userEmailService.signupEmail(newUser, req).then(); + + const user = await userAuthService.authenticateAndLogin(req, reply); + if (user) { + userAuthorizationService.updateRoles(user); + await teamService.updateTeams(user); + return reply.send(user); + } + return reply; + } + }); + + fastify.route({ + method: 'POST', + url: '/auth/forgot', + schema: { + tags: ['Auth'], + description: 'Initiates password reset', + body: { + type: 'object', + properties: { + username: { type: 'string' } + }, + required: ['username'] + } + }, + handler: async function (req, reply) { + const user = await userPasswordService.initiatePasswordReset( + req.body.username, + req + ); + return reply.send( + `An email has been sent to ${user.email} with further instructions.` + ); + } + }); + + fastify.route({ + method: 'GET', + url: '/auth/reset/:token', + schema: { + tags: ['Auth'], + description: 'Validates password reset token', + params: { + type: 'object', + properties: { + token: { type: 'string' } + }, + required: ['token'] + } + }, + handler: async function (req, reply) { + const user = await userPasswordService.findUserForActiveToken( + req.params.token + ); + if (!user) { + throw new BadRequestError('invalid-token'); + } + return reply.send({ message: 'valid-token' }); + } + }); + + fastify.route({ + method: 'POST', + url: '/auth/reset/:token', + schema: { + tags: ['Auth'], + description: 'Resets password', + params: { + type: 'object', + properties: { + token: { type: 'string' } + }, + required: ['token'] + }, + body: { + type: 'object', + properties: { + password: { type: 'string' } + }, + required: ['password'] + } + }, + handler: async function (req, reply) { + const user = await userPasswordService.resetPasswordForToken( + req.params.token, + req.body.password + ); + await userPasswordService.sendPasswordResetConfirmEmail(user, req); + + return reply.send( + `An email has been sent to ${user.email} letting them know their password was reset.` + ); + } + }); + } +} diff --git a/src/app/core/user/auth/auth.middleware.ts b/src/app/core/user/auth/auth.middleware.ts new file mode 100644 index 00000000..61faa44e --- /dev/null +++ b/src/app/core/user/auth/auth.middleware.ts @@ -0,0 +1,140 @@ +import { FastifyReply, FastifyRequest } from 'fastify'; +import _ from 'lodash'; + +import userAuthorizationService from './user-authorization.service'; +import { config } from '../../../../dependencies'; +import { ForbiddenError, UnauthorizedError } from '../../../common/errors'; +import userAuthService from '../auth/user-authentication.service'; +import { requireEua } from '../eua/eua.middleware'; + +export type AuthRequirementFunction = ( + req: FastifyRequest, + reply: FastifyReply +) => Promise; + +export function requireLogin( + req: FastifyRequest, + rep: FastifyReply +): Promise { + if (req.isAuthenticated()) { + return Promise.resolve(); + } + // Only try to auto login if it's explicitly set in the config + if (config.get('auth.autoLogin')) { + return userAuthService.authenticateAndLogin(req, rep).then(); + } + + return Promise.reject(new UnauthorizedError('User is not logged in')); +} + +export function requireExternalRoles( + req: FastifyRequest, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + rep: FastifyReply +): Promise { + const requiredRoles = config.get('auth.requiredRoles'); + + // If there are required roles, check for them + if (req.user.bypassAccessCheck === false && requiredRoles.length > 0) { + // Get the user roles + const userRoles = _.isArray(req.user.externalRoles) + ? req.user.externalRoles + : []; + + // Reject if the user is missing required roles + if (_.difference(requiredRoles, userRoles).length > 0) { + return Promise.reject( + new ForbiddenError('User is missing required external roles') + ); + } + // Resolve if they had all the roles + return Promise.resolve(); + } + // Resolve if we don't need to check + return Promise.resolve(); +} + +export function requireRoles( + roles: string[], + errorMessage = 'User is missing required roles' +): AuthRequirementFunction { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + return function (req: FastifyRequest, rep: FastifyReply): Promise { + if (userAuthorizationService.hasRoles(req.user, roles)) { + return Promise.resolve(); + } + return Promise.reject(new ForbiddenError(errorMessage)); + }; +} + +export function requireAny( + ...requirements: AuthRequirementFunction[] +): AuthRequirementFunction { + return async (req: FastifyRequest, rep: FastifyReply): Promise => { + if (requirements.length > 0) { + let lastError: unknown; + for (const requirement of requirements) { + try { + // eslint-disable-next-line no-await-in-loop + await requirement(req, rep); + return Promise.resolve(); + } catch (error) { + // Failure means keep going. + lastError = error; + } + } + return Promise.reject(lastError); + } + return Promise.resolve(); + }; +} + +export function requireAll( + ...requirements: AuthRequirementFunction[] +): AuthRequirementFunction { + return async (req: FastifyRequest, rep: FastifyReply): Promise => { + for (const requirement of requirements) { + try { + // eslint-disable-next-line no-await-in-loop + await requirement(req, rep); + } catch (error) { + return Promise.reject(error); + } + } + return Promise.resolve(); + }; +} + +export const requireUserRole = requireRoles( + ['user'], + 'User account is inactive' +); +export const requireMachineRole = requireRoles(['machine']); +export const requireEditorRole = requireRoles(['editor']); +export const requireAdminRole = requireRoles(['admin']); +export const requireAuditorRole = requireRoles(['auditor']); + +export const requireAccess = requireAll( + requireLogin, + requireAny(requireUserRole, requireMachineRole), + requireExternalRoles, + requireEua +); + +export const requireAdminAccess = requireAll(requireLogin, requireAdminRole); + +export const requireAuditorAccess = requireAll( + requireLogin, + requireUserRole, + requireExternalRoles, + requireAuditorRole, + requireEua +); + +export const requireEditorAccess = requireAll( + requireLogin, + requireAny(requireUserRole, requireMachineRole), + requireExternalRoles, + requireAny(requireAdminRole, requireEditorRole), + requireEua +); diff --git a/src/app/core/user/auth/user-authentication.controller.spec.ts b/src/app/core/user/auth/user-authentication.controller.spec.ts deleted file mode 100644 index fea6a1a9..00000000 --- a/src/app/core/user/auth/user-authentication.controller.spec.ts +++ /dev/null @@ -1,743 +0,0 @@ -import assert from 'node:assert/strict'; - -import _ from 'lodash'; -import { DateTime } from 'luxon'; -import passport from 'passport'; -import { assert as sinonAssert, createSandbox } from 'sinon'; - -import * as userAuthenticationController from './user-authentication.controller'; -import { config } from '../../../../dependencies'; -import local from '../../../../lib/strategies/local'; -import proxyPki from '../../../../lib/strategies/proxy-pki'; -import { getResponseSpy } from '../../../../spec/helpers'; -import { - BadRequestError, - ForbiddenError, - UnauthorizedError -} from '../../../common/errors'; -import { - CacheEntry, - ICacheEntry -} from '../../access-checker/cache/cache-entry.model'; -import { IUser, User } from '../user.model'; - -// eslint-disable-next-line @typescript-eslint/no-empty-function -const emptyFn = () => {}; - -/** - * Helpers - */ -function clearDatabase() { - return Promise.all([ - User.deleteMany({}).exec(), - CacheEntry.deleteMany({}).exec() - ]); -} - -function userSpec(key): Partial { - return { - name: `${key} Name`, - email: `${key}@mail.com`, - username: `${key}_username`, - organization: `${key} Organization` - }; -} - -function localUserSpec(key) { - const spec = userSpec(key); - spec.provider = 'local'; - spec.password = 'password'; - return spec; -} - -function proxyPkiUserSpec(key) { - const spec = userSpec(key); - spec.provider = 'proxy-pki'; - spec.providerData = { - dn: key, - dnLower: key.toLowerCase() - }; - return spec; -} - -function cacheSpec(key): Partial { - return { - key: key.toLowerCase(), - value: { - name: `${key} Name`, - organization: `${key} Organization`, - email: `${key}@mail.com`, - username: `${key}_username` - } - }; -} - -/** - * Unit tests - */ -describe('User Auth Controller:', () => { - let res; - let sandbox; - - before(() => { - return clearDatabase(); - }); - - after(() => { - return clearDatabase(); - }); - - beforeEach(() => { - sandbox = createSandbox(); - res = getResponseSpy(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe('signout', () => { - it('should successfully redirect after logout', () => { - const req = { - logout: (cb: () => void) => { - if (cb) { - return cb(); - } - } - }; - - userAuthenticationController.signout(req, res); - - sinonAssert.calledWith(res.redirect, '/'); - }); - }); - - describe("'local' Strategy", () => { - const spec = { user: localUserSpec('user1') }; - let user; - - beforeEach(async () => { - await clearDatabase(); - user = await new User(spec.user).save(); - - //setup to use local passport - const configGetStub = sandbox.stub(config, 'get'); - configGetStub.withArgs('auth.strategy').returns('local'); - configGetStub.callThrough(); - - passport.use(local); - }); - - afterEach(() => { - return clearDatabase(); - }); - - describe('login', () => { - it('should succeed with correct credentials', async () => { - const req: Record = {}; - req.body = { - username: spec.user.username, - password: spec.user.password - }; - req.headers = {}; - req.logIn = (u, cb) => { - return cb && cb(); - }; - - await userAuthenticationController.signin(req, res, emptyFn); - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - - const [result] = res.json.getCall(0).args; - // Should return the user - assert(result); - assert.equal(result.username, user.username); - assert.equal(result.name, user.name); - // The user's password should have been removed - assert.equal(result.password, undefined); - }); - - it('should fail with incorrect password', async () => { - const req: Record = {}; - req.body = { username: user.username, password: 'wrong' }; - req.headers = {}; - req.logIn = (u, cb) => { - return cb && cb(); - }; - - await assert.rejects( - userAuthenticationController.signin(req, res, emptyFn), - new UnauthorizedError('Incorrect username or password') - ); - - sinonAssert.notCalled(res.status); - sinonAssert.notCalled(res.json); - }); - - it('should fail with missing password', async () => { - const req: Record = {}; - req.body = { username: user.username, password: undefined }; - req.headers = {}; - req.logIn = (_user, cb) => { - return cb && cb(); - }; - - await assert.rejects( - userAuthenticationController.signin(req, res, emptyFn), - { message: 'Missing credentials' } - ); - - sinonAssert.notCalled(res.status); - sinonAssert.notCalled(res.json); - }); - - it('should fail with missing username', async () => { - const req: Record = {}; - req.body = { username: undefined, password: 'asdfasdf' }; - req.headers = {}; - req.login = (_user, cb) => { - return cb && cb(); - }; - - await assert.rejects( - userAuthenticationController.signin(req, res, emptyFn), - { message: 'Missing credentials' } - ); - - sinonAssert.notCalled(res.status); - sinonAssert.notCalled(res.json); - }); - - it('should fail with unknown user', async () => { - const req: Record = {}; - req.body = { username: 'totally doesnt exist', password: 'asdfasdf' }; - req.headers = {}; - req.logIn = (_user, cb) => { - return cb && cb(); - }; - - await assert.rejects( - userAuthenticationController.signin(req, res, emptyFn), - new UnauthorizedError('Incorrect username or password') - ); - - sinonAssert.notCalled(res.status); - sinonAssert.notCalled(res.json); - }); - }); // describe - login - }); - - describe('Proxy PKI Strategy', () => { - // Specs for tests - const spec = { - cache: {} as Record>, - user: {} as Record> - }; - - // Synced User/Cache Entry - spec.cache.synced = cacheSpec('synced'); - spec.cache.synced.value.roles = ['role1', 'role2']; - spec.cache.synced.value.groups = ['group1', 'group2']; - spec.user.synced = proxyPkiUserSpec('synced'); - spec.user.synced.externalRoles = ['role1', 'role2']; - spec.user.synced.externalGroups = ['group1', 'group2']; - - // Different user metadata in cache - spec.cache.oldMd = cacheSpec('oldMd'); - spec.user.oldMd = proxyPkiUserSpec('oldMd'); - spec.cache.oldMd.value.name = 'New Name'; - spec.cache.oldMd.value.organization = 'New Organization'; - spec.cache.oldMd.value.email = 'new.email@mail.com'; - - // Different roles in cache - spec.cache.differentRolesAndGroups = cacheSpec('differentRoles'); - spec.cache.differentRolesAndGroups.value.roles = ['role1', 'role2']; - spec.cache.differentRolesAndGroups.value.groups = ['group1', 'group2']; - spec.user.differentRolesAndGroups = proxyPkiUserSpec('differentRoles'); - spec.user.differentRolesAndGroups.externalRoles = ['role3', 'role4']; - spec.user.differentRolesAndGroups.externalGroups = ['group3', 'group4']; - - // Missing from cache, no bypass - spec.user.missingUser = proxyPkiUserSpec('missingUser'); - spec.user.missingUser.externalRoles = ['role1', 'role2']; - spec.user.missingUser.externalGroups = ['group1', 'group2']; - - // Expired in cache, no bypass - spec.user.expiredUser = proxyPkiUserSpec('expiredUser'); - spec.cache.expiredUser = cacheSpec('expiredUser'); - spec.cache.expiredUser.ts = DateTime.now().minus({ days: 2 }).toJSDate(); - spec.user.expiredUser.externalRoles = ['role1', 'role2']; - spec.user.expiredUser.externalGroups = ['group1', 'group2']; - - // Missing from cache, with bypass - spec.user.missingUserBypassed = proxyPkiUserSpec('missingUserBypassed'); - spec.user.missingUserBypassed.bypassAccessCheck = true; - spec.user.missingUserBypassed.externalRoles = ['role1', 'role2']; - spec.user.missingUserBypassed.externalGroups = ['group1', 'group2']; - - // Missing from cache, in access checker, with bypass with local changes - spec.user.userBypassed = proxyPkiUserSpec('userBypassed'); - spec.user.userBypassed.bypassAccessCheck = true; - spec.user.userBypassed.name = 'My New Name'; - spec.user.userBypassed.organization = 'My New Org'; - - // Only in cache - spec.cache.cacheOnly = cacheSpec('cacheOnly'); - spec.cache.cacheOnly.value.roles = ['role1', 'role2', 'role3']; - spec.cache.cacheOnly.value.groups = ['group1', 'group2', 'group3']; - - spec.user.userCanProxy = proxyPkiUserSpec('proxyableUser'); - spec.user.userCanProxy.canProxy = true; - spec.user.userCanProxy.name = 'Trusted Server'; - spec.user.userCanProxy.organization = 'Trusted Organization'; - - const cache = {}; - const user = {}; - - beforeEach(async () => { - await clearDatabase(); - let defers = []; - defers = defers.concat( - _.keys(spec.cache).map(async (k) => { - cache[k] = await new CacheEntry(spec.cache[k]).save(); - }) - ); - defers = defers.concat( - _.keys(spec.user).map(async (k_1) => { - user[k_1] = await new User(spec.user[k_1]).save(); - }) - ); - await Promise.all(defers); - - const configGetStub = sandbox.stub(config, 'get'); - configGetStub.withArgs('auth.strategy').returns('proxy-pki'); - configGetStub - .withArgs('auth.accessChecker.provider.file') - .returns('src/app/core/access-checker/providers/example.provider'); - configGetStub.withArgs('auth.accessChecker.provider.config').returns({ - userbypassed: { - name: 'Invalid Name', - organization: 'Invalid Org', - email: 'invalid@invalid.org', - username: 'invalid' - } - }); - configGetStub.callThrough(); - - // All of the data is loaded, so initialize proxy-pki - passport.use(proxyPki); - }); - - afterEach(() => { - return clearDatabase(); - }); - - /** - * Test basic login where access checker isn't really involved. - * Granting access and denying access based on known/unknown dn - */ - describe('basic login', () => { - const req: Record = {}; - req.logIn = (_user, cb) => { - return cb && cb(); - }; - - it('should work when user is synced with access checker', async () => { - req.headers = { - [config.get('proxyPkiPrimaryUserHeader')]: - spec.user.synced.providerData.dn - }; - - await userAuthenticationController.signin(req, res, emptyFn); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - - const [result] = res.json.getCall(0).args; - - assert(result); - assert.equal(result.name, spec.user.synced.name); - assert.equal(result.organization, spec.user.synced.organization); - assert.equal(result.email, spec.user.synced.email); - assert.equal(result.username, spec.user.synced.username); - - assert( - Array.isArray(result.externalRoles), - 'expect externalRoles should be an Array' - ); - assert.deepStrictEqual( - result.externalRoles, - spec.user.synced.externalRoles - ); - }); - - // No DN header - it('should fail when there is no dn', async () => { - req.headers = {}; - - await assert.rejects( - userAuthenticationController.signin(req, res, emptyFn), - new BadRequestError('Missing certificate') - ); - - sinonAssert.notCalled(res.status); - sinonAssert.notCalled(res.json); - }); - - // Unknown DN header - it('should fail when the dn is unknown and auto create is disabled', async () => { - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - const configGetStub = config.get as any; - configGetStub.withArgs('auth.autoCreateAccounts').returns(false); - - req.headers = { - [config.get('proxyPkiPrimaryUserHeader')]: 'unknown' - }; - - await assert.rejects( - userAuthenticationController.signin(req, {}, emptyFn), - new UnauthorizedError( - 'Could not authenticate request, please verify your credentials.' - ) - ); - }); - }); - - /** - * Test situations where access checking is more involved because the cache - * is not in sync with the user - */ - describe('syncing with access checker', () => { - const req: Record = {}; - req.logIn = (_user, cb) => { - return cb && cb(); - }; - - it('should update the user info from access checker on login', async () => { - req.headers = { - [config.get('proxyPkiPrimaryUserHeader')]: - spec.user.oldMd.providerData.dn - }; - - await userAuthenticationController.signin(req, res, emptyFn); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - const [result] = res.json.getCall(0).args; - - assert(result); - assert.equal(result.name, spec.cache.oldMd.value.name); - assert.equal(result.organization, spec.cache.oldMd.value.organization); - assert.equal(result.email, spec.cache.oldMd.value.email); - assert.equal(result.username, spec.cache.oldMd.value.username); - }); - - it('should sync roles and groups from access checker on login', async () => { - req.headers = { - [config.get('proxyPkiPrimaryUserHeader')]: - spec.user.differentRolesAndGroups.providerData.dn - }; - - await userAuthenticationController.signin(req, res, emptyFn); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - - const [result] = res.json.getCall(0).args; - assert(result); - assert( - Array.isArray(result.externalRoles), - 'expect externalRoles should be an Array' - ); - assert.deepStrictEqual( - result.externalRoles, - spec.cache.differentRolesAndGroups.value.roles - ); - - assert( - Array.isArray(result.externalGroups), - 'expect externalGroups should be an Array' - ); - assert.deepStrictEqual( - result.externalGroups, - spec.cache.differentRolesAndGroups.value.groups - ); - }); - }); - - describe('missing or expired cache entries with no bypass', () => { - const req: Record = {}; - req.logIn = (_user, cb) => { - return cb && cb(); - }; - - it('should have external roles and groups removed on login when missing from cache', async () => { - req.headers = { - [config.get('proxyPkiPrimaryUserHeader')]: - spec.user.missingUser.providerData.dn - }; - - await userAuthenticationController.signin(req, res, emptyFn); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - - const [result] = res.json.getCall(0).args; - assert(result); - assert.equal(result.name, spec.user.missingUser.name); - assert.equal(result.organization, spec.user.missingUser.organization); - assert.equal(result.email, spec.user.missingUser.email); - assert.equal(result.username, spec.user.missingUser.username); - - assert( - Array.isArray(result.externalRoles), - 'expect externalRoles should be an Array' - ); - assert.equal(result.externalRoles.length, 0); - - assert( - Array.isArray(result.externalGroups), - 'expect externalGroups should be an Array' - ); - assert.equal(result.externalGroups.length, 0); - }); - - it('should have external roles and groups removed on login when cache expired', async () => { - req.headers = { - [config.get('proxyPkiPrimaryUserHeader')]: - spec.user.expiredUser.providerData.dn - }; - - await userAuthenticationController.signin(req, res, emptyFn); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - - const [result] = res.json.getCall(0).args; - assert(result); - assert.equal(result.name, spec.user.expiredUser.name); - assert.equal(result.organization, spec.user.expiredUser.organization); - assert.equal(result.email, spec.user.expiredUser.email); - assert.equal(result.username, spec.user.expiredUser.username); - - assert( - Array.isArray(result.externalRoles), - 'expect externalRoles should be an Array' - ); - assert.equal(result.externalRoles.length, 0); - - assert( - Array.isArray(result.externalGroups), - 'expect externalGroups should be an Array' - ); - assert.equal(result.externalGroups.length, 0); - }); - }); - - describe('missing cache entries with bypass access checker enabled', () => { - const req: Record = {}; - req.logIn = (_user, cb) => { - return cb && cb(); - }; - - it('should preserve user info, roles and groups on login', async () => { - req.headers = { - [config.get('proxyPkiPrimaryUserHeader')]: - spec.user.missingUserBypassed.providerData.dn - }; - - await userAuthenticationController.signin(req, res, emptyFn); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - - const [result] = res.json.getCall(0).args; - - assert(result); - assert.equal(result.name, spec.user.missingUserBypassed.name); - assert.equal( - result.organization, - spec.user.missingUserBypassed.organization - ); - assert.equal(result.email, spec.user.missingUserBypassed.email); - assert.equal(result.username, spec.user.missingUserBypassed.username); - - assert( - Array.isArray(result.externalRoles), - 'expect externalRoles should be an Array' - ); - assert.deepStrictEqual( - result.externalRoles, - spec.user.missingUserBypassed.externalRoles - ); - - assert( - Array.isArray(result.externalGroups), - 'expect externalGroups should be an Array' - ); - assert.deepStrictEqual( - result.externalGroups, - spec.user.missingUserBypassed.externalGroups - ); - }); - }); - - describe('in cache, access checker enabled, but with fields modified locally', () => { - const req: Record = {}; - req.logIn = (_user, cb) => { - return cb && cb(); - }; - - it('should preserve user info, roles and groups on login', async () => { - req.headers = { - [config.get('proxyPkiPrimaryUserHeader')]: - spec.user.userBypassed.providerData.dn - }; - - await userAuthenticationController.signin(req, res, emptyFn); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - - const [result] = res.json.getCall(0).args; - assert(result); - assert.equal(result.name, spec.user.userBypassed.name); - assert.equal(result.organization, spec.user.userBypassed.organization); - assert.equal(result.email, spec.user.userBypassed.email); - assert.equal(result.username, spec.user.userBypassed.username); - - assert( - Array.isArray(result.externalRoles), - 'expect externalRoles should be an Array' - ); - assert.equal(result.externalRoles.length, 0); - - assert( - Array.isArray(result.externalGroups), - 'expect externalGroups should be an Array' - ); - assert.equal(result.externalGroups.length, 0); - }); - }); - - describe('auto create accounts', () => { - const req: Record = {}; - req.logIn = (_user, cb) => { - return cb && cb(); - }; - - it('should create a new account from access checker information', async () => { - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - const configGetStub = config.get as any; - configGetStub.withArgs('auth.autoCreateAccounts').returns(true); - - req.headers = { - [config.get('proxyPkiPrimaryUserHeader')]: - spec.cache.cacheOnly.key - }; - - await userAuthenticationController.signin(req, res, () => { - sinonAssert.error('should not be called'); - }); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - - const [result] = res.json.getCall(0).args; - assert(result); - assert.equal(result.name, spec.cache.cacheOnly.value.name); - assert.equal( - result.organization, - spec.cache.cacheOnly.value.organization - ); - assert.equal(result.email, spec.cache.cacheOnly.value.email); - assert.equal(result.username, spec.cache.cacheOnly.value.username); - - assert( - Array.isArray(result.externalRoles), - 'expect externalRoles should be an Array' - ); - assert.deepStrictEqual( - result.externalRoles, - spec.cache.cacheOnly.value.roles - ); - - assert( - Array.isArray(result.externalGroups), - 'expect externalGroups should be an Array' - ); - assert.deepStrictEqual( - result.externalGroups, - spec.cache.cacheOnly.value.groups - ); - }); - }); - - describe('proxy for other users', () => { - /** - * @type {any} - */ - let req; - - beforeEach(() => { - req = {}; - req.logIn = (_user, cb) => { - return cb && cb(); - }; - }); - - it('should fail when not authorized to proxy users', async () => { - req.headers = { - [config.get('proxyPkiPrimaryUserHeader')]: - spec.user.synced.providerData.dn, - [config.get('proxyPkiProxiedUserHeader')]: - spec.user.userBypassed.providerData.dn - }; - - await assert.rejects( - userAuthenticationController.signin(req, res, emptyFn), - new ForbiddenError( - 'Not approved to proxy users. Please verify your credentials.' - ) - ); - - sinonAssert.notCalled(res.status); - sinonAssert.notCalled(res.json); - }); - - it('should succeed when authorized to proxy users', async () => { - req.headers = { - [config.get('proxyPkiPrimaryUserHeader')]: - spec.user.userCanProxy.providerData.dn, - [config.get('proxyPkiProxiedUserHeader')]: - spec.user.userBypassed.providerData.dn - }; - - await userAuthenticationController.signin(req, res, emptyFn); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - - // Verify that the user returned is the proxied user (not the primary user) - const [result] = res.json.getCall(0).args; - assert(result); - assert.equal(result.name, spec.user.userBypassed.name); - assert.equal(result.organization, spec.user.userBypassed.organization); - assert.equal(result.email, spec.user.userBypassed.email); - assert.equal(result.username, spec.user.userBypassed.username); - - assert( - Array.isArray(result.externalRoles), - 'expect externalRoles should be an Array' - ); - assert.equal(result.externalRoles.length, 0); - - assert( - Array.isArray(result.externalGroups), - 'expect externalGroups should be an Array' - ); - assert.equal(result.externalGroups.length, 0); - }); - }); - }); -}); diff --git a/src/app/core/user/auth/user-authentication.controller.ts b/src/app/core/user/auth/user-authentication.controller.ts deleted file mode 100644 index f2603ab7..00000000 --- a/src/app/core/user/auth/user-authentication.controller.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { StatusCodes } from 'http-status-codes'; - -import userAuthService from './user-authentication.service'; -import userAuthorizationService from './user-authorization.service'; -import { auditService, config } from '../../../../dependencies'; -import teamService from '../../teams/teams.service'; -import userEmailService from '../user-email.service'; -import { UserDocument, User } from '../user.model'; - -/** - * ========================================================== - * Private methods - * ========================================================== - */ - -// Signup the user - creates the user object and logs in the user -const _signup = async (user: UserDocument, req, res) => { - // Initialize the user - const newUser = await userAuthService.initializeNewUser(user); - await newUser.save(); - - userEmailService.signupEmail(newUser, req); - userEmailService.welcomeNoAccessEmail(newUser, req); - - auditService.audit( - 'user signup', - 'user', - 'user signup', - req, - newUser.auditCopy() - ); - - const result = await userAuthService.login(user, req); - userAuthorizationService.updateRoles(result); - await teamService.updateTeams(result); - res.status(StatusCodes.OK).json(result); -}; - -/** - * ========================================================== - * Public Methods - * ========================================================== - */ - -/** - * Local Signup strategy. Provide a username/password - * and user info in the request body. - */ -export const signup = (req, res) => { - const user = new User(User.createCopy(req.body)); - user.provider = 'local'; - - // Need to set null passwords to empty string for mongoose validation to work - if (null == user.password) { - user.password = ''; - } - - return _signup(user, req, res); -}; - -/** - * Proxy PKI signup. Provide a DN in the request header - * and then user info in the request body. - */ -export const proxyPkiSignup = (req, res) => { - const dn = req.headers[config.get('auth.header')]; - if (null == dn) { - res.status('400').json({ message: 'Missing PKI information.' }); - return; - } - - const user = new User(User.createCopy(req.body)); - user.providerData = { dn: dn, dnLower: dn.toLowerCase() }; - user.username = dn; //TODO: extract the username - user.provider = 'pki'; - - return _signup(user, req, res); -}; - -/** - * Local Signin - */ -export const signin = async (req, res, next) => { - const result = await userAuthService.authenticateAndLogin(req, res, next); - - userAuthorizationService.updateRoles(result); - await teamService.updateTeams(result); - res.status(StatusCodes.OK).json(result); -}; - -/** - * Signout - logs the user out and redirects them - */ -export const signout = (req, res) => { - req.logout(() => { - res.redirect('/'); - }); -}; diff --git a/src/app/core/user/auth/user-authentication.service.ts b/src/app/core/user/auth/user-authentication.service.ts index 83007bd4..a5b52832 100644 --- a/src/app/core/user/auth/user-authentication.service.ts +++ b/src/app/core/user/auth/user-authentication.service.ts @@ -1,7 +1,7 @@ -import passport from 'passport'; +import { FastifyReply, FastifyRequest } from 'fastify'; import { auditService, config } from '../../../../dependencies'; -import { InternalServerError, UnauthorizedError } from '../../../common/errors'; +import { UnauthorizedError } from '../../../common/errors'; import accessChecker from '../../access-checker/access-checker.service'; import userEmailService from '../user-email.service'; import { IUser, UserDocument, User, UserModel } from '../user.model'; @@ -20,97 +20,54 @@ class UserAuthenticationService { return Promise.resolve(user); } - /** - * Login the user - * Does the work to log the user into the system - * Updates the last logged in time - * Audits the action - */ - login(user: UserDocument, req): Promise { - return new Promise((resolve, reject) => { - // Calls the login function (which goes to passport) - req.login(user, (err) => { - if (err) { - return reject(new InternalServerError(err)); - } - - userEmailService.welcomeWithAccessEmail(user, req); - - // update the user's last login time - this.userModel - .findByIdAndUpdate( - user._id, - { lastLogin: Date.now() }, - { new: true, upsert: false } - ) - .then((_user: UserDocument) => { - return resolve(_user.fullCopy()); - }) - .catch((err) => { - return reject(new InternalServerError(err.message)); - }); - - // Audit the login - auditService.audit( - 'User successfully logged in', - 'user-authentication', - 'authentication succeeded', - req, - {} - ); - }); - }); + async login(req: FastifyRequest): Promise { + userEmailService.welcomeNoAccessEmail(req.user, req).then(); + userEmailService.welcomeWithAccessEmail(req.user, req).then(); + + // Audit the login + auditService + .audit( + 'User successfully logged in', + 'user-authentication', + 'authentication succeeded', + req, + {} + ) + .then(); + + // update the user's last login time + const user = await this.userModel.findByIdAndUpdate( + req.user._id, + { lastLogin: Date.now() }, + { new: true, upsert: false } + ); + return user.fullCopy(); } - /** - * Authenticate and then login depending on the outcome - */ - authenticateAndLogin(req, res, next): Promise { - return new Promise((resolve, reject) => { - // Attempt to authenticate the user using passport - passport.authenticate( - config.get('auth.strategy'), - (err, user, info, status) => { - // If there was an error - if (err) { - // Reject the promise with a 500 error - return reject(new InternalServerError(err)); - } - // If the authentication failed - if (!user) { - // In the case of a auth failure, info should have the reason - // Here is a hack for the local strategy... - if (null == info.status && null != status) { - info.status = status; - if (info.message === 'Missing credentials') { - info.type = 'missing-credentials'; - } - } - - // Try to grab the username from the request - const username = - req.body && req.body.username - ? req.body.username - : 'none provided'; - - // Audit the failed attempt - auditService.audit( - info.message, - 'user-authentication', - 'authentication failed', - req, - { username: username } - ); - - return reject(info); - } - // Else the authentication was successful - // Set the user ip if available. - user.ip = req.headers?.['x-real-ip'] ?? null; - this.login(user, req).then(resolve).catch(reject); - } - )(req, res, next); - }); + async authenticateAndLogin( + req: FastifyRequest, + reply: FastifyReply + ): Promise { + await req.passport + .authenticate(config.get('auth.strategy')) + .bind(req.server)(req, reply); + if (!req.user) { + // Try to grab the username from the request + const username = req.body?.['username'] ?? 'none provided'; + + // Audit the failed attempt + auditService + .audit( + 'Authentication failed', + 'user-authentication', + 'authentication failed', + req, + { username: username } + ) + .then(); + } else { + return this.login(req); + } } copyACMetadata(dest, src) { diff --git a/src/app/core/user/auth/user-authenticiation.routes.ts b/src/app/core/user/auth/user-authenticiation.routes.ts deleted file mode 100644 index 9d67cf9f..00000000 --- a/src/app/core/user/auth/user-authenticiation.routes.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Router } from 'express'; - -import * as userAuthentication from './user-authentication.controller'; -import * as userPassword from './user-password.controller'; -import { config } from '../../../../dependencies'; -import { logger } from '../../../../lib/logger'; - -const router = Router(); - -/** - * @swagger - * /auth/signin: - * post: - * tags: [Auth] - * description: authenticates the user. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * username: - * type: string - * password: - * type: string - * example: - * username: 'some_user' - * password: 'abc124' - * responses: - * '200': - * description: The authenticated user's profile - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - */ -router.route('/auth/signin').post(userAuthentication.signin); - -/** - * @swagger - * /auth/signout: - * get: - * tags: [Auth] - * description: signs out the user. - * responses: - * '200': - * description: User was signed out. - */ -router.route('/auth/signout').get(userAuthentication.signout); - -/** - * Routes that only apply to the 'local' passport strategy - */ -if (config.get('auth.strategy') === 'local') { - logger.info('Configuring local user authentication routes.'); - - // Default setup is basic local auth - router.route('/auth/signup').post(userAuthentication.signup); - - router.route('/auth/forgot').post(userPassword.forgot); - router.route('/auth/reset/:token').get(userPassword.validateResetToken); - router.route('/auth/reset/:token').post(userPassword.reset); -} else if (config.get('auth.strategy') === 'proxy-pki') { - /** - * Routes that only apply to the 'proxy-pki' passport strategy - */ - logger.info('Configuring proxy-pki user authentication routes.'); - - // DN passed via header from proxy - router.route('/auth/signup').post(userAuthentication.proxyPkiSignup); -} - -export = router; diff --git a/src/app/core/user/auth/user-password.controller.ts b/src/app/core/user/auth/user-password.controller.ts deleted file mode 100644 index 808095a0..00000000 --- a/src/app/core/user/auth/user-password.controller.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { StatusCodes } from 'http-status-codes'; - -import userPasswordService from './user-password.service'; -import { BadRequestError } from '../../../common/errors'; - -/** - * Forgot for reset password (forgot POST) - */ -export const forgot = async (req, res) => { - // Make sure there is a username - if (!req.body.username) { - throw new BadRequestError('Username is missing.'); - } - - try { - const token = await userPasswordService.generateToken(); - - const user = await userPasswordService.setResetTokenForUser( - req.body.username, - token - ); - - await userPasswordService.sendResetPasswordEmail(user, token, req); - - res - .status(StatusCodes.OK) - .json( - `An email has been sent to ${user.email} with further instructions.` - ); - } catch (error) { - throw new BadRequestError('Failure generating reset password token.'); - } -}; - -/** - * Reset password GET from email token - */ -export const validateResetToken = async (req, res) => { - const user = await userPasswordService.findUserForActiveToken( - req.params.token - ); - - if (!user) { - throw new BadRequestError('invalid-token'); - } - res.status(StatusCodes.OK).json({ message: 'valid-token' }); -}; - -/** - * Reset password POST from email token - */ -export const reset = async (req, res) => { - // Init Variables - const password = req.body.password; - - // Make sure there is a password - if (!password) { - throw new BadRequestError('Password is missing.'); - } - - try { - const user = await userPasswordService.resetPasswordForToken( - req.params.token, - req.body.password - ); - - await userPasswordService.sendPasswordResetConfirmEmail(user, req); - - res - .status(StatusCodes.OK) - .json( - `An email has been sent to ${user.email} letting them know their password was reset.` - ); - } catch (error) { - throw new BadRequestError('Failure resetting password.'); - } -}; diff --git a/src/app/core/user/auth/user-password.service.ts b/src/app/core/user/auth/user-password.service.ts index 4c5df046..00a41d8e 100644 --- a/src/app/core/user/auth/user-password.service.ts +++ b/src/app/core/user/auth/user-password.service.ts @@ -18,6 +18,20 @@ class UserPasswordService { }).exec(); } + async initiatePasswordReset(username: string, req: unknown) { + try { + const token = await this.generateToken(); + + const user = await this.setResetTokenForUser(username, token); + + await this.sendResetPasswordEmail(user, token, req); + + return user; + } catch (error) { + throw new BadRequestError('Failure generating reset password token.'); + } + } + async generateToken(): Promise { const buffer = await promisify(crypto.randomBytes)(20); return buffer.toString('hex'); @@ -53,23 +67,22 @@ class UserPasswordService { token: string, password: string ): Promise { - let user; try { - user = await module.exports.findUserForActiveToken(token); - } catch { - // ignore error - } + const user = await module.exports.findUserForActiveToken(token); - if (!user) { - return Promise.reject( - new BadRequestError('Password reset token is invalid or has expired.') - ); - } + if (!user) { + return Promise.reject( + new BadRequestError('Password reset token is invalid or has expired.') + ); + } - user.password = password; - user.resetPasswordToken = undefined; - user.resetPasswordExpires = undefined; - return user.save(); + user.password = password; + user.resetPasswordToken = undefined; + user.resetPasswordExpires = undefined; + return user.save(); + } catch (error) { + throw new BadRequestError('Failure resetting password.'); + } } // Send email to user with instructions on resetting password @@ -81,7 +94,6 @@ class UserPasswordService { try { const mailOptions = await emailService.generateMailOptions( user, - req, config.get('coreEmails.resetPassword'), { token: token @@ -107,7 +119,6 @@ class UserPasswordService { try { const mailOptions = await emailService.generateMailOptions( user, - req, config.get('coreEmails.resetPasswordConfirm'), {}, {}, diff --git a/src/app/core/user/eua/eua.controller.spec.ts b/src/app/core/user/eua/eua.controller.spec.ts deleted file mode 100644 index 93dafc3d..00000000 --- a/src/app/core/user/eua/eua.controller.spec.ts +++ /dev/null @@ -1,294 +0,0 @@ -import assert from 'node:assert/strict'; - -import { Request } from 'express'; -import { assert as sinonAssert, createSandbox, match, stub } from 'sinon'; - -import * as euaController from './eua.controller'; -import { UserAgreement, UserAgreementDocument } from './eua.model'; -import euaService from './eua.service'; -import { auditService } from '../../../../dependencies'; -import { getResponseSpy } from '../../../../spec/helpers'; -import { ForbiddenError } from '../../../common/errors'; -import { User } from '../user.model'; - -/** - * Unit tests - */ -describe('EUA Controller:', () => { - let res; - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - res = getResponseSpy(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe('searchEuas', () => { - it('search returns euas', async () => { - const req = { - body: {} - }; - - sandbox.stub(euaService, 'search').resolves(); - - await euaController.searchEuas(req, res); - - sinonAssert.calledOnce(euaService.search); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('acceptEua', () => { - it('accept eua is successful', async () => { - const req = { - user: new User({}) - }; - - sandbox.stub(euaService, 'acceptEua').resolves(req.user); - - await euaController.acceptEua(req, res); - - sinonAssert.calledOnce(euaService.acceptEua); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('publishEua', () => { - it('eua found', async () => { - const req = { - euaParam: new UserAgreement({ _id: '12345' }), - user: new User({}) - }; - - sandbox.stub(auditService, 'audit').resolves(); - sandbox.stub(euaService, 'publishEua').resolves(req.euaParam); - - await euaController.publishEua(req, res); - - sinonAssert.calledOnce(euaService.publishEua); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('createEua', () => { - it('create successful', async () => { - const req = { - body: {}, - user: new User({}) - }; - sandbox.stub(auditService, 'audit').resolves(); - sandbox.stub(euaService, 'create').resolves(new UserAgreement()); - - await euaController.createEua(req, res); - - sinonAssert.calledOnce(euaService.create); - sinonAssert.calledOnce(auditService.audit); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('getCurrentEua', () => { - it('current eua not found', async () => { - const req = {}; - await euaController.getCurrentEua(req, res); - - sandbox.stub(euaService, 'getCurrentEua').resolves(null); - - await euaController.getCurrentEua(req, res); - - sinonAssert.calledOnce(euaService.getCurrentEua); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - - it('current eua found', async () => { - const req = { - euaParam: new UserAgreement({ _id: '12345' }), - user: new User({}) - }; - - sandbox.stub(euaService, 'getCurrentEua').resolves({}); - - await euaController.getCurrentEua(req, res); - - sinonAssert.calledOnce(euaService.getCurrentEua); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('read', () => { - it('eua found', async () => { - const req = { - euaParam: new UserAgreement({ _id: '12345' }), - user: new User({}) - }; - - await euaController.read(req, res); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('updateEua', () => { - it('eua found', async () => { - const req = { - euaParam: new UserAgreement({ _id: '12345' }), - user: new User({}) - }; - - sandbox.stub(auditService, 'audit').resolves(); - sandbox.stub(euaService, 'update').resolves(req.euaParam); - - await euaController.updateEua(req, res); - - sinonAssert.calledOnce(auditService.audit); - sinonAssert.calledOnce(euaService.update); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('deleteEua', () => { - it('eua found', async () => { - const req = { - euaParam: new UserAgreement({ _id: '12345' }), - user: new User({}) - }; - - sandbox.stub(auditService, 'audit').resolves(); - sandbox.stub(euaService, 'delete').resolves(req.euaParam); - - await euaController.deleteEua(req, res); - - sinonAssert.calledOnce(auditService.audit); - sinonAssert.calledOnce(euaService.delete); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('euaById', () => { - it('eua found', async () => { - sandbox.stub(euaService, 'read').resolves({}); - - const nextFn = stub(); - const req = {} as Request & { - euaParam: UserAgreementDocument; - }; - - await euaController.euaById(req, {}, nextFn, 'id'); - - assert(req.euaParam); - sinonAssert.calledWith(nextFn); - }); - - it('eua not found', async () => { - sandbox.stub(euaService, 'read').resolves(); - - const nextFn = stub(); - const req = {} as Request & { - euaParam: UserAgreementDocument; - }; - - await euaController.euaById(req, {}, nextFn, 'id'); - - assert.equal(req.euaParam, undefined); - sinonAssert.calledWith( - nextFn, - match - .instanceOf(Error) - .and(match.has('message', 'Failed to load User Agreement id')) - ); - }); - }); - - describe('requiresEua:', () => { - const successTests = [ - { - currentEuaReturnValue: undefined, - input: {}, - expected: undefined, - description: 'Current eua is undefined' - }, - { - currentEuaReturnValue: null, - expected: undefined, - description: 'Current eua is null' - }, - { - currentEuaReturnValue: {}, - input: {}, - expected: undefined, - description: 'Current eua is not published' - }, - { - currentEuaReturnValue: { - published: 1 - }, - input: { user: { acceptedEua: 2 } }, - expected: undefined, - description: 'Current eua is accepted' - } - ]; - - successTests.forEach((test) => { - it(test.description, async () => { - sandbox - .stub(euaService, 'getCurrentEua') - .resolves(test.currentEuaReturnValue); - - const result = await euaController.requiresEua(test.input); - - assert.equal(result, test.expected); - }); - }); - - const euaNotAcceptedTests = [ - { - currentEuaReturnValue: { - published: 2 - }, - input: { user: {} }, - description: 'user has not accepted the current eua.' - }, - { - currentEuaReturnValue: { - published: 2 - }, - input: { user: { acceptedEua: 1 } }, - description: 'User has accepted an older eua.' - } - ]; - - euaNotAcceptedTests.forEach((test) => { - it(test.description, async () => { - sandbox - .stub(euaService, 'getCurrentEua') - .resolves(test.currentEuaReturnValue); - - await assert.rejects( - euaController.requiresEua(test.input), - new ForbiddenError('User must accept end-user agreement.') - ); - }); - }); - }); -}); diff --git a/src/app/core/user/eua/eua.controller.ts b/src/app/core/user/eua/eua.controller.ts index a6f90ac2..1f7dbb40 100644 --- a/src/app/core/user/eua/eua.controller.ts +++ b/src/app/core/user/eua/eua.controller.ts @@ -1,136 +1,195 @@ -import { StatusCodes } from 'http-status-codes'; +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; +import { FastifyInstance, FastifyRequest } from 'fastify'; import euaService from './eua.service'; import { auditService } from '../../../../dependencies'; -import { ForbiddenError } from '../../../common/errors'; - -// Search (Retrieve) all user Agreements -export const searchEuas = async (req, res) => { - // Handle the query/search - const query = req.body.q ?? {}; - const search = req.body.s ?? null; - - const results = await euaService.search(req.query, search, query); - res.status(StatusCodes.OK).json(results); -}; - -// Publish the EUA -export const publishEua = async (req, res) => { - // The eua is placed into this parameter by the middleware - const eua = req.euaParam; - - const result = await euaService.publishEua(eua); - - // Audit eua create - await auditService.audit( - 'eua published', - 'eua', - 'published', - req, - result.auditCopy() - ); - - res.status(StatusCodes.OK).json(result); -}; - -// Accept the current EUA -export const acceptEua = async (req, res) => { - const user = await euaService.acceptEua(req.user); - - // Audit accepted eua - await auditService.audit('eua accepted', 'eua', 'accepted', req, {}); - - res.status(StatusCodes.OK).json(user.fullCopy()); -}; - -// Create a new User Agreement -export const createEua = async (req, res) => { - const result = await euaService.create(req.body); - - // Audit eua create - await auditService.audit( - 'eua create', - 'eua', - 'create', - req, - result.auditCopy() - ); - - res.status(StatusCodes.OK).json(result); -}; - -// Retrieve the Current User Agreement -export const getCurrentEua = async (req, res) => { - const results = await euaService.getCurrentEua(); - res.status(StatusCodes.OK).json(results); -}; - -// Retrieve the arbitrary User Agreement -export const read = (req, res) => { - res.status(StatusCodes.OK).json(req.euaParam); -}; - -// Update a User Agreement -export const updateEua = async (req, res) => { - // A copy of the original eua for auditing purposes - const originalEua = req.euaParam.auditCopy(); - - const results = await euaService.update(req.euaParam, req.body); - - // Audit user update - await auditService.audit('end user agreement updated', 'eua', 'update', req, { - before: originalEua, - after: results.auditCopy() +import { PagingQueryStringSchema, SearchBodySchema } from '../../core.schemas'; +import { requireAdminAccess, requireLogin } from '../auth/auth.middleware'; + +export default function (_fastify: FastifyInstance) { + const fastify = _fastify.withTypeProvider(); + fastify.route({ + method: 'POST', + url: '/euas', + schema: { + description: 'Returns EUAs matching search criteria', + tags: ['EUA'], + body: SearchBodySchema, + querystring: PagingQueryStringSchema + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + // Handle the query/search + const query = req.body.q ?? {}; + const search = req.body.s ?? null; + + const results = await euaService.search(req.query, search, query); + return reply.send(results); + } }); - res.status(StatusCodes.OK).json(results); -}; - -// Delete a User Agreement -export const deleteEua = async (req, res) => { - // The eua is placed into this parameter by the middleware - const eua = req.euaParam; - - const results = await euaService.delete(eua); - - // Audit eua delete - await auditService.audit( - 'eua deleted', - 'eua', - 'delete', - req, - eua.auditCopy() - ); - - res.status(StatusCodes.OK).json(results); -}; - -// EUA middleware - stores user corresponding to id in 'euaParam' -export const euaById = async (req, res, next, id) => { - const eua = await euaService.read(id); - if (null == eua) { - return next(new Error(`Failed to load User Agreement ${id}`)); - } - req.euaParam = eua; - return next(); -}; - -/** - * Check the state of the EUA - */ -export const requiresEua = async (req) => { - const result = await euaService.getCurrentEua(); - - // Compare the current eua to the user's acceptance state - if ( - null == result || - null == result.published || - (req.user.acceptedEua && req.user.acceptedEua >= result.published) - ) { - // if the user's acceptance is valid, then proceed - return Promise.resolve(); + fastify.route({ + method: 'GET', + url: '/eua', + schema: { + description: 'Retrieve current system EUA', + tags: ['EUA'] + }, + preValidation: requireLogin, + handler: async function (req, reply) { + const result = await euaService.getCurrentEua(); + return reply.send(result); + } + }); + + fastify.route({ + method: 'POST', + url: '/eua', + schema: { + description: 'Create EUA', + tags: ['EUA'] + }, + preValidation: requireAdminAccess, + handler: async function (req, reply) { + const result = await euaService.create(req.body); + + // Audit eua create + await auditService.audit( + 'eua create', + 'eua', + 'create', + req, + result.auditCopy() + ); + + return reply.send(result); + } + }); + + fastify.route({ + method: 'POST', + url: '/eua/accept', + schema: { + description: 'Accept EUA for current user', + tags: ['EUA'] + }, + preValidation: requireLogin, + handler: async function (req, reply) { + const user = await euaService.acceptEua(req.user); + + // Audit accepted eua + auditService.audit('eua accepted', 'eua', 'accepted', req, {}).then(); + + return reply.send(user.fullCopy()); + } + }); + + fastify.route({ + method: 'GET', + url: '/eua/:id', + schema: { + description: 'Retrieve EUA details', + tags: ['EUA'] + }, + preValidation: requireAdminAccess, + preHandler: loadEuaById, + handler: function (req, reply) { + return reply.send(req.euaParam); + } + }); + + fastify.route({ + method: 'POST', + url: '/eua/:id', + schema: { + description: 'Update EUA details', + tags: ['Eua'] + }, + preValidation: requireAdminAccess, + preHandler: loadEuaById, + handler: async function (req, reply) { + // A copy of the original eua for auditing purposes + const originalEua = req.euaParam.auditCopy(); + + const result = await euaService.update(req.euaParam, req.body); + + // Audit user update + await auditService.audit( + 'end user agreement updated', + 'eua', + 'update', + req, + { + before: originalEua, + after: result.auditCopy() + } + ); + + return reply.send(result); + } + }); + + fastify.route({ + method: 'DELETE', + url: '/eua/:id', + schema: { + description: '', + tags: ['EUA'] + }, + preValidation: requireAdminAccess, + preHandler: loadEuaById, + handler: async function (req, reply) { + // The eua is placed into this parameter by the middleware + const eua = req.euaParam; + + const result = await euaService.delete(eua); + + // Audit eua delete + await auditService.audit( + 'eua deleted', + 'eua', + 'delete', + req, + eua.auditCopy() + ); + + return reply.send(result); + } + }); + + fastify.route({ + method: 'POST', + url: '/eua/:id/publish', + schema: { + description: '', + tags: ['EUA'] + }, + preValidation: requireAdminAccess, + preHandler: loadEuaById, + handler: async function (req, reply) { + // The eua is placed into this parameter by the middleware + const eua = req.euaParam; + + const result = await euaService.publishEua(eua); + + // Audit eua create + await auditService.audit( + 'eua published', + 'eua', + 'published', + req, + result.auditCopy() + ); + + return reply.send(result); + } + }); +} + +async function loadEuaById(req: FastifyRequest) { + const id = req.params['id']; + req.euaParam = await euaService.read(id); + if (!req.euaParam) { + throw new Error(`Failed to load User Agreement ${id}`); } - return Promise.reject( - new ForbiddenError('User must accept end-user agreement.') - ); -}; +} diff --git a/src/app/core/user/eua/eua.middleware.ts b/src/app/core/user/eua/eua.middleware.ts new file mode 100644 index 00000000..e05d2241 --- /dev/null +++ b/src/app/core/user/eua/eua.middleware.ts @@ -0,0 +1,22 @@ +import { FastifyReply, FastifyRequest } from 'fastify'; + +import euaService from './eua.service'; +import { ForbiddenError } from '../../../common/errors'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function requireEua(req: FastifyRequest, rep: FastifyReply) { + const result = await euaService.getCurrentEua(); + + // Compare the current eua to the user's acceptance state + if ( + null == result?.published || + (req.user.acceptedEua && req.user.acceptedEua >= result.published) + ) { + // if the user's acceptance is valid, then proceed + return Promise.resolve(); + } + // return Promise.resolve(); + return Promise.reject( + new ForbiddenError('User must accept end-user agreement.') + ); +} diff --git a/src/app/core/user/eua/eua.routes.ts b/src/app/core/user/eua/eua.routes.ts deleted file mode 100644 index 644e0d1c..00000000 --- a/src/app/core/user/eua/eua.routes.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Router } from 'express'; - -import * as euas from './eua.controller'; -import { hasAdminAccess, hasLogin } from '../user-auth.middleware'; - -/** - * End User Agreement Routes - */ - -const router = Router(); - -router.route('/euas').post(hasAdminAccess, euas.searchEuas); - -router - .route('/eua') - .get(hasLogin, euas.getCurrentEua) - .post(hasAdminAccess, euas.createEua); - -router.route('/eua/accept').post(hasLogin, euas.acceptEua); - -router - .route('/eua/:euaId') - .get(hasAdminAccess, euas.read) - .post(hasAdminAccess, euas.updateEua) - .delete(hasAdminAccess, euas.deleteEua); - -router.route('/eua/:euaId/publish').post(hasAdminAccess, euas.publishEua); - -// Finish by binding the user middleware -router.param('euaId', euas.euaById); - -export = router; diff --git a/src/app/core/user/inactive/inactive-user.job.ts b/src/app/core/user/inactive/inactive-user.job.ts index 8395b4f1..f73ecea6 100644 --- a/src/app/core/user/inactive/inactive-user.job.ts +++ b/src/app/core/user/inactive/inactive-user.job.ts @@ -20,7 +20,6 @@ export default class InactiveUsersJobService implements JobService { try { const mailOptions = await emailService.generateMailOptions( user, - null, emailConfig, { daysAgo: numOfDays diff --git a/src/app/core/user/user-auth.middleware.ts b/src/app/core/user/user-auth.middleware.ts deleted file mode 100644 index 177793a9..00000000 --- a/src/app/core/user/user-auth.middleware.ts +++ /dev/null @@ -1,176 +0,0 @@ -import _ from 'lodash'; - -import userAuthService from './auth/user-authentication.service'; -import userAuthorizationService from './auth/user-authorization.service'; -import { requiresEua } from './eua/eua.controller'; -import { config } from '../../../dependencies'; -import { ForbiddenError, UnauthorizedError } from '../../common/errors'; -import { has, hasAll, requiresAny } from '../../common/express/auth-middleware'; - -/** - * Checks that the user is logged in - * 1. The user is logged in - */ -export const hasLogin = (req, res, next) => { - has(requiresLogin)(req, res, next); -}; - -/** - * Checks that the user has base access - * 1. The user is logged in - * 2. The user has accepted the EUA if applicable - * 3. The user has the 'user' role - */ -export const hasAccess = (req, res, next) => { - hasAll( - requiresLogin, - requiresOrganizationLevels, - requiresAny([requiresUserRole, requiresMachineRole]), - requiresExternalRoles, - requiresEua - )(req, res, next); -}; - -/** - * Checks that the user has editor access - * 1. The user has met the base access requirements - * 2. The user has the 'editor' role - */ -export const hasEditorAccess = (req, res, next) => { - hasAll( - requiresLogin, - requiresOrganizationLevels, - requiresAny([requiresUserRole, requiresMachineRole]), - requiresExternalRoles, - requiresAny([requiresAdminRole, requiresEditorRole]), - requiresEua - )(req, res, next); -}; - -/** - * Checks that the user has auditor access - * 1. The user has met the base access requirements - * 2. The user has the 'auditor' role - */ -export const hasAuditorAccess = (req, res, next) => { - hasAll( - requiresLogin, - requiresOrganizationLevels, - requiresUserRole, - requiresExternalRoles, - requiresAuditorRole, - requiresEua - )(req, res, next); -}; - -/** - * Checks that the user has admin access - * 1. The user has met the base access requirements - * 2. The user has the 'admin' role - */ -export const hasAdminAccess = (req, res, next) => { - hasAll(requiresLogin, requiresAdminRole)(req, res, next); -}; - -/** - * Require an authenticated user - */ -export const requiresLogin = (req, res, next) => { - if (req.isAuthenticated()) { - return Promise.resolve(); - } - - // Only try to auto login if it's explicitly set in the config - if (config.get('auth.autoLogin')) { - return userAuthService.authenticateAndLogin(req, res, next); - } - // Otherwise don't - return Promise.reject(new UnauthorizedError('User is not logged in')); -}; - -/** - * Require the passed roles - */ -export const requiresRoles = ( - roles: string[], - errorMessage = 'User is missing required roles' -) => { - return (req) => { - if (userAuthorizationService.hasRoles(req.user, roles)) { - return Promise.resolve(); - } - return Promise.reject(new ForbiddenError(errorMessage)); - }; -}; - -//Detects if the user has the user role -export const requiresUserRole = (req) => { - return requiresRoles(['user'], 'User account is inactive')(req); -}; - -//Detects if the user has the editor role -export const requiresEditorRole = (req) => { - return requiresRoles(['editor'])(req); -}; - -//Detects if the user has the auditor role -export const requiresAuditorRole = (req) => { - return requiresRoles(['auditor'])(req); -}; - -// Detects if the user has admin role -export const requiresAdminRole = (req) => { - return requiresRoles(['admin'])(req); -}; - -//Detects if the user has the machine role -export const requiresMachineRole = (req) => { - return requiresRoles(['machine'])(req); -}; - -// Checks to see if all required external roles are accounted for -export const requiresExternalRoles = (req) => { - const requiredRoles = config.get('auth.requiredRoles'); - - // If there are required roles, check for them - if (req.user.bypassAccessCheck === false && requiredRoles.length > 0) { - // Get the user roles - const userRoles = _.isArray(req.user.externalRoles) - ? req.user.externalRoles - : []; - - // Reject if the user is missing required roles - if (_.difference(requiredRoles, userRoles).length > 0) { - return Promise.reject( - new ForbiddenError('User is missing required external roles') - ); - } - // Resolve if they had all the roles - return Promise.resolve(); - } - // Resolve if we don't need to check - return Promise.resolve(); -}; - -/** - * Checks whether user has defined organization level values if values are required - */ -export const requiresOrganizationLevels = (req) => { - const required = config.get('auth.orgLevelConfig.required'); - - if (!required) { - // Organization levels are not required, proceed - return Promise.resolve(); - } - - if (userAuthorizationService.hasRoles(req.user, ['admin'])) { - // Admins can bypass this requirement - return Promise.resolve(); - } - - return !_.isEmpty(req.user.organizationLevels) - ? Promise.resolve() - : Promise.reject( - new ForbiddenError('User must select organization levels.') - ); -}; diff --git a/src/app/core/user/user-email.service.ts b/src/app/core/user/user-email.service.ts index 113fca85..1267cb43 100644 --- a/src/app/core/user/user-email.service.ts +++ b/src/app/core/user/user-email.service.ts @@ -16,7 +16,6 @@ class UserEmailService { try { const mailOptions = await emailService.generateMailOptions( user, - req, config.get('coreEmails.approvedUserEmail'), {}, {}, @@ -39,7 +38,6 @@ class UserEmailService { try { const mailOptions = await emailService.generateMailOptions( user, - req, config.get('coreEmails.userSignupAlert') ); await emailService.sendMail(mailOptions); @@ -61,7 +59,6 @@ class UserEmailService { try { const mailOptions = await emailService.generateMailOptions( user, - req, config.get('coreEmails.welcomeNoAccess'), {}, {}, @@ -95,7 +92,6 @@ class UserEmailService { try { const mailOptions = await emailService.generateMailOptions( user, - req, config.get('coreEmails.welcomeWithAccess'), {}, {}, diff --git a/src/app/core/user/user.components.yml b/src/app/core/user/user.components.yml deleted file mode 100644 index 5f80c7cb..00000000 --- a/src/app/core/user/user.components.yml +++ /dev/null @@ -1,72 +0,0 @@ -components: - schemas: - User: - type: object - required: - - name - - organization - - email - - username - - provider - properties: - phone: - type: string - canProxy: - type: boolean - default: false - externalGroups: - type: array - items: - type: string - externalRoles: - type: array - items: - type: string - bypassAccessCheck: - type: boolean - default: false - messsagesAcknowledged: - type: integer - minimum: 0 - acceptedEua: - type: boolean - default: null - lastLogin: - type: integer - lastLoginWithAccess: - type: integer - newFeatureDismissed: - type: boolean - default: null - _id: - type: string - name: - type: string - organization: - type: string - email: - type: string - format: email - username: - type: string - created: - type: integer - updated: - type: integer - alertsViewed: - type: integer - teams: - type: array - items: - $ref: "#/components/schemas/TeamRole" - roles: - type: object - provider: - type: string - resetPasswordExpires: - type: boolean - default: null - id: - type: string - preferences: - type: object diff --git a/src/app/core/user/user.controller.spec.ts b/src/app/core/user/user.controller.spec.ts deleted file mode 100644 index f5e191f9..00000000 --- a/src/app/core/user/user.controller.spec.ts +++ /dev/null @@ -1,338 +0,0 @@ -import assert from 'node:assert/strict'; - -import { assert as sinonAssert, createSandbox } from 'sinon'; - -import userAuthorizationService from './auth/user-authorization.service'; -import * as userController from './user.controller'; -import { User } from './user.model'; -import userService from './user.service'; -import { auditService, config } from '../../../dependencies'; -import { getResponseSpy } from '../../../spec/helpers'; -import { - BadRequestError, - ForbiddenError, - UnauthorizedError -} from '../../common/errors'; - -/** - * Unit tests - */ -describe('User Profile Controller:', () => { - let res; - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - res = getResponseSpy(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe('getCurrentUser', () => { - it('user is logged in (user attached to request)', async () => { - const req = { - body: {}, - user: new User() - }; - - sandbox.stub(userAuthorizationService, 'updateRoles').resolves(); - - await userController.getCurrentUser(req, res); - - sinonAssert.calledOnce(userAuthorizationService.updateRoles); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - - it('user is not logged in (user not attached to request)', async () => { - const req = { - body: {} - }; - - sandbox.stub(userAuthorizationService, 'updateRoles').resolves(); - - await assert.rejects( - userController.getCurrentUser(req, res), - new UnauthorizedError('User is not logged in') - ); - - sinonAssert.notCalled(userAuthorizationService.updateRoles); - }); - }); - - describe('updateCurrentUser', () => { - it('user is logged in; success', async () => { - const user = new User(); - user.save = () => Promise.resolve(user); - const req = { - body: {}, - user: user, - login: (u, callback) => { - callback(); - } - }; - - sandbox.stub(userService, 'read').resolves(user); - sandbox.stub(auditService, 'audit').resolves(); - - await userController.updateCurrentUser(req, res); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledWithMatch(res.json, { id: user.id }); - - sinonAssert.calledWithMatch(auditService.audit, 'user updated'); - }); - - it('user is logged in; success w/ password change', async () => { - const user = new User(); - user.password = 'oldPassword'; - user.save = () => Promise.resolve(user); - const req = { - body: { - currentPassword: 'oldPassword', - password: 'newPassword' - }, - user: user, - login: (u, callback) => { - callback(); - } - }; - user.auditCopy(); - - sandbox.stub(userService, 'read').resolves(user); - sandbox.stub(auditService, 'audit').resolves(); - - await userController.updateCurrentUser(req, res); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledWithMatch(res.json, {}); - - sinonAssert.calledWithMatch(auditService.audit, 'user updated'); - }); - - it('user is logged in; changing password, currentPassword incorrect', async () => { - const user = new User(); - user.password = 'differentPassword'; - - const req = { - body: { - currentPassword: 'oldPassword', - password: 'newPassword' - }, - user: user - }; - - sandbox.stub(userService, 'read').resolves(user); - sandbox.stub(auditService, 'audit').resolves(); - - await assert.rejects( - userController.updateCurrentUser(req, res), - new BadRequestError('Current password invalid') - ); - - sinonAssert.calledWithMatch( - auditService.audit, - 'user update authentication failed' - ); - }); - - it('user is logged in; login returns error', async () => { - const user = new User(); - user.password = 'oldPassword'; - user.save = () => Promise.resolve(user); - const req = { - body: { - currentPassword: 'oldPassword', - password: 'newPassword' - }, - user: user, - login: (u, callback) => { - callback('error'); - } - }; - - sandbox.stub(userService, 'read').resolves(user); - sandbox.stub(auditService, 'audit').resolves(); - - await userController.updateCurrentUser(req, res); - - sinonAssert.calledWith(res.status, 400); - sinonAssert.calledWithMatch(res.json, 'error'); - - sinonAssert.calledWithMatch(auditService.audit, 'user updated'); - }); - - it('user is not logged in', async () => { - const req = { - body: {} - }; - - await assert.rejects( - userController.updateCurrentUser(req, res), - new BadRequestError('User is not logged in') - ); - }); - }); - - describe('updatePreferences', () => { - it('user is logged in (user attached to request)', async () => { - const req = { - body: {}, - user: new User() - }; - - sandbox.stub(userService, 'updatePreferences').resolves(); - - await userController.updatePreferences(req, res); - - sinonAssert.calledOnce(userService.updatePreferences); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('updateRequiredOrgs', () => { - it('user is logged in (user attached to request)', async () => { - const req = { - body: {}, - user: new User() - }; - - sandbox.stub(userService, 'updateRequiredOrgs').resolves(); - - await userController.updateRequiredOrgs(req, res); - - sinonAssert.calledOnce(userService.updateRequiredOrgs); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('getUserById', () => { - it('user is found', async () => { - const req = { - body: {}, - userParam: new User() - }; - - await userController.getUserById(req, res); - - sinonAssert.calledWith(res.status, 200); - sinonAssert.called(res.json); - }); - }); - - describe('searchUsers', () => { - let req; - beforeEach(() => { - req = { - body: {}, - user: new User() - }; - }); - - it('search returns successfully', async () => { - sandbox.stub(userService, 'searchUsers').resolves({ - elements: [] - }); - - await userController.searchUsers(req, res); - - sinonAssert.calledOnce(userService.searchUsers); - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - }); - }); - - describe('matchUsers', () => { - let req; - beforeEach(() => { - req = { - body: {}, - user: new User() - }; - }); - - it('search returns successfully', async () => { - sandbox.stub(userService, 'searchUsers').resolves({ - elements: [] - }); - - await userController.matchUsers(req, res); - - sinonAssert.calledOnce(userService.searchUsers); - sinonAssert.calledWith(res.status, 200); - sinonAssert.calledOnce(res.json); - }); - }); - - describe('canEditProfile', () => { - it('local auth and undef bypass should be able to edit', () => { - const _user = new User(); - const result = userController.canEditProfile('local', _user); - assert.equal(result, true); - }); - - it('local auth and no bypass should be able to edit', () => { - const _user = new User({ bypassAccessCheck: false }); - const result = userController.canEditProfile('local', _user); - assert.equal(result, true); - }); - - it('local auth and bypass should be able to edit', () => { - const _user = new User({ bypassAccessCheck: true }); - const result = userController.canEditProfile('local', _user); - assert.equal(result, true); - }); - - it('proxy-pki auth and undef bypass should not be able to edit', () => { - const _user = new User({}); - const result = userController.canEditProfile('proxy-pki', _user); - assert.equal(result, false); - }); - - it('proxy-pki auth and no bypass should not be able to edit', () => { - const _user = new User({ bypassAccessCheck: false }); - const result = userController.canEditProfile('proxy-pki', _user); - assert.equal(result, false); - }); - - it('proxy-pki auth and bypass should be able to edit', () => { - const _user = new User({ bypassAccessCheck: true }); - const result = userController.canEditProfile('proxy-pki', _user); - assert.equal(result, true); - }); - }); - - describe('hasEdit', () => { - it('user has edit', async () => { - const configGetStub = sandbox.stub(config, 'get'); - configGetStub.withArgs('auth.strategy').returns('proxy-pki'); - const req = { - body: {}, - user: { bypassAccessCheck: true } - }; - - await assert.doesNotReject(userController.hasEdit(req)); - }); - - it('user does not have edit', async () => { - const configGetStub = sandbox.stub(config, 'get'); - configGetStub.withArgs('auth.strategy').returns('proxy-pki'); - const req = { - body: {}, - user: { bypassAccessCheck: false } - }; - - await assert.rejects( - userController.hasEdit(req), - new ForbiddenError('User not authorized to edit their profile') - ); - }); - }); -}); diff --git a/src/app/core/user/user.controller.ts b/src/app/core/user/user.controller.ts index 013e7804..64c89272 100644 --- a/src/app/core/user/user.controller.ts +++ b/src/app/core/user/user.controller.ts @@ -1,174 +1,204 @@ -import { StatusCodes } from 'http-status-codes'; +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; +import { FastifyInstance, FastifyRequest } from 'fastify'; import _ from 'lodash'; +import { requireAccess, requireLogin } from './auth/auth.middleware'; import userAuthorizationService from './auth/user-authorization.service'; -import { UserDocument } from './user.model'; import userService from './user.service'; -import { auditService, config } from '../../../dependencies'; -import { - BadRequestError, - ForbiddenError, - UnauthorizedError -} from '../../common/errors'; +import { auditService } from '../../../dependencies'; +import { BadRequestError } from '../../common/errors'; +import { PagingQueryStringSchema, SearchBodySchema } from '../core.schemas'; import teamService from '../teams/teams.service'; -/** - * Standard User Operations - */ +export default function (_fastify: FastifyInstance) { + const fastify = _fastify.withTypeProvider(); + fastify.route({ + method: 'GET', + url: '/user/me', + schema: { + tags: ['User'], + description: 'Returns details about the authenticated user.' + }, + preValidation: requireLogin, + handler: async function (req, reply) { + const user = req.user.fullCopy(); -// Get Current User -export const getCurrentUser = async (req, res) => { - // The user that is a parameter of the request is stored in 'userParam' - const user = req.user; + userAuthorizationService.updateRoles(user); - if (null == user) { - throw new UnauthorizedError('User is not logged in'); - } - - const userCopy = user.fullCopy(); - - userAuthorizationService.updateRoles(userCopy); - - await teamService.updateTeams(userCopy); - - res.status(StatusCodes.OK).json(userCopy); -}; + await teamService.updateTeams(user); -// Update Current User -export const updateCurrentUser = async (req, res) => { - // Make sure the user is logged in - if (null == req.user) { - throw new BadRequestError('User is not logged in'); - } - - // Get the full user (including the password) - const user = await userService.read(req.user._id); - const originalUser = user.auditCopy(); - - // Copy over the new user properties - user.name = req.body.name; - user.organization = req.body.organization; - user.email = req.body.email; - user.phone = req.body.phone; - user.username = req.body.username; - user.messagesAcknowledged = req.body.messagesAcknowledged; - user.alertsViewed = req.body.alertsViewed; - user.newFeatureDismissed = req.body.newFeatureDismissed; - - // If they are changing the password, verify the current password - if (_.isString(req.body.password) && !_.isEmpty(req.body.password)) { - if (!user.authenticate(req.body.currentPassword)) { - // Audit failed authentication - auditService.audit( - 'user update authentication failed', - 'user', - 'update authentication failed', - req, - {} - ); - - throw new BadRequestError('Current password invalid'); + return reply.send(user); } + }); - // We passed the auth check and we're updating the password - user.password = req.body.password; - } + fastify.route({ + method: 'POST', + url: '/user/me', + schema: { + tags: ['User'], + description: 'Updates details about the authenticated user.', + body: { + type: 'object', + properties: { + name: { type: 'string' }, + organization: { type: 'string' }, + email: { type: 'string' }, + username: { type: 'string' }, + password: { type: 'string' }, + currentPassword: { type: 'string' } + }, + required: ['name', 'organization', 'email', 'username'] + } + }, + preValidation: requireLogin, + handler: async function (req, reply) { + // Get the full user (including the password) + const user = await userService.read(req.user._id); + const originalUser = user.auditCopy(); + + // Copy over the new user properties + user.name = req.body.name; + user.organization = req.body.organization; + user.email = req.body.email; + user.username = req.body.username; + + // If they are changing the password, verify the current password + if (_.isString(req.body.password) && !_.isEmpty(req.body.password)) { + if (!user.authenticate(req.body.currentPassword)) { + // Audit failed authentication + auditService + .audit( + 'user update authentication failed', + 'user', + 'update authentication failed', + req, + {} + ) + .then(); + + throw new BadRequestError('Current password invalid'); + } + + // We passed the auth check and we're updating the password + user.password = req.body.password; + } + + // Save the user + await user.save(); + + // Remove the password/salt + delete user.password; + delete user.salt; + + // Audit user update + auditService + .audit('user updated', 'user', 'update', req, { + before: originalUser, + after: user.auditCopy() + }) + .then(); + + return reply.send(user.fullCopy()); + } + }); - // Save the user - await user.save(); + fastify.route({ + method: 'GET', + url: '/user/:id', + schema: { + tags: ['User'], + description: '' + }, + preValidation: requireAccess, + preHandler: loadUserById, + handler: function (req, reply) { + return reply.send(req.userParam.filteredCopy()); + } + }); - // Remove the password/salt - delete user.password; - delete user.salt; + fastify.route({ + method: 'POST', + url: '/user-preference', + schema: { + tags: ['User'], + description: '', + body: { + type: 'object' + } + }, + preValidation: requireLogin, + handler: async function (req, reply) { + await userService.updatePreferences(req.user, req.body); + return reply.send({}); + } + }); - // Audit user update - auditService.audit('user updated', 'user', 'update', req, { - before: originalUser, - after: user.auditCopy() + fastify.route({ + method: 'POST', + url: '/users', + schema: { + tags: ['User'], + description: 'Returns users matching search criteria', + body: SearchBodySchema, + querystring: PagingQueryStringSchema + }, + preValidation: requireAccess, + handler: async function (req, reply) { + // Handle the query/search + const query = req.body.q; + const search = req.body.s; + + const results = await userService.searchUsers(req.query, query, search); + const mappedResults = { + pageSize: results.pageSize, + pageNumber: results.pageNumber, + totalSize: results.totalSize, + totalPages: results.totalPages, + elements: results.elements.map((user) => user.filteredCopy()) + }; + return reply.send(mappedResults); + } }); - // Log in with the new info - req.login(user, (error) => { - if (error) { - return res.status(StatusCodes.BAD_REQUEST).json(error); + fastify.route({ + method: 'POST', + url: '/user/match', + schema: { + tags: ['User'], + description: '', + body: SearchBodySchema, + querystring: PagingQueryStringSchema + }, + preValidation: requireAccess, + preHandler: loadUserById, + handler: async function (req, reply) { + // Handle the query/search/page + const query = req.body.q; + const search = req.body.s; + + const results = await userService.searchUsers(req.query, query, search, [ + 'name', + 'username', + 'email' + ]); + const mappedResults = { + pageSize: results.pageSize, + pageNumber: results.pageNumber, + totalSize: results.totalSize, + totalPages: results.totalPages, + elements: results.elements.map((user) => user.filteredCopy()) + }; + return reply.send(mappedResults); } - res.status(StatusCodes.OK).json(user.fullCopy()); }); -}; - -export const updatePreferences = async (req, res) => { - await userService.updatePreferences(req.user, req.body); - res.status(StatusCodes.OK).json({}); -}; - -export const updateRequiredOrgs = async (req, res) => { - await userService.updateRequiredOrgs(req.user, req.body); - res.status(StatusCodes.OK).json({}); -}; - -// Get a filtered version of a user by id -export const getUserById = (req, res) => { - res.status(StatusCodes.OK).json(req.userParam.filteredCopy()); -}; - -// Search for users (return filtered version of user) -export const searchUsers = async (req, res) => { - // Handle the query/search - const query = req.body.q; - const search = req.body.s; - - const results = await userService.searchUsers(req.query, query, search); - const mappedResults = { - pageSize: results.pageSize, - pageNumber: results.pageNumber, - totalSize: results.totalSize, - totalPages: results.totalPages, - elements: results.elements.map((user) => user.filteredCopy()) - }; - res.status(StatusCodes.OK).json(mappedResults); -}; - -// Match users given a search fragment -export const matchUsers = async (req, res) => { - // Handle the query/search/page - const query = req.body.q; - const search = req.body.s; - - const results = await userService.searchUsers(req.query, query, search, [ - 'name', - 'username', - 'email' - ]); - const mappedResults = { - pageSize: results.pageSize, - pageNumber: results.pageNumber, - totalSize: results.totalSize, - totalPages: results.totalPages, - elements: results.elements.map((user) => user.filteredCopy()) - }; - res.status(StatusCodes.OK).json(mappedResults); -}; - -export const canEditProfile = (authStrategy: string, user: UserDocument) => { - return authStrategy !== 'proxy-pki' || user.bypassAccessCheck === true; -}; - -// Are allowed to edit user profile info -export const hasEdit = (req) => { - if (canEditProfile(config.get('auth.strategy'), req.user)) { - return Promise.resolve(); - } - return Promise.reject( - new ForbiddenError('User not authorized to edit their profile') - ); -}; +} // User middleware - stores user corresponding to id in 'userParam' -export const userById = async (req, res, next, id) => { - const user = await userService.read(id); - if (!user) { - return next(new Error(`Failed to load User ${id}`)); +export async function loadUserById(req: FastifyRequest) { + const id = req.params['id']; + req.userParam = await userService.read(id); + + if (!req.userParam) { + throw new Error(`Failed to load User ${id}`); } - req.userParam = user; - return next(); -}; +} diff --git a/src/app/core/user/user.routes.ts b/src/app/core/user/user.routes.ts deleted file mode 100644 index 474cef16..00000000 --- a/src/app/core/user/user.routes.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Router } from 'express'; - -import { hasAccess, hasLogin } from './user-auth.middleware'; -import * as users from './user.controller'; - -const router = Router(); - -/** - * @swagger - * /user/me: - * get: - * tags: [User] - * description: Returns information about the authenticated user. - * responses: - * '200': - * description: The authenticated user's profile - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - */ -// Self-service user routes -router - .route('/user/me') - .get(hasLogin, users.getCurrentUser) - .post(hasLogin, users.updateCurrentUser); - -// User getting another user's info -router.route('/user/:userId').get(hasAccess, users.getUserById); - -router.route('/user-preference').post(hasLogin, users.updatePreferences); - -router.route('/user/required-org').post(hasLogin, users.updateRequiredOrgs); - -// User searching for other users -router.route('/users').post(hasAccess, users.searchUsers); - -// User match-based search for other users (this searches based on a fragment) -router.route('/users/match').post(hasAccess, users.matchUsers); - -// Finish by binding the user middleware -router.param('userId', users.userById); - -export = router; diff --git a/src/app/site/test/test.controller.ts b/src/app/site/test/test.controller.ts new file mode 100644 index 00000000..0a22d5ca --- /dev/null +++ b/src/app/site/test/test.controller.ts @@ -0,0 +1,11 @@ +import { FastifyInstance } from 'fastify'; + +export default function (fastify: FastifyInstance) { + fastify.route({ + method: 'GET', + url: '/test', + handler: function (req, reply) { + return reply.send({ message: 'hello world' }); + } + }); +} diff --git a/src/app/site/test/test.routes.ts b/src/app/site/test/test.routes.ts deleted file mode 100644 index f07ce9c3..00000000 --- a/src/app/site/test/test.routes.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Router } from 'express'; -import { StatusCodes } from 'http-status-codes'; - -const router = Router(); - -router.route('/test').get((req, res) => { - res.status(StatusCodes.OK).json({ message: 'hello world' }); -}); - -export = router; diff --git a/src/lib/express.spec.ts b/src/lib/express.spec.ts deleted file mode 100644 index ef1c5cdb..00000000 --- a/src/lib/express.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import path from 'path'; - -import { globSync } from 'glob'; -import { OpenAPI } from 'openapi-types'; -import swaggerJsDoc from 'swagger-jsdoc'; -import SwaggerParser from 'swagger-parser'; - -import { config } from '../dependencies'; - -/** - * Unit tests - */ -describe('Init Swagger API:', () => { - it('Generated Swagger API should be valid', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const swaggerOptions: any = { - swaggerDefinition: { - openapi: '3.0.2', - info: { - title: config.get('app.title'), - description: config.get('app.description'), - version: 'test' - }, - servers: [ - { - url: 'https://api.example.com/api' - } - ], - components: {} - }, - apis: [ - ...globSync(config.get('assets.docs')).map((doc: string) => - path.posix.resolve(doc) - ), - ...globSync(config.get('assets.routes')).map( - (route: string) => path.posix.resolve(route) - ), - ...globSync(config.get('assets.models')).map( - (model: string) => path.posix.resolve(model) - ) - ] - }; - - if (config.get('auth.strategy') === 'local') { - swaggerOptions.swaggerDefinition.components.securitySchemes = { - basicAuth: { - type: 'http', - scheme: 'basic' - } - }; - } - - const swaggerSpec = swaggerJsDoc(swaggerOptions) as OpenAPI.Document; - // @ts-expect-error tsc:watch reports type error, but code runs properly. SwaggerParser is defined as both a class and namespace. - await SwaggerParser.validate(swaggerSpec); - }); -}); diff --git a/src/lib/express.ts b/src/lib/express.ts deleted file mode 100644 index 6669cad8..00000000 --- a/src/lib/express.ts +++ /dev/null @@ -1,318 +0,0 @@ -import path from 'path'; - -import compress from 'compression'; -import config from 'config'; -import MongoStore from 'connect-mongo'; -import cors from 'cors'; -import express, { Express, Request, Response } from 'express'; -import actuator from 'express-actuator'; -import session from 'express-session'; -import { glob, globSync } from 'glob'; -import helmet from 'helmet'; -import { StatusCodes } from 'http-status-codes'; -import _ from 'lodash'; -import methodOverride from 'method-override'; -import { Mongoose } from 'mongoose'; -import morgan from 'morgan'; -import { OpenAPI } from 'openapi-types'; -import passport from 'passport'; -import swaggerJsDoc from 'swagger-jsdoc'; -import swaggerUi from 'swagger-ui-express'; - -import { logger } from './logger'; -import { - defaultErrorHandler, - jsonSchemaValidationErrorHandler, - mongooseValidationErrorHandler -} from '../app/common/express/error-handlers'; - -// Patches express to support async/await. Should be called immediately after express. -// Must still use require vs. import -require('express-async-errors'); - -const baseApiPath = '/api'; - -/** - * Initialize application middleware - */ -function initMiddleware(app: Express) { - // Showing stack errors - app.set('showStackError', true); - - // Should be placed before express.static - app.use( - compress({ - filter: function (req: Request, res: Response) { - if (req.headers['x-no-compression']) { - // don't compress responses with this request header - return false; - } - - // fallback to standard filter function - return compress.filter(req, res); - }, - level: 6 - }) - ); - - // Environment dependent middleware - if (config.get('mode') === 'development') { - // Disable views cache - app.set('view cache', false); - } else if (config.get('mode') === 'production') { - app.locals.cache = 'memory'; - } - - // Optionally turn on express logging - if (config.get('expressLogging')) { - app.use(morgan('dev')); - } - - // Request body parsing middleware should be above methodOverride - app.use( - express.urlencoded({ - extended: true - }) - ); - app.use(express.json()); - app.use(methodOverride()); -} - -/** - * Configure view engine - */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function initViewEngine(app: Express) { - // Not using server rendering for views -} - -/** - * Configure Express session - */ -function initSession(app: Express, db: Mongoose) { - // Express MongoDB session storage - app.use( - session({ - saveUninitialized: true, - resave: true, - secret: config.get('auth.sessionSecret'), - cookie: config.get('auth.sessionCookie'), - store: MongoStore.create({ - client: db.connection.getClient(), - collectionName: config.get('auth.sessionCollection') - } as unknown) - }) - ); -} - -/** - * Configure passport - */ -async function initPassport(app: Express) { - app.use(passport.initialize()); - app.use(passport.session()); - - await import('./passport').then((p) => p.init()); -} - -/** - * Invoke modules server configuration - */ -async function initModulesConfiguration(app: Express, db: Mongoose) { - const configPaths = await glob(config.get('assets.config')); - - const moduleConfigs = await Promise.all( - configPaths.map((configPath) => import(path.posix.resolve(configPath))) - ); - moduleConfigs.forEach((moduleConfig) => { - moduleConfig.default(app, db); - }); -} - -/** - * Configure Helmet headers configuration - */ -function initHelmetHeaders(app: Express) { - // Use helmet to secure Express headers - app.use(helmet.frameguard()); - app.use(helmet.xssFilter()); - app.use(helmet.noSniff()); - app.use(helmet.ieNoOpen()); - app.disable('x-powered-by'); -} - -function initCORS(app: Express) { - if (config.get('cors.enabled') !== true) { - return; - } - app.use(cors({ ...config.get>('cors.options') })); -} - -/** - * Configure the modules server routes - */ -async function initModulesServerRoutes(app: Express) { - // Init the global route prefix - const router = express.Router(); - - const routePaths = await glob(config.get('assets.routes')); - const routes = await Promise.all( - routePaths.map((routePath: string) => import(path.posix.resolve(routePath))) - ); - routes.forEach((route) => { - router.use(route.default); - }); - - // Host everything behind a single endpoint - app.use(baseApiPath, router); -} - -/** - * Configure final error handlers - */ -function initErrorRoutes(app: Express) { - app.use(jsonSchemaValidationErrorHandler); - app.use(mongooseValidationErrorHandler); - app.use(defaultErrorHandler); - - // Assume 404 since no middleware responded - app.use((req, res) => { - // Send 404 with error message - res.status(StatusCodes.NOT_FOUND).json({ - status: StatusCodes.NOT_FOUND, - type: 'not-found', - message: 'The resource was not found' - }); - }); -} - -function initActuator(app: Express) { - // actuator must be enabled explicitly in the config - if (config.get('actuator.enabled') !== true) { - return; - } - logger.info('Configuring actuator endpoints'); - app.use(actuator(config.get>('actuator.options'))); -} - -function initSwaggerAPI(app: Express) { - // apiDocs must be enabled explicitly in the config - if (config.get('apiDocs.enabled') !== true) { - return; - } - - logger.info('Configuring api docs'); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const swaggerOptions: any = { - swaggerDefinition: { - openapi: '3.0.2', - info: { - title: config.get('app.title'), - description: config.get('app.description'), - contact: { - email: config.get('mailer.from') - } - }, - servers: [ - { - url: baseApiPath - } - ], - components: {} - }, - apis: [ - ...globSync(config.get('assets.docs')).map((doc: string) => - path.posix.resolve(doc) - ), - ...globSync(config.get('assets.routes')).map((route: string) => - path.posix.resolve(route) - ), - ...globSync(config.get('assets.models')).map((model: string) => - path.posix.resolve(model) - ) - ] - }; - - if (config.get('auth.strategy') === 'local') { - swaggerOptions.swaggerDefinition.components.securitySchemes = { - basicAuth: { - type: 'http', - scheme: 'basic' - } - }; - } - - const swaggerSpec = swaggerJsDoc(swaggerOptions) as OpenAPI.Document; - - /* - * Some api calls are dependent on whether local or proxy-pki are used. - * If no strategy is defined, assume it is used in both. - */ - swaggerSpec.paths = _.pickBy(swaggerSpec.paths, (_path) => { - return ( - _path['strategy'] === undefined || - _path['strategy'] === config.get('auth.strategy') - ); - }); - - const uiOptions = { - filter: true, - ...config.get>('apiDocs.uiOptions') - }; - - app.use( - config.get('apiDocs.path'), - swaggerUi.serve, - swaggerUi.setup(swaggerSpec, null, uiOptions) - ); - - app.get(config.get('apiDocs.jsonPath'), (req, res) => { - res.send(swaggerSpec); - }); -} - -/** - * Initialize the Express application - */ -export const init = async (db: Mongoose): Promise => { - // Initialize express app - logger.info('Initializing Express'); - - const app: Express = express(); - - // Initialize Express middleware - initMiddleware(app); - - // Initialize Express view engine - initViewEngine(app); - - // Initialize Express session - initSession(app, db); - - // Initialize passport auth - await initPassport(app); - - // Initialize Modules configuration - await initModulesConfiguration(app, db); - - // Initialize Helmet security headers - initHelmetHeaders(app); - - // Initialize CORS headers - initCORS(app); - - // Initialize modules server routes - await initModulesServerRoutes(app); - - // Initialize Swagger API - initSwaggerAPI(app); - - // Initialize Actuator routes - initActuator(app); - - // Initialize error routes - initErrorRoutes(app); - - return app; -}; diff --git a/src/lib/fastify.ts b/src/lib/fastify.ts new file mode 100644 index 00000000..b18a4c3b --- /dev/null +++ b/src/lib/fastify.ts @@ -0,0 +1,203 @@ +import path from 'path'; + +import fastifyCompress from '@fastify/compress'; +import { fastifyCookie } from '@fastify/cookie'; +import fastifyCors from '@fastify/cors'; +import fastifyFormbody from '@fastify/formbody'; +import fastifyHelmet from '@fastify/helmet'; +import { Authenticator } from '@fastify/passport'; +import fastifySession from '@fastify/session'; +import fastifySwagger from '@fastify/swagger'; +import fastifySwaggerUi from '@fastify/swagger-ui'; +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; +import config from 'config'; +import MongoStore from 'connect-mongo'; +import { fastify, FastifyInstance } from 'fastify'; +import { glob } from 'glob'; +import { Mongoose } from 'mongoose'; + +import { logger } from './logger'; +import pkg from '../../package.json'; + +const baseApiPath = '/api'; + +export async function init(db: Mongoose) { + const app = fastify({ + logger: config.get('fastifyLogging') + }).withTypeProvider(); + + // Configure compression + await app.register(fastifyCompress); + + // Configure parser for `application/x-www-form-urlencoded` content + await app.register(fastifyFormbody); + + // Configure authentication with session/passport + await initSession(app, db); + + // Configure Helmet security headers + app.register(fastifyHelmet, { + frameguard: true, + xssFilter: true, + noSniff: true, + ieNoOpen: true, + hidePoweredBy: true + }); + + // Configure CORS + if (config.get('cors.enabled') === true) { + app.register(fastifyCors, { + ...config.get>('cors.options') + }); + } + + initSwaggerAPI(app); + + initActuator(app); + + await initModulesServerRoutes(app); + + app.setErrorHandler((error, request) => { + logger.error({ + message: error.message, + stack: error.stack, + req: { method: request.method, url: request.url, host: request.host } + }); + throw error; + }); + + return app; +} + +async function initSession(app: FastifyInstance, db: Mongoose) { + // setup an Authenticator instance which uses @fastify/session + const fastifyPassport = new Authenticator(); + + app.register(fastifyCookie); + app.register(fastifySession, { + secret: config.get('auth.sessionSecret'), + cookie: { + secure: false + // maxAge: config.get('auth.sessionCookie') + }, + store: MongoStore.create({ + client: db.connection.getClient(), + collectionName: config.get('auth.sessionCollection') + } as unknown) + }); + + // initialize @fastify/passport and connect it to the secure-session storage. Note: both of these plugins are mandatory. + app.register(fastifyPassport.initialize()); + app.register(fastifyPassport.secureSession()); + + await import('./passport').then((p) => p.initFastify(fastifyPassport)); +} + +/** + * Configure the modules server routes + */ +async function initModulesServerRoutes(app: FastifyInstance) { + // Init the global route prefix + + const routePaths = await glob(config.get('assets.controllers')); + const routes = await Promise.all( + routePaths.map((routePath: string) => import(path.posix.resolve(routePath))) + ); + routes + .filter((route) => route.default) + .forEach((route) => { + app.register(route.default, { prefix: baseApiPath }); + }); +} + +function initSwaggerAPI(app: FastifyInstance) { + // apiDocs must be enabled explicitly in the config + if (config.get('apiDocs.enabled') !== true) { + return; + } + + app.log.info('Configuring api docs'); + + app.register(fastifySwagger, { + openapi: { + openapi: '3.0.2', + info: { + title: config.get('app.title'), + description: config.get('app.description'), + contact: { + email: config.get('mailer.from') + }, + version: pkg.version + }, + servers: [ + { + url: baseApiPath + } + ], + components: {} + } + }); + app.register(fastifySwaggerUi, { + routePrefix: '/api-docs', + uiConfig: { + filter: true, + ...config.get>('apiDocs.uiOptions') + } + }); +} + +function initActuator(app: FastifyInstance) { + // actuator must be enabled explicitly in the config + if (config.get('actuator.enabled') !== true) { + return; + } + app.log.info('Configuring actuator endpoints'); + + const basePath = config.get('actuator.options.basePath'); + app.register( + (instance) => { + instance.route({ + method: 'GET', + url: '/health', + schema: { + hide: true + }, + handler: function (req, reply) { + return reply.send({ status: 'UP' }); + } + }); + + instance.route({ + method: 'GET', + url: '/info', + schema: { + hide: true + }, + handler: function (req, reply) { + return reply.send({ + name: pkg.name, + description: pkg.description, + version: pkg.version + }); + } + }); + + instance.route({ + method: 'GET', + url: '/metrics', + schema: { + hide: true + }, + handler: function (req, reply) { + return reply.send({ + mem: process.memoryUsage(), + uptime: process.uptime() + }); + } + }); + }, + { + prefix: basePath + } + ); +} diff --git a/src/lib/passport.ts b/src/lib/passport.ts index 4fb97c2a..6dc0947c 100644 --- a/src/lib/passport.ts +++ b/src/lib/passport.ts @@ -1,11 +1,12 @@ import path from 'path'; +import { Authenticator } from '@fastify/passport'; import { globSync } from 'glob'; import passport from 'passport'; import { User } from '../app/core/user/user.model'; -export const init = async () => { +export const initSocketIO = async () => { // Serialize sessions passport.serializeUser((user, done) => { done(null, user['id']); @@ -36,3 +37,28 @@ export const init = async () => { }) ); }; + +export const initFastify = async (fastifyPassport: Authenticator) => { + // Serialize sessions + fastifyPassport.registerUserSerializer((user) => { + return user['id']; + }); + + // Deserialize sessions + fastifyPassport.registerUserDeserializer((id) => { + return User.findById(id, '-salt -password'); + }); + + // Initialize strategies + await Promise.all( + globSync([ + './src/lib/strategies/**/*.js', + './src/lib/strategies/**/*.ts' + ]).map(async (strategyPath) => { + const { default: strategy } = await import( + path.posix.resolve(strategyPath) + ); + fastifyPassport.use(strategy); + }) + ); +}; diff --git a/src/lib/socket.io.ts b/src/lib/socket.io.ts index 49caffbc..aeb9b68a 100644 --- a/src/lib/socket.io.ts +++ b/src/lib/socket.io.ts @@ -71,7 +71,6 @@ class SocketIo { allowEIO3: true // @FIXME: Set to true for client compatibility. Fix when UI is updated. }); - // io.use(expressToIO(cookieParser(config.get('auth.sessionSecret')))); io.engine.use( expressSession({ saveUninitialized: true, @@ -86,6 +85,7 @@ class SocketIo { ); io.engine.use(passport.initialize()); io.engine.use(passport.session()); + await import('./passport').then((p) => p.initSocketIO()); // Verify if user was found in session io.use((socket, next) => { diff --git a/src/lib/strategies/local.ts b/src/lib/strategies/local.ts index 065364b7..64a44d72 100644 --- a/src/lib/strategies/local.ts +++ b/src/lib/strategies/local.ts @@ -1,11 +1,10 @@ import { Strategy as LocalStrategy } from 'passport-local'; -import { BadRequestError, UnauthorizedError } from '../../app/common/errors'; import { User } from '../../app/core/user/user.model'; const verify = (username: string, password: string, done) => { if (!username) { - return done(null, false, new BadRequestError('No username provided')); + return done(null, false, { message: 'No username provided' }); } User.findOne({ username: username }) @@ -13,11 +12,7 @@ const verify = (username: string, password: string, done) => { .then((user) => { // The user wasn't found or the password was wrong if (!user || !user.authenticate(password)) { - return done( - null, - false, - new UnauthorizedError('Incorrect username or password') - ); + return done(null, false, { message: 'Incorrect username or password' }); } // Return the user diff --git a/src/lib/winston.ts b/src/lib/winston.ts index d89cea13..a7543b79 100644 --- a/src/lib/winston.ts +++ b/src/lib/winston.ts @@ -5,13 +5,25 @@ import config, { IConfig } from 'config'; import winston, { LoggerOptions } from 'winston'; import DailyRotateFile from 'winston-daily-rotate-file'; -const { combine, errors, json, splat, timestamp } = winston.format; +const { combine, errors, json, splat, timestamp, prettyPrint } = winston.format; function createLogger(loggerName: string) { const loggerConfig = config.get('logger').get(loggerName); + const prettyPrintEnabled = loggerConfig.get('prettyPrint'); + + const format = prettyPrintEnabled + ? combine( + timestamp(), + errors({ stack: true }), + splat(), + json(), + prettyPrint() + ) + : combine(timestamp(), errors({ stack: true }), splat(), json()); + const options = { - format: combine(timestamp(), errors({ stack: true }), splat(), json()), + format, silent: loggerConfig.get('silent'), defaultMeta: { hostname: os.hostname(), diff --git a/src/server.ts b/src/server.ts index 3fdf7712..97b58c6e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,7 +6,7 @@ import startupFn from './startup'; startupFn() .then((server) => { // Start the app - server.listen(config.get('port')); + server.listen({ port: config.get('port') }); logger.info( `${config.get('app.instanceName')} started on port ${config.get('port')}` ); diff --git a/src/spec/fastify.ts b/src/spec/fastify.ts new file mode 100644 index 00000000..17ac332a --- /dev/null +++ b/src/spec/fastify.ts @@ -0,0 +1,24 @@ +import { fastify, FastifyPluginCallback } from 'fastify'; + +import { IUser, User } from '../app/core/user/user.model'; + +type FastifyTestConfig = { + logger?: boolean | { level: string }; + user?: Partial; +}; +export const fastifyTest = ( + plugin: FastifyPluginCallback, + { logger, user }: FastifyTestConfig +) => { + const instance = fastify({ logger: logger ?? false }); + instance.decorateRequest('user', { + getter() { + return new User(user); + } + }); + instance.decorateRequest('isAuthenticated', function () { + return !!this.user; + }); + instance.register(plugin); + return instance; +}; diff --git a/src/spec/helpers.ts b/src/spec/helpers.ts deleted file mode 100644 index 74051e38..00000000 --- a/src/spec/helpers.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { spy, stub } from 'sinon'; - -export const getResponseSpy = () => { - const res = { - json: spy(), - end: spy(), - redirect: spy(), - status: stub() - }; - res.status.returns(res); - return res; -}; diff --git a/src/startup.ts b/src/startup.ts index 732b0876..1c1011ac 100644 --- a/src/startup.ts +++ b/src/startup.ts @@ -1,9 +1,7 @@ -import http from 'http'; - import { Mongoose } from 'mongoose'; import * as agenda from './lib/agenda'; -import * as express from './lib/express'; +import * as fastify from './lib/fastify'; import { logger } from './lib/logger'; import * as migrate_mongo from './lib/migrate-mongo'; import * as mongoose from './lib/mongoose'; @@ -21,15 +19,11 @@ export default async function () { // Init agenda.ts scheduler await agenda.init(); - // Initialize express - const app = await express.init(db.admin as Mongoose); - - // Create a new HTTP server - logger.info('Creating HTTP Server'); - const server = http.createServer(app); + logger.info('Creating Fastify Server'); + const app = await fastify.init(db.admin as Mongoose); // Initialize socket.io - await socketio.init(server, db.admin as Mongoose); + await socketio.init(app.server, db.admin as Mongoose); - return server; + return app; } diff --git a/src/type-extensions.d.ts b/src/type-extensions.d.ts new file mode 100644 index 00000000..72800023 --- /dev/null +++ b/src/type-extensions.d.ts @@ -0,0 +1,18 @@ +import { ExportConfigDocument } from './app/core/export/export-config.model'; +import { TeamDocument } from './app/core/teams/team.model'; +import { UserAgreementDocument } from './app/core/user/eua/eua.model'; +import { UserDocument } from './app/core/user/user.model'; + +declare module 'fastify' { + // @ts-expect-error sets PassportUser type to the IUser + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface PassportUser extends UserDocument {} + + interface FastifyRequest { + team: TeamDocument; + userParam: UserDocument; + euaParam: UserAgreementDocument; + exportConfig: ExportConfigDocument; + exportQuery: unknown; + } +}