From b2b9816b6bc3dff217dfddc1496d4ecb9772a6e9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 23 Dec 2024 09:49:01 -0800 Subject: [PATCH 01/32] always restart docker images --- Docker/dev/docker-compose.yaml | 4 ++++ Docker/dist/docker-compose.yaml | 4 ++++ Docker/prod/docker-compose.yaml | 5 +++++ Docker/test/docker-compose.yaml | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/Docker/dev/docker-compose.yaml b/Docker/dev/docker-compose.yaml index 5cae3f5f3..d3b00adf6 100644 --- a/Docker/dev/docker-compose.yaml +++ b/Docker/dev/docker-compose.yaml @@ -1,6 +1,7 @@ services: client: image: uptime_client:latest + restart: always ports: - "5173:5173" @@ -8,6 +9,7 @@ services: - server server: image: uptime_server:latest + restart: always ports: - "5000:5000" env_file: @@ -17,12 +19,14 @@ services: - mongodb redis: image: uptime_redis:latest + restart: always ports: - "6379:6379" volumes: - ./redis/data:/data mongodb: image: uptime_database_mongo:latest + restart: always command: ["mongod", "--quiet"] ports: - "27017:27017" diff --git a/Docker/dist/docker-compose.yaml b/Docker/dist/docker-compose.yaml index 6d3faaf7f..f12c0979d 100644 --- a/Docker/dist/docker-compose.yaml +++ b/Docker/dist/docker-compose.yaml @@ -1,6 +1,7 @@ services: client: image: bluewaveuptime/uptime_client:latest + restart: always environment: UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1" ports: @@ -10,6 +11,7 @@ services: - server server: image: bluewaveuptime/uptime_server:latest + restart: always ports: - "5000:5000" depends_on: @@ -22,12 +24,14 @@ services: # - /var/run/docker.sock:/var/run/docker.sock:ro redis: image: bluewaveuptime/uptime_redis:latest + restart: always ports: - "6379:6379" volumes: - ./redis/data:/data mongodb: image: bluewaveuptime/uptime_database_mongo:latest + restart: always volumes: - ./mongo/data:/data/db command: ["mongod", "--quiet"] diff --git a/Docker/prod/docker-compose.yaml b/Docker/prod/docker-compose.yaml index e262ef1a2..67bfe42e8 100644 --- a/Docker/prod/docker-compose.yaml +++ b/Docker/prod/docker-compose.yaml @@ -1,6 +1,7 @@ services: client: image: uptime_client:latest + restart: always environment: UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1" ports: @@ -15,11 +16,13 @@ services: certbot: image: certbot/certbot:latest + restart: always volumes: - ./certbot/www/:/var/www/certbot/:rw - ./certbot/conf/:/etc/letsencrypt/:rw server: image: uptime_server:latest + restart: always ports: - "5000:5000" env_file: @@ -29,12 +32,14 @@ services: - mongodb redis: image: uptime_redis:latest + restart: always ports: - "6379:6379" volumes: - ./redis/data:/data mongodb: image: uptime_database_mongo:latest + restart: always command: ["mongod", "--quiet", "--auth"] ports: - "27017:27017" diff --git a/Docker/test/docker-compose.yaml b/Docker/test/docker-compose.yaml index e262ef1a2..434b5ef68 100644 --- a/Docker/test/docker-compose.yaml +++ b/Docker/test/docker-compose.yaml @@ -1,6 +1,7 @@ services: client: image: uptime_client:latest + restart: always environment: UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1" ports: @@ -15,11 +16,13 @@ services: certbot: image: certbot/certbot:latest + restart: always volumes: - ./certbot/www/:/var/www/certbot/:rw - ./certbot/conf/:/etc/letsencrypt/:rw server: image: uptime_server:latest + restart: always ports: - "5000:5000" env_file: @@ -29,6 +32,7 @@ services: - mongodb redis: image: uptime_redis:latest + restart: always ports: - "6379:6379" volumes: From 3a4aa2f45f89965227077f343979d9875b108bea Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 23 Dec 2024 18:43:38 +0000 Subject: [PATCH 02/32] fix(deps): update material-ui monorepo --- Client/package-lock.json | 90 ++++++++++++++++++++-------------------- Client/package.json | 6 +-- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index c3ac51a62..e2398e9ba 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -11,9 +11,9 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.13", - "@mui/icons-material": "6.2.1", - "@mui/lab": "6.0.0-beta.20", - "@mui/material": "6.2.1", + "@mui/icons-material": "6.3.0", + "@mui/lab": "6.0.0-beta.21", + "@mui/material": "6.3.0", "@mui/x-charts": "^7.5.1", "@mui/x-data-grid": "7.23.3", "@mui/x-date-pickers": "7.23.3", @@ -1071,15 +1071,15 @@ } }, "node_modules/@mui/base": { - "version": "5.0.0-beta.67", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.67.tgz", - "integrity": "sha512-pwjibXqBrSw1Z1fZEIhWJnMJHSvh3BLz0pZcrX/x01B/BVf6QUbFlGVV/k8KCsZ9sPzln1l8bBJ+ynIIE9LtjA==", + "version": "5.0.0-beta.68", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.68.tgz", + "integrity": "sha512-F1JMNeLS9Qhjj3wN86JUQYBtJoXyQvknxlzwNl6eS0ZABo1MiohMONj3/WQzYPSXIKC2bS/ZbyBzdHhi2GnEpA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", "@floating-ui/react-dom": "^2.1.1", "@mui/types": "^7.2.20", - "@mui/utils": "^6.2.1", + "@mui/utils": "^6.3.0", "@popperjs/core": "^2.11.8", "clsx": "^2.1.1", "prop-types": "^15.8.1" @@ -1103,9 +1103,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.2.1.tgz", - "integrity": "sha512-U/8vS1+1XiHBnnRRESSG1gvr6JDHdPjrpnW6KEebkAQWBn6wrpbSF/XSZ8/vJIRXH5NyDmMHi4Ro5Q70//JKhA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.3.0.tgz", + "integrity": "sha512-/d8NwSuC3rMwCjswmGB3oXC4sdDuhIUJ8inVQAxGrADJhf0eq/kmy+foFKvpYhHl2siOZR+MLdFttw6/Bzqtqg==", "license": "MIT", "funding": { "type": "opencollective", @@ -1113,9 +1113,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.2.1.tgz", - "integrity": "sha512-bP0XtW+t5KFL+wjfQp2UctN/8CuWqF1qaxbYuCAsJhL+AzproM8gGOh2n8sNBcrjbVckzDNqaXqxdpn+OmoWug==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.3.0.tgz", + "integrity": "sha512-3uWws6DveDn5KxCS34p+sUNMxehuclQY6OmoJeJJ+Sfg9L7LGBpksY/nX5ywKAqickTZnn+sQyVcp963ep9jvw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0" @@ -1128,7 +1128,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^6.2.1", + "@mui/material": "^6.3.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1139,16 +1139,16 @@ } }, "node_modules/@mui/lab": { - "version": "6.0.0-beta.20", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.20.tgz", - "integrity": "sha512-Sg3aTrSSsfXcSHwKhnZbXmz7kUZzvXww3vO4p8U/DbMVSGJxll1GRpKNQkFLpZy2y7ZBTgKaZTbPYw4Q6UKrsA==", + "version": "6.0.0-beta.21", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.21.tgz", + "integrity": "sha512-hiFZgTwBNhJMUlEhmqfW4+5wy3C8UF9KFuzSOux6x4kgc9hsC0l+motXcF1Vyh+jhJYGeZ6yUoImqCf9RWzEvw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/base": "5.0.0-beta.67", - "@mui/system": "^6.2.1", + "@mui/base": "5.0.0-beta.68", + "@mui/system": "^6.3.0", "@mui/types": "^7.2.20", - "@mui/utils": "^6.2.1", + "@mui/utils": "^6.3.0", "clsx": "^2.1.1", "prop-types": "^15.8.1" }, @@ -1162,8 +1162,8 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material": "^6.2.1", - "@mui/material-pigment-css": "^6.2.1", + "@mui/material": "^6.3.0", + "@mui/material-pigment-css": "^6.3.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1184,16 +1184,16 @@ } }, "node_modules/@mui/material": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.2.1.tgz", - "integrity": "sha512-7VlKGsRKsy1bOSOPaSNgpkzaL+0C7iWAVKd2KYyAvhR9fTLJtiAMpq+KuzgEh1so2mtvQERN0tZVIceWMiIesw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.3.0.tgz", + "integrity": "sha512-qhlTFyRMxfoVPxUtA5e8IvqxP0dWo2Ij7cvot7Orag+etUlZH+3UwD8gZGt+3irOoy7Ms3UNBflYjwEikUXtAQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.2.1", - "@mui/system": "^6.2.1", + "@mui/core-downloads-tracker": "^6.3.0", + "@mui/system": "^6.3.0", "@mui/types": "^7.2.20", - "@mui/utils": "^6.2.1", + "@mui/utils": "^6.3.0", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", @@ -1212,7 +1212,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.2.1", + "@mui/material-pigment-css": "^6.3.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1239,13 +1239,13 @@ "license": "MIT" }, "node_modules/@mui/private-theming": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.2.1.tgz", - "integrity": "sha512-u1y0gpcfrRRxCcIdVeU5eIvkinA82Q8ft178WUNYuoFQrsOrXdlBdZlRVi+eYuUFp1iXI55Cud7sMZZtETix5Q==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.3.0.tgz", + "integrity": "sha512-tdS8jvqMokltNTXg6ioRCCbVdDmZUJZa/T9VtTnX2Lwww3FTgCakst9tWLZSxm1fEE9Xp0m7onZJmgeUmWQYVw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.2.1", + "@mui/utils": "^6.3.0", "prop-types": "^15.8.1" }, "engines": { @@ -1266,9 +1266,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.2.1.tgz", - "integrity": "sha512-6R3OgYw6zgCZWFYYMfxDqpGfJA78mUTOIlUDmmJlr60ogVNCrM87X0pqx5TbZ2OwUyxlJxN9qFgRr+J9H6cOBg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.3.0.tgz", + "integrity": "sha512-iWA6eyiPkO07AlHxRUvI7dwVRSc+84zV54kLmjUms67GJeOWVuXlu8ZO+UhCnwJxHacghxnabsMEqet5PYQmHg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -1300,16 +1300,16 @@ } }, "node_modules/@mui/system": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.2.1.tgz", - "integrity": "sha512-0lc8CbBP4WAAF+SmGMFJI9bpIyQvW3zvwIDzLsb26FIB/4Z0pO7qGe8mkAl0RM63Vb37899qxnThhHKgAAdy6w==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.3.0.tgz", + "integrity": "sha512-L+8hUHMNlfReKSqcnVslFrVhoNfz/jw7Fe9NfDE85R3KarvZ4O3MU9daF/lZeqEAvnYxEilkkTfDwQ7qCgJdFg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.2.1", - "@mui/styled-engine": "^6.2.1", + "@mui/private-theming": "^6.3.0", + "@mui/styled-engine": "^6.3.0", "@mui/types": "^7.2.20", - "@mui/utils": "^6.2.1", + "@mui/utils": "^6.3.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1354,9 +1354,9 @@ } }, "node_modules/@mui/utils": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.2.1.tgz", - "integrity": "sha512-ubLqGIMhKUH2TF/Um+wRzYXgAooQw35th+DPemGrTpgrZHpOgcnUDIDbwsk1e8iQiuJ3mV/ErTtcQrecmlj5cg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.3.0.tgz", + "integrity": "sha512-MkDBF08OPVwXhAjedyMykRojgvmf0y/jxkBWjystpfI/pasyTYrfdv4jic6s7j3y2+a+SJzS9qrD6X8ZYj/8AQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", diff --git a/Client/package.json b/Client/package.json index 1e22e76d3..300e79e3e 100644 --- a/Client/package.json +++ b/Client/package.json @@ -14,9 +14,9 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.13", - "@mui/icons-material": "6.2.1", - "@mui/lab": "6.0.0-beta.20", - "@mui/material": "6.2.1", + "@mui/icons-material": "6.3.0", + "@mui/lab": "6.0.0-beta.21", + "@mui/material": "6.3.0", "@mui/x-charts": "^7.5.1", "@mui/x-data-grid": "7.23.3", "@mui/x-date-pickers": "7.23.3", From be45dba3df42d7d1f8c72bfb5c3a4f1ef0c9fd21 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 23 Dec 2024 15:08:36 -0800 Subject: [PATCH 03/32] fix test config docker-compose URL --- Docker/test/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docker/test/docker-compose.yaml b/Docker/test/docker-compose.yaml index e262ef1a2..2b10f4700 100644 --- a/Docker/test/docker-compose.yaml +++ b/Docker/test/docker-compose.yaml @@ -2,7 +2,7 @@ services: client: image: uptime_client:latest environment: - UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1" + UPTIME_APP_API_BASE_URL: "https://checkmate-test.bluewavelabs.ca/api/v1" ports: - "80:80" - "443:443" From fdb77bb073f39186795a6dc133da0e3a3e1dfa35 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 23 Dec 2024 15:16:52 -0800 Subject: [PATCH 04/32] move middleware to after JobQueue initialization --- Server/index.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Server/index.js b/Server/index.js index 996c4fd07..55e9cec34 100644 --- a/Server/index.js +++ b/Server/index.js @@ -70,15 +70,6 @@ const startApp = async () => { app.use(express.json()); app.use(helmet()); - // Add db, jobQueue, emailService, and settingsService to request object for easy access - app.use((req, res, next) => { - req.db = db; - req.jobQueue = jobQueue; - req.emailService = emailService; - req.settingsService = settingsService; - next(); - }); - // Swagger UI app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); @@ -162,6 +153,15 @@ const startApp = async () => { Worker ); + // Add db, jobQueue, emailService, and settingsService to request object for easy access + app.use((req, res, next) => { + req.db = db; + req.jobQueue = jobQueue; + req.emailService = emailService; + req.settingsService = settingsService; + next(); + }); + const shutdown = async () => { if (isShuttingDown) { return; From bd81c49b1e52d714339e9da005822cc6c3d01afb Mon Sep 17 00:00:00 2001 From: Alexander Holliday Date: Mon, 23 Dec 2024 15:52:16 -0800 Subject: [PATCH 05/32] Revert "hotfix: move middleware to after JobQueue initialization" --- Server/index.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Server/index.js b/Server/index.js index 55e9cec34..996c4fd07 100644 --- a/Server/index.js +++ b/Server/index.js @@ -70,6 +70,15 @@ const startApp = async () => { app.use(express.json()); app.use(helmet()); + // Add db, jobQueue, emailService, and settingsService to request object for easy access + app.use((req, res, next) => { + req.db = db; + req.jobQueue = jobQueue; + req.emailService = emailService; + req.settingsService = settingsService; + next(); + }); + // Swagger UI app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); @@ -153,15 +162,6 @@ const startApp = async () => { Worker ); - // Add db, jobQueue, emailService, and settingsService to request object for easy access - app.use((req, res, next) => { - req.db = db; - req.jobQueue = jobQueue; - req.emailService = emailService; - req.settingsService = settingsService; - next(); - }); - const shutdown = async () => { if (isShuttingDown) { return; From 293bc12cef65a6cdd67589d1ab6a3ad4825a6c39 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 24 Dec 2024 04:27:42 +0000 Subject: [PATCH 06/32] chore(deps): update dependency eslint-plugin-react to v7.37.3 --- Client/package-lock.json | 796 +++++++++++++++++++++++---------------- 1 file changed, 470 insertions(+), 326 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index c3ac51a62..644551e56 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -2506,14 +2506,14 @@ "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -2584,16 +2584,16 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2620,20 +2620,19 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -2741,17 +2740,47 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -3043,15 +3072,15 @@ } }, "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3061,31 +3090,31 @@ } }, "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -3219,6 +3248,21 @@ "tslib": "^2.0.3" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.65", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.65.tgz", @@ -3247,58 +3291,59 @@ } }, "node_modules/es-abstract": { - "version": "1.23.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.5.tgz", - "integrity": "sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==", + "version": "1.23.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.7.tgz", + "integrity": "sha512-OygGC8kIcDhXX+6yAZRGLqwi2CmEXCbLQixeGUgYeR+Qwlppqmo7DIDr8XibtEBZp+fJcoYpoatp5qwLMEdcqQ==", "dev": true, "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.6", + "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", - "gopd": "^1.0.1", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", + "object.assign": "^4.1.7", "regexp.prototype.flags": "^1.5.3", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", + "safe-array-concat": "^1.1.3", + "safe-regex-test": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" }, "engines": { "node": ">= 0.4" @@ -3308,14 +3353,11 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -3331,27 +3373,28 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.0.tgz", - "integrity": "sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", + "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", - "gopd": "^1.0.1", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.3", - "safe-array-concat": "^1.1.2" + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" }, "engines": { "node": ">= 0.4" @@ -3396,15 +3439,15 @@ } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -3530,29 +3573,29 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", - "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", + "version": "7.37.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.3.tgz", + "integrity": "sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==", "dev": true, "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.2", + "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.1.0", + "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11", + "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "engines": { @@ -3911,16 +3954,18 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -3949,17 +3994,22 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", + "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", "dev": true, "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -3969,15 +4019,15 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -4048,13 +4098,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4101,11 +4151,14 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -4114,9 +4167,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -4235,15 +4288,15 @@ "license": "ISC" }, "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4259,14 +4312,15 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -4298,27 +4352,30 @@ } }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz", + "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4356,12 +4413,14 @@ } }, "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "license": "MIT", "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -4372,13 +4431,14 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4398,13 +4458,13 @@ } }, "node_modules/is-finalizationregistry": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.0.tgz", - "integrity": "sha512-qfMdqbAQEwBw78ZyReKnlA8ezmPdb9BemzIIip/JkjaZUhitfXDkkr+3QTboW0JrSXT1QWyYShpvnNHGZ4c4yA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -4455,27 +4515,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4495,14 +4543,16 @@ } }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -4525,13 +4575,13 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -4541,13 +4591,14 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4557,13 +4608,15 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4573,13 +4626,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -4602,27 +4655,30 @@ } }, "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz", + "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -4646,17 +4702,18 @@ "license": "ISC" }, "node_modules/iterator.prototype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", - "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", + "integrity": "sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA==", "dev": true, "license": "MIT", "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "reflect.getprototypeof": "^1.0.8", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -4858,6 +4915,16 @@ "yallist": "^3.0.2" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4971,15 +5038,17 @@ } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -5024,13 +5093,14 @@ } }, "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -5509,19 +5579,20 @@ } }, "node_modules/reflect.getprototypeof": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz", - "integrity": "sha512-bMvFGIUKlc/eSfXNX+aZ+EL95/EgZzuwA0OBPTbZZDEJw/0AkentjMuM1oiRfwHrshqk4RzdgiTg5CcDalXN5g==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz", + "integrity": "sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", + "dunder-proto": "^1.0.1", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "which-builtin-type": "^1.1.4" + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "which-builtin-type": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -5683,15 +5754,16 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -5702,15 +5774,15 @@ } }, "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "is-regex": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -5795,16 +5867,73 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -5842,24 +5971,25 @@ } }, "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", - "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5880,16 +6010,19 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5899,16 +6032,20 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6040,32 +6177,32 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -6075,19 +6212,19 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.3.tgz", - "integrity": "sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "reflect.getprototypeof": "^1.0.6" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -6118,16 +6255,19 @@ } }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6294,42 +6434,45 @@ } }, "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-builtin-type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.0.tgz", - "integrity": "sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", + "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", + "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", + "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", - "which-typed-array": "^1.1.15" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -6358,16 +6501,17 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "for-each": "^0.3.3", - "gopd": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { From 7c1442e32f5e90399593ec36aa37674a64b54eaf Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 24 Dec 2024 11:56:08 -0800 Subject: [PATCH 07/32] Add a service registry --- Server/service/serviceRegistry.js | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Server/service/serviceRegistry.js diff --git a/Server/service/serviceRegistry.js b/Server/service/serviceRegistry.js new file mode 100644 index 000000000..69a3b8be5 --- /dev/null +++ b/Server/service/serviceRegistry.js @@ -0,0 +1,35 @@ +const SERVICE_NAME = "ServiceRegistry"; +import logger from "../utils/logger.js"; +class ServiceRegistry { + static SERVICE_NAME = SERVICE_NAME; + constructor() { + this.services = {}; + } + + register(name, service) { + logger.info({ + message: `Registering service ${name}`, + service: SERVICE_NAME, + method: "register", + }); + this.services[name] = service; + } + + get(name) { + if (!this.services[name]) { + logger.error({ + message: `Service ${name} is not registered`, + service: SERVICE_NAME, + method: "get", + }); + throw new Error(`Service ${name} is not registered`); + } + return this.services[name]; + } + + listServices() { + return Object.keys(this.services); + } +} + +export default new ServiceRegistry(); From 8b0af64761bd2d246ee561c477dfa425368b9d06 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 24 Dec 2024 11:58:26 -0800 Subject: [PATCH 08/32] Add static name exports to all services --- Server/service/emailService.js | 1 + Server/service/jobQueue.js | 4 +++- Server/service/networkService.js | 5 ++++- Server/service/notificationService.js | 5 ++++- Server/service/settingsService.js | 1 + Server/service/statusService.js | 5 ++++- 6 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Server/service/emailService.js b/Server/service/emailService.js index 846b7c6c8..3aeb2c8a8 100644 --- a/Server/service/emailService.js +++ b/Server/service/emailService.js @@ -10,6 +10,7 @@ const SERVICE_NAME = "EmailService"; * Represents an email service that can load templates, build, and send emails. */ class EmailService { + static SERVICE_NAME = SERVICE_NAME; /** * Constructs an instance of the EmailService, initializing template loaders and the email transporter. * @param {Object} settingsService - The settings service to get email configuration. diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index 182f0235a..cfe1b2ccf 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -1,7 +1,8 @@ const QUEUE_NAME = "monitors"; const JOBS_PER_WORKER = 5; -import { errorMessages, successMessages } from "../utils/messages.js"; const SERVICE_NAME = "JobQueue"; + +import { errorMessages, successMessages } from "../utils/messages.js"; /** * JobQueue * @@ -10,6 +11,7 @@ const SERVICE_NAME = "JobQueue"; * It scales the number of workers based on the number of jobs/worker */ class JobQueue { + static SERVICE_NAME = SERVICE_NAME; /** * @class JobQueue * @classdesc Manages job queue and workers. diff --git a/Server/service/networkService.js b/Server/service/networkService.js index 60405df90..0de66aa00 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -1,4 +1,6 @@ import { errorMessages, successMessages } from "../utils/messages.js"; +const SERVICE_NAME = "NetworkService"; + /** * Constructs a new NetworkService instance. * @@ -8,13 +10,14 @@ import { errorMessages, successMessages } from "../utils/messages.js"; * @param {Object} http - The HTTP utility for network operations. */ class NetworkService { + static SERVICE_NAME = SERVICE_NAME; constructor(axios, ping, logger, http, Docker) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; this.TYPE_PAGESPEED = "pagespeed"; this.TYPE_HARDWARE = "hardware"; this.TYPE_DOCKER = "docker"; - this.SERVICE_NAME = "NetworkService"; + this.SERVICE_NAME = SERVICE_NAME; this.NETWORK_ERROR = 5000; this.PING_ERROR = 5001; this.axios = axios; diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index 25bf5bfa6..807640a61 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -1,4 +1,7 @@ +const SERVICE_NAME = "NotificationService"; + class NotificationService { + static SERVICE_NAME = SERVICE_NAME; /** * Creates an instance of NotificationService. * @@ -7,7 +10,7 @@ class NotificationService { * @param {Object} logger - The logger instance for logging activities. */ constructor(emailService, db, logger) { - this.SERVICE_NAME = "NotificationService"; + this.SERVICE_NAME = SERVICE_NAME; this.emailService = emailService; this.db = db; this.logger = logger; diff --git a/Server/service/settingsService.js b/Server/service/settingsService.js index 9f44f7d07..9b3757783 100644 --- a/Server/service/settingsService.js +++ b/Server/service/settingsService.js @@ -25,6 +25,7 @@ const envConfig = { * from the database if they are not set in the environment. */ class SettingsService { + static SERVICE_NAME = SERVICE_NAME; /** * Constructs a new SettingsService * @constructor diff --git a/Server/service/statusService.js b/Server/service/statusService.js index 251f4e391..96013ae7f 100644 --- a/Server/service/statusService.js +++ b/Server/service/statusService.js @@ -1,4 +1,7 @@ +const SERVICE_NAME = "StatusService"; + class StatusService { + static SERVICE_NAME = SERVICE_NAME; /** * Creates an instance of StatusService. * @@ -8,7 +11,7 @@ class StatusService { constructor(db, logger) { this.db = db; this.logger = logger; - this.SERVICE_NAME = "StatusService"; + this.SERVICE_NAME = SERVICE_NAME; } getStatusString = (status) => { From cc40bd6006b51a7787793d1479ecddeeb35309e0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 24 Dec 2024 11:58:42 -0800 Subject: [PATCH 09/32] refactor controllers to be class based --- Server/controllers/authController.js | 941 ++++++++--------- Server/controllers/checkController.js | 245 +++-- Server/controllers/inviteController.js | 141 +-- .../maintenanceWindowController.js | 279 +++--- Server/controllers/monitorController.js | 947 +++++++++--------- Server/controllers/queueController.js | 103 +- Server/controllers/settingsController.js | 75 +- Server/controllers/statusPageController.js | 79 +- 8 files changed, 1414 insertions(+), 1396 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index e4566429a..58458b0da 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -17,500 +17,501 @@ import { handleValidationError, handleError } from "./controllerUtils.js"; const SERVICE_NAME = "authController"; dotenv.config(); -/** - * Creates and returns JWT token with an arbitrary payload - * @function - * @param {Object} payload - * @param {tokenType} typeOfToken - Whether to generate refresh token with long TTL or access token with short TTL. - * @param {Object} appSettings - * @returns {String} - * @throws {Error} - */ -const issueToken = (payload, typeOfToken, appSettings) => { - try { - const tokenTTL = - typeOfToken === tokenType.REFRESH_TOKEN - ? (appSettings?.refreshTokenTTL ?? "7d") - : (appSettings?.jwtTTL ?? "2h"); - const tokenSecret = - typeOfToken === tokenType.REFRESH_TOKEN - ? appSettings?.refreshTokenSecret - : appSettings?.jwtSecret; - const payloadData = typeOfToken === tokenType.REFRESH_TOKEN ? {} : payload; - - return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); - } catch (error) { - throw handleError(error, SERVICE_NAME, "issueToken"); +class AuthController { + constructor(db, settingsService, emailService, jobQueue) { + this.db = db; + this.settingsService = settingsService; + this.emailService = emailService; + this.jobQueue = jobQueue; } -}; -/** - * Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.inviteToken - The invite token for registration. - * @property {Object} req.file - The file object for the user's profile image. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the creation of the user, the created user data, and a JWT token. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ -const registerUser = async (req, res, next) => { - // joi validation - try { - await registrationBodyValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - // Create a new user - try { - const { inviteToken } = req.body; - // If superAdmin exists, a token should be attached to all further register requests - const superAdminExists = await req.db.checkSuperadmin(req, res); - if (superAdminExists) { - await req.db.getInviteTokenAndDelete(inviteToken); - } else { - // This is the first account, create JWT secret to use if one is not supplied by env - const jwtSecret = crypto.randomBytes(64).toString("hex"); - await req.db.updateAppSettings({ jwtSecret }); + /** + * Creates and returns JWT token with an arbitrary payload + * @function + * @param {Object} payload + * @param {tokenType} typeOfToken - Whether to generate refresh token with long TTL or access token with short TTL. + * @param {Object} appSettings + * @returns {String} + * @throws {Error} + */ + issueToken = (payload, typeOfToken, appSettings) => { + try { + const tokenTTL = + typeOfToken === tokenType.REFRESH_TOKEN + ? (appSettings?.refreshTokenTTL ?? "7d") + : (appSettings?.jwtTTL ?? "2h"); + const tokenSecret = + typeOfToken === tokenType.REFRESH_TOKEN + ? appSettings?.refreshTokenSecret + : appSettings?.jwtSecret; + const payloadData = typeOfToken === tokenType.REFRESH_TOKEN ? {} : payload; + + return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); + } catch (error) { + throw handleError(error, SERVICE_NAME, "issueToken"); } + }; - const newUser = await req.db.insertUser({ ...req.body }, req.file); - logger.info({ - message: successMessages.AUTH_CREATE_USER, - service: SERVICE_NAME, - details: newUser._id, - }); + /** + * Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.body - The body of the request. + * @property {string} req.body.inviteToken - The invite token for registration. + * @property {Object} req.file - The file object for the user's profile image. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message indicating the creation of the user, the created user data, and a JWT token. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + */ + registerUser = async (req, res, next) => { + try { + await registrationBodyValidation.validateAsync(req.body); + } catch (error) { + const validationError = handleValidationError(error, SERVICE_NAME); + next(validationError); + return; + } + // Create a new user + try { + const { inviteToken } = req.body; + // If superAdmin exists, a token should be attached to all further register requests + const superAdminExists = await this.db.checkSuperadmin(req, res); + if (superAdminExists) { + await this.db.getInviteTokenAndDelete(inviteToken); + } else { + // This is the first account, create JWT secret to use if one is not supplied by env + const jwtSecret = crypto.randomBytes(64).toString("hex"); + await this.db.updateAppSettings({ jwtSecret }); + } - const userForToken = { ...newUser._doc }; - delete userForToken.profileImage; - delete userForToken.avatarImage; - - const appSettings = await req.settingsService.getSettings(); - - const token = issueToken(userForToken, tokenType.ACCESS_TOKEN, appSettings); - const refreshToken = issueToken({}, tokenType.REFRESH_TOKEN, appSettings); - - req.emailService - .buildAndSendEmail( - "welcomeEmailTemplate", - { name: newUser.firstName }, - newUser.email, - "Welcome to Uptime Monitor" - ) - .catch((error) => { - logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "registerUser", - stack: error.stack, - }); + const newUser = await this.db.insertUser({ ...req.body }, req.file); + logger.info({ + message: successMessages.AUTH_CREATE_USER, + service: SERVICE_NAME, + details: newUser._id, }); - return res.status(200).json({ - success: true, - msg: successMessages.AUTH_CREATE_USER, - data: { user: newUser, token: token, refreshToken: refreshToken }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "registerController")); - } -}; + const userForToken = { ...newUser._doc }; + delete userForToken.profileImage; + delete userForToken.avatarImage; -/** - * Logs in a user by validating the user's credentials and issuing a JWT token. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.email - The email of the user. - * @property {string} req.body.password - The password of the user. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the login of the user, the user data (without password and avatar image), and a JWT token. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422) or the password is incorrect. - */ -const loginUser = async (req, res, next) => { - try { - await loginValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - try { - const { email, password } = req.body; + const appSettings = await this.settingsService.getSettings(); - // Check if user exists - const user = await req.db.getUserByEmail(email); + const token = this.issueToken(userForToken, tokenType.ACCESS_TOKEN, appSettings); + const refreshToken = this.issueToken({}, tokenType.REFRESH_TOKEN, appSettings); - // Compare password - const match = await user.comparePassword(password); - if (match !== true) { - next(new Error(errorMessages.AUTH_INCORRECT_PASSWORD)); + this.emailService + .buildAndSendEmail( + "welcomeEmailTemplate", + { name: newUser.firstName }, + newUser.email, + "Welcome to Uptime Monitor" + ) + .catch((error) => { + logger.error({ + message: error.message, + service: SERVICE_NAME, + method: "registerUser", + stack: error.stack, + }); + }); + + return res.status(200).json({ + success: true, + msg: successMessages.AUTH_CREATE_USER, + data: { user: newUser, token: token, refreshToken: refreshToken }, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "registerController")); + } + }; + + /** + * Logs in a user by validating the user's credentials and issuing a JWT token. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.body - The body of the request. + * @property {string} req.body.email - The email of the user. + * @property {string} req.body.password - The password of the user. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message indicating the login of the user, the user data (without password and avatar image), and a JWT token. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422) or the password is incorrect. + */ + loginUser = async (req, res, next) => { + try { + await loginValidation.validateAsync(req.body); + } catch (error) { + const validationError = handleValidationError(error, SERVICE_NAME); + next(validationError); + return; + } + try { + const { email, password } = req.body; + + // Check if user exists + const user = await this.db.getUserByEmail(email); + + // Compare password + const match = await user.comparePassword(password); + if (match !== true) { + next(new Error(errorMessages.AUTH_INCORRECT_PASSWORD)); + return; + } + + // Remove password from user object. Should this be abstracted to DB layer? + const userWithoutPassword = { ...user._doc }; + delete userWithoutPassword.password; + delete userWithoutPassword.avatarImage; + + // Happy path, return token + const appSettings = await this.settingsService.getSettings(); + const token = this.issueToken( + userWithoutPassword, + tokenType.ACCESS_TOKEN, + appSettings + ); + const refreshToken = this.issueToken({}, tokenType.REFRESH_TOKEN, appSettings); + // reset avatar image + userWithoutPassword.avatarImage = user.avatarImage; + + return res.status(200).json({ + success: true, + msg: successMessages.AUTH_LOGIN_USER, + data: { user: userWithoutPassword, token: token, refreshToken: refreshToken }, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "loginUser")); + } + }; + + /** + * Generates new auth token if the refresh token is valid + * @async + * @param {Express.Request} req - The Express request object. + * @property {Object} req.headers - The parameter of the request. + * @param {Express.Response} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message indicating new auth token is generated. + * @throws {Error} If there is an error during the process such as any of the token is not received + */ + refreshAuthToken = async (req, res, next) => { + try { + // check for refreshToken + const refreshToken = req.headers["x-refresh-token"]; + + if (!refreshToken) { + // No refresh token provided + const error = new Error(errorMessages.NO_REFRESH_TOKEN); + error.status = 401; + error.service = SERVICE_NAME; + error.method = "refreshAuthToken"; + return next(error); + } + + // Verify refresh token + const appSettings = await this.settingsService.getSettings(); + const { refreshTokenSecret } = appSettings; + jwt.verify(refreshToken, refreshTokenSecret, async (refreshErr, refreshDecoded) => { + if (refreshErr) { + // Invalid or expired refresh token, trigger logout + const errorMessage = + refreshErr.name === "TokenExpiredError" + ? errorMessages.EXPIRED_REFRESH_TOKEN + : errorMessages.INVALID_REFRESH_TOKEN; + const error = new Error(errorMessage); + error.status = 401; + error.service = SERVICE_NAME; + return next(error); + } + }); + // Refresh token is valid and unexpired, generate new access token + const oldAuthToken = getTokenFromHeaders(req.headers); + const { jwtSecret } = await this.settingsService.getSettings(); + const payloadData = jwt.verify(oldAuthToken, jwtSecret, { ignoreExpiration: true }); + // delete old token related data + delete payloadData.iat; + delete payloadData.exp; + const newAuthToken = this.issueToken( + payloadData, + tokenType.ACCESS_TOKEN, + appSettings + ); + + return res.status(200).json({ + success: true, + msg: successMessages.AUTH_TOKEN_REFRESHED, + data: { user: payloadData, token: newAuthToken, refreshToken: refreshToken }, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "refreshAuthToken")); + } + }; + /** + * Edits a user's information. If the user wants to change their password, the current password is checked before updating to the new password. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.params - The parameters of the request. + * @property {string} req.params.userId - The ID of the user to be edited. + * @property {Object} req.body - The body of the request. + * @property {string} req.body.password - The current password of the user. + * @property {string} req.body.newPassword - The new password of the user. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message indicating the update of the user, and the updated user data. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422), the user is unauthorized (401), or the password is incorrect (403). + */ + editUser = async (req, res, next) => { + try { + await editUserParamValidation.validateAsync(req.params); + await editUserBodyValidation.validateAsync(req.body); + } catch (error) { + const validationError = handleValidationError(error, SERVICE_NAME); + next(validationError); return; } - // Remove password from user object. Should this be abstracted to DB layer? - const userWithoutPassword = { ...user._doc }; - delete userWithoutPassword.password; - delete userWithoutPassword.avatarImage; - - // Happy path, return token - const appSettings = await req.settingsService.getSettings(); - const token = issueToken(userWithoutPassword, tokenType.ACCESS_TOKEN, appSettings); - const refreshToken = issueToken({}, tokenType.REFRESH_TOKEN, appSettings); - // reset avatar image - userWithoutPassword.avatarImage = user.avatarImage; - - return res.status(200).json({ - success: true, - msg: successMessages.AUTH_LOGIN_USER, - data: { user: userWithoutPassword, token: token, refreshToken: refreshToken }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "loginUser")); - } -}; - -/** - * Generates new auth token if the refresh token is valid - * @async - * @param {Express.Request} req - The Express request object. - * @property {Object} req.headers - The parameter of the request. - * @param {Express.Response} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating new auth token is generated. - * @throws {Error} If there is an error during the process such as any of the token is not received - */ -const refreshAuthToken = async (req, res, next) => { - try { - // check for refreshToken - const refreshToken = req.headers["x-refresh-token"]; - - if (!refreshToken) { - // No refresh token provided - const error = new Error(errorMessages.NO_REFRESH_TOKEN); + // TODO is this neccessary any longer? Verify ownership middleware should handle this + if (req.params.userId !== req.user._id.toString()) { + const error = new Error(errorMessages.AUTH_UNAUTHORIZED); error.status = 401; error.service = SERVICE_NAME; - error.method = "refreshAuthToken"; - return next(error); + next(error); + return; } - // Verify refresh token - const appSettings = await req.settingsService.getSettings(); - const { refreshTokenSecret } = appSettings; - jwt.verify(refreshToken, refreshTokenSecret, async (refreshErr, refreshDecoded) => { - if (refreshErr) { - // Invalid or expired refresh token, trigger logout - const errorMessage = - refreshErr.name === "TokenExpiredError" - ? errorMessages.EXPIRED_REFRESH_TOKEN - : errorMessages.INVALID_REFRESH_TOKEN; - const error = new Error(errorMessage); - error.status = 401; - error.service = SERVICE_NAME; - return next(error); + try { + // Change Password check + if (req.body.password && req.body.newPassword) { + // Get token from headers + const token = getTokenFromHeaders(req.headers); + // Get email from token + const { jwtSecret } = this.settingsService.getSettings(); + const { email } = jwt.verify(token, jwtSecret); + // Add user email to body for DB operation + req.body.email = email; + // Get user + const user = await this.db.getUserByEmail(email); + // Compare passwords + const match = await user.comparePassword(req.body.password); + // If not a match, throw a 403 + // 403 instead of 401 to avoid triggering axios interceptor + if (!match) { + const error = new Error(errorMessages.AUTH_INCORRECT_PASSWORD); + error.status = 403; + next(error); + return; + } + // If a match, update the password + req.body.password = req.body.newPassword; } - }); - // Refresh token is valid and unexpired, generate new access token - const oldAuthToken = getTokenFromHeaders(req.headers); - const { jwtSecret } = await req.settingsService.getSettings(); - const payloadData = jwt.verify(oldAuthToken, jwtSecret, { ignoreExpiration: true }); - // delete old token related data - delete payloadData.iat; - delete payloadData.exp; - const newAuthToken = issueToken(payloadData, tokenType.ACCESS_TOKEN, appSettings); - return res.status(200).json({ - success: true, - msg: successMessages.AUTH_TOKEN_REFRESHED, - data: { user: payloadData, token: newAuthToken, refreshToken: refreshToken }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "refreshAuthToken")); - } -}; + const updatedUser = await this.db.updateUser(req, res); + return res.status(200).json({ + success: true, + msg: successMessages.AUTH_UPDATE_USER, + data: updatedUser, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "userEditController")); + } + }; -/** - * Edits a user's information. If the user wants to change their password, the current password is checked before updating to the new password. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.userId - The ID of the user to be edited. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.password - The current password of the user. - * @property {string} req.body.newPassword - The new password of the user. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the update of the user, and the updated user data. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422), the user is unauthorized (401), or the password is incorrect (403). - */ -const editUser = async (req, res, next) => { - try { - await editUserParamValidation.validateAsync(req.params); - await editUserBodyValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - - // TODO is this neccessary any longer? Verify ownership middleware should handle this - if (req.params.userId !== req.user._id.toString()) { - const error = new Error(errorMessages.AUTH_UNAUTHORIZED); - error.status = 401; - error.service = SERVICE_NAME; - next(error); - return; - } - - try { - // Change Password check - if (req.body.password && req.body.newPassword) { - // Get token from headers - const token = getTokenFromHeaders(req.headers); - // Get email from token - const { jwtSecret } = req.settingsService.getSettings(); - const { email } = jwt.verify(token, jwtSecret); - // Add user email to body for DB operation - req.body.email = email; - // Get user - const user = await req.db.getUserByEmail(email); - // Compare passwords - const match = await user.comparePassword(req.body.password); - // If not a match, throw a 403 - // 403 instead of 401 to avoid triggering axios interceptor - if (!match) { - const error = new Error(errorMessages.AUTH_INCORRECT_PASSWORD); - error.status = 403; - next(error); - return; - } - // If a match, update the password - req.body.password = req.body.newPassword; + /** + * Checks if a superadmin account exists in the database. + * @async + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message indicating the existence of a superadmin, and a boolean indicating the existence of a superadmin. + * @throws {Error} If there is an error during the process. + */ + checkSuperadminExists = async (req, res, next) => { + try { + const superAdminExists = await this.db.checkSuperadmin(req, res); + return res.status(200).json({ + success: true, + msg: successMessages.AUTH_ADMIN_EXISTS, + data: superAdminExists, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "checkSuperadminController")); + } + }; + /** + * Requests a recovery token for a user. The user's email is validated and a recovery token is created and sent via email. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.body - The body of the request. + * @property {string} req.body.email - The email of the user requesting recovery. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message indicating the creation of the recovery token, and the message ID of the sent email. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + */ + requestRecovery = async (req, res, next) => { + try { + await recoveryValidation.validateAsync(req.body); + } catch (error) { + const validationError = handleValidationError(error, SERVICE_NAME); + next(validationError); + return; } - const updatedUser = await req.db.updateUser(req, res); - return res.status(200).json({ - success: true, - msg: successMessages.AUTH_UPDATE_USER, - data: updatedUser, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "userEditController")); - } -}; - -/** - * Checks if a superadmin account exists in the database. - * @async - * @param {Object} req - The Express request object. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the existence of a superadmin, and a boolean indicating the existence of a superadmin. - * @throws {Error} If there is an error during the process. - */ -const checkSuperadminExists = async (req, res, next) => { - try { - const superAdminExists = await req.db.checkSuperadmin(req, res); - return res.status(200).json({ - success: true, - msg: successMessages.AUTH_ADMIN_EXISTS, - data: superAdminExists, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "checkSuperadminController")); - } -}; - -/** - * Requests a recovery token for a user. The user's email is validated and a recovery token is created and sent via email. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.email - The email of the user requesting recovery. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the creation of the recovery token, and the message ID of the sent email. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ -const requestRecovery = async (req, res, next) => { - try { - await recoveryValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - - try { - const { email } = req.body; - const user = await req.db.getUserByEmail(email); - const recoveryToken = await req.db.requestRecoveryToken(req, res); - const name = user.firstName; - const { clientHost } = req.settingsService.getSettings(); - const url = `${clientHost}/set-new-password/${recoveryToken.token}`; - const msgId = await req.emailService.buildAndSendEmail( - "passwordResetTemplate", - { - name, + try { + const { email } = req.body; + const user = await this.db.getUserByEmail(email); + const recoveryToken = await this.db.requestRecoveryToken(req, res); + const name = user.firstName; + const { clientHost } = this.settingsService.getSettings(); + const url = `${clientHost}/set-new-password/${recoveryToken.token}`; + const msgId = await this.emailService.buildAndSendEmail( + "passwordResetTemplate", + { + name, + email, + url, + }, email, - url, - }, - email, - "Checkmate Password Reset" - ); + "Checkmate Password Reset" + ); - return res.status(200).json({ - success: true, - msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN, - data: msgId, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "recoveryRequestController")); - } -}; - -/** - * Validates a recovery token. The recovery token is validated and if valid, a success message is returned. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.token - The recovery token to be validated. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status and a message indicating the validation of the recovery token. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ -const validateRecovery = async (req, res, next) => { - try { - await recoveryTokenValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - - try { - await req.db.validateRecoveryToken(req, res); - return res.status(200).json({ - success: true, - msg: successMessages.AUTH_VERIFY_RECOVERY_TOKEN, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "validateRecoveryTokenController")); - } -}; - -/** - * Resets a user's password. The new password is validated and if valid, the user's password is updated in the database and a new JWT token is issued. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.token - The recovery token. - * @property {string} req.body.password - The new password of the user. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the reset of the password, the updated user data (without password and avatar image), and a new JWT token. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ -const resetPassword = async (req, res, next) => { - try { - await newPasswordValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - try { - const user = await req.db.resetPassword(req, res); - const appSettings = await req.settingsService.getSettings(); - const token = issueToken(user._doc, tokenType.ACCESS_TOKEN, appSettings); - res.status(200).json({ - success: true, - msg: successMessages.AUTH_RESET_PASSWORD, - data: { user, token }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "resetPasswordController")); - } -}; - -/** - * Deletes a user and all associated monitors, checks, and alerts. - * - * @param {Object} req - The request object. - * @param {Object} res - The response object. - * @param {Function} next - The next middleware function. - * @returns {Object} The response object with success status and message. - * @throws {Error} If user validation fails or user is not found in the database. - */ -const deleteUser = async (req, res, next) => { - try { - const token = getTokenFromHeaders(req.headers); - const decodedToken = jwt.decode(token); - const { email } = decodedToken; - - // Check if the user exists - const user = await req.db.getUserByEmail(email); - // 1. Find all the monitors associated with the team ID if superadmin - - const result = await req.db.getMonitorsByTeamId({ - params: { teamId: user.teamId }, - }); - - if (user.role.includes("superadmin")) { - //2. Remove all jobs, delete checks and alerts - result?.monitors.length > 0 && - (await Promise.all( - result.monitors.map(async (monitor) => { - await req.jobQueue.deleteJob(monitor); - await req.db.deleteChecks(monitor._id); - await req.db.deletePageSpeedChecksByMonitorId(monitor._id); - await req.db.deleteNotificationsByMonitorId(monitor._id); - }) - )); - - // 3. Delete team - await req.db.deleteTeam(user.teamId); - // 4. Delete all other team members - await req.db.deleteAllOtherUsers(); - // 5. Delete each monitor - await req.db.deleteMonitorsByUserId(user._id); + return res.status(200).json({ + success: true, + msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN, + data: msgId, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "recoveryRequestController")); + } + }; + /** + * Validates a recovery token. The recovery token is validated and if valid, a success message is returned. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.body - The body of the request. + * @property {string} req.body.token - The recovery token to be validated. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status and a message indicating the validation of the recovery token. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + */ + validateRecovery = async (req, res, next) => { + try { + await recoveryTokenValidation.validateAsync(req.body); + } catch (error) { + const validationError = handleValidationError(error, SERVICE_NAME); + next(validationError); + return; } - // 6. Delete the user by id - await req.db.deleteUser(user._id); - return res.status(200).json({ - success: true, - msg: successMessages.AUTH_DELETE_USER, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteUserController")); - } -}; -const getAllUsers = async (req, res, next) => { - try { - const allUsers = await req.db.getAllUsers(req, res); - res.status(200).json({ success: true, msg: "Got all users", data: allUsers }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAllUsersController")); - } -}; + try { + await this.db.validateRecoveryToken(req, res); + return res.status(200).json({ + success: true, + msg: successMessages.AUTH_VERIFY_RECOVERY_TOKEN, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "validateRecoveryTokenController")); + } + }; -export { - issueToken, - registerUser, - loginUser, - refreshAuthToken, - editUser, - checkSuperadminExists, - requestRecovery, - validateRecovery, - resetPassword, - deleteUser, - getAllUsers, -}; + /** + * Resets a user's password. The new password is validated and if valid, the user's password is updated in the database and a new JWT token is issued. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.body - The body of the request. + * @property {string} req.body.token - The recovery token. + * @property {string} req.body.password - The new password of the user. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message indicating the reset of the password, the updated user data (without password and avatar image), and a new JWT token. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + */ + resetPassword = async (req, res, next) => { + try { + await newPasswordValidation.validateAsync(req.body); + } catch (error) { + const validationError = handleValidationError(error, SERVICE_NAME); + next(validationError); + return; + } + try { + const user = await this.db.resetPassword(req, res); + const appSettings = await this.settingsService.getSettings(); + const token = this.issueToken(user._doc, tokenType.ACCESS_TOKEN, appSettings); + res.status(200).json({ + success: true, + msg: successMessages.AUTH_RESET_PASSWORD, + data: { user, token }, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "resetPasswordController")); + } + }; + + /** + * Deletes a user and all associated monitors, checks, and alerts. + * + * @param {Object} req - The request object. + * @param {Object} res - The response object. + * @param {Function} next - The next middleware function. + * @returns {Object} The response object with success status and message. + * @throws {Error} If user validation fails or user is not found in the database. + */ + deleteUser = async (req, res, next) => { + try { + const token = getTokenFromHeaders(req.headers); + const decodedToken = jwt.decode(token); + const { email } = decodedToken; + + // Check if the user exists + const user = await this.db.getUserByEmail(email); + // 1. Find all the monitors associated with the team ID if superadmin + + const result = await this.db.getMonitorsByTeamId({ + params: { teamId: user.teamId }, + }); + + if (user.role.includes("superadmin")) { + //2. Remove all jobs, delete checks and alerts + result?.monitors.length > 0 && + (await Promise.all( + result.monitors.map(async (monitor) => { + await this.jobQueue.deleteJob(monitor); + await this.db.deleteChecks(monitor._id); + await this.db.deletePageSpeedChecksByMonitorId(monitor._id); + await this.db.deleteNotificationsByMonitorId(monitor._id); + }) + )); + + // 3. Delete team + await this.db.deleteTeam(user.teamId); + // 4. Delete all other team members + await this.db.deleteAllOtherUsers(); + // 5. Delete each monitor + await this.db.deleteMonitorsByUserId(user._id); + } + // 6. Delete the user by id + await this.db.deleteUser(user._id); + return res.status(200).json({ + success: true, + msg: successMessages.AUTH_DELETE_USER, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "deleteUserController")); + } + }; + + getAllUsers = async (req, res, next) => { + try { + const allUsers = await this.db.getAllUsers(req, res); + res.status(200).json({ success: true, msg: "Got all users", data: allUsers }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getAllUsersController")); + } + }; +} + +export default AuthController; diff --git a/Server/controllers/checkController.js b/Server/controllers/checkController.js index a47f3f8c4..fce245b69 100644 --- a/Server/controllers/checkController.js +++ b/Server/controllers/checkController.js @@ -16,139 +16,138 @@ import { handleValidationError, handleError } from "./controllerUtils.js"; const SERVICE_NAME = "checkController"; -const createCheck = async (req, res, next) => { - try { - await createCheckParamValidation.validateAsync(req.params); - await createCheckBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; +class CheckController { + constructor(db, settingsService) { + this.db = db; + this.settingsService = settingsService; } - try { - const checkData = { ...req.body }; - const check = await req.db.createCheck(checkData); - return res - .status(200) - .json({ success: true, msg: successMessages.CHECK_CREATE, data: check }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createCheck")); - } -}; + createCheck = async (req, res, next) => { + try { + await createCheckParamValidation.validateAsync(req.params); + await createCheckBodyValidation.validateAsync(req.body); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } -const getChecks = async (req, res, next) => { - try { - await getChecksParamValidation.validateAsync(req.params); - await getChecksQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + try { + const checkData = { ...req.body }; + const check = await this.db.createCheck(checkData); + return res + .status(200) + .json({ success: true, msg: successMessages.CHECK_CREATE, data: check }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "createCheck")); + } + }; - try { - const checks = await req.db.getChecks(req); - const checksCount = await req.db.getChecksCount(req); - return res.status(200).json({ - success: true, - msg: successMessages.CHECK_GET, - data: { checksCount, checks }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getChecks")); - } -}; + getChecks = async (req, res, next) => { + try { + await getChecksParamValidation.validateAsync(req.params); + await getChecksQueryValidation.validateAsync(req.query); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } -const getTeamChecks = async (req, res, next) => { - try { - await getTeamChecksParamValidation.validateAsync(req.params); - await getTeamChecksQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const checkData = await req.db.getTeamChecks(req); - return res.status(200).json({ - success: true, - msg: successMessages.CHECK_GET, - data: checkData, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getTeamChecks")); - } -}; + try { + const checks = await this.db.getChecks(req); + const checksCount = await this.db.getChecksCount(req); + return res.status(200).json({ + success: true, + msg: successMessages.CHECK_GET, + data: { checksCount, checks }, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getChecks")); + } + }; -const deleteChecks = async (req, res, next) => { - try { - await deleteChecksParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + getTeamChecks = async (req, res, next) => { + try { + await getTeamChecksParamValidation.validateAsync(req.params); + await getTeamChecksQueryValidation.validateAsync(req.query); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } + try { + const checkData = await this.db.getTeamChecks(req); + return res.status(200).json({ + success: true, + msg: successMessages.CHECK_GET, + data: checkData, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getTeamChecks")); + } + }; - try { - const deletedCount = await req.db.deleteChecks(req.params.monitorId); - return res.status(200).json({ - success: true, - msg: successMessages.CHECK_DELETE, - data: { deletedCount }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteChecks")); - } -}; + deleteChecks = async (req, res, next) => { + try { + await deleteChecksParamValidation.validateAsync(req.params); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } -const deleteChecksByTeamId = async (req, res, next) => { - try { - await deleteChecksByTeamIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + try { + const deletedCount = await this.db.deleteChecks(req.params.monitorId); + return res.status(200).json({ + success: true, + msg: successMessages.CHECK_DELETE, + data: { deletedCount }, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "deleteChecks")); + } + }; - try { - const deletedCount = await req.db.deleteChecksByTeamId(req.params.teamId); - return res.status(200).json({ - success: true, - msg: successMessages.CHECK_DELETE, - data: { deletedCount }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteChecksByTeamId")); - } -}; + deleteChecksByTeamId = async (req, res, next) => { + try { + await deleteChecksByTeamIdParamValidation.validateAsync(req.params); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } -const updateChecksTTL = async (req, res, next) => { - const SECONDS_PER_DAY = 86400; + try { + const deletedCount = await this.db.deleteChecksByTeamId(req.params.teamId); + return res.status(200).json({ + success: true, + msg: successMessages.CHECK_DELETE, + data: { deletedCount }, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "deleteChecksByTeamId")); + } + }; - try { - await updateChecksTTLBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + updateChecksTTL = async (req, res, next) => { + const SECONDS_PER_DAY = 86400; - try { - // Get user's teamId - const token = getTokenFromHeaders(req.headers); - const { jwtSecret } = req.settingsService.getSettings(); - const { teamId } = jwt.verify(token, jwtSecret); - const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY; - await req.db.updateChecksTTL(teamId, ttl); - return res.status(200).json({ - success: true, - msg: successMessages.CHECK_UPDATE_TTL, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "updateTTL")); - } -}; + try { + await updateChecksTTLBodyValidation.validateAsync(req.body); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } -export { - createCheck, - getChecks, - getTeamChecks, - deleteChecks, - deleteChecksByTeamId, - updateChecksTTL, -}; + try { + // Get user's teamId + const token = getTokenFromHeaders(req.headers); + const { jwtSecret } = this.settingsService.getSettings(); + const { teamId } = jwt.verify(token, jwtSecret); + const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY; + await this.db.updateChecksTTL(teamId, ttl); + return res.status(200).json({ + success: true, + msg: successMessages.CHECK_UPDATE_TTL, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "updateTTL")); + } + }; +} +export default CheckController; diff --git a/Server/controllers/inviteController.js b/Server/controllers/inviteController.js index 96b19cc56..d4eedd502 100644 --- a/Server/controllers/inviteController.js +++ b/Server/controllers/inviteController.js @@ -4,83 +4,90 @@ import { inviteVerificationBodyValidation, } from "../validation/joi.js"; import logger from "../utils/logger.js"; -import dotenv from "dotenv"; import jwt from "jsonwebtoken"; import { handleError, handleValidationError } from "./controllerUtils.js"; import { getTokenFromHeaders } from "../utils/utils.js"; -dotenv.config(); - const SERVICE_NAME = "inviteController"; -/** - * Issues an invitation to a new user. Only admins can invite new users. An invitation token is created and sent via email. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.headers - The headers of the request. - * @property {string} req.headers.authorization - The authorization header containing the JWT token. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.email - The email of the user to be invited. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the sending of the invitation, and the invitation token. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ -const issueInvitation = async (req, res, next) => { - try { - // Only admins can invite - const token = getTokenFromHeaders(req.headers); - const { role, firstname, teamId } = jwt.decode(token); - req.body.teamId = teamId; +class InviteController { + constructor(db, settingsService, emailService) { + this.db = db; + this.settingsService = settingsService; + this.emailService = emailService; + } + + /** + * Issues an invitation to a new user. Only admins can invite new users. An invitation token is created and sent via email. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.headers - The headers of the request. + * @property {string} req.headers.authorization - The authorization header containing the JWT token. + * @property {Object} req.body - The body of the request. + * @property {string} req.body.email - The email of the user to be invited. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message indicating the sending of the invitation, and the invitation token. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + */ + issueInvitation = async (req, res, next) => { try { - await inviteRoleValidation.validateAsync({ roles: role }); - await inviteBodyValidation.validateAsync(req.body); + // Only admins can invite + const token = getTokenFromHeaders(req.headers); + const { role, firstname, teamId } = jwt.decode(token); + req.body.teamId = teamId; + try { + await inviteRoleValidation.validateAsync({ roles: role }); + await inviteBodyValidation.validateAsync(req.body); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } + + const inviteToken = await this.db.requestInviteToken({ ...req.body }); + const { clientHost } = this.settingsService.getSettings(); + this.emailService + .buildAndSendEmail( + "employeeActivationTemplate", + { + name: firstname, + link: `${clientHost}/register/${inviteToken.token}`, + }, + req.body.email, + "Welcome to Uptime Monitor" + ) + .catch((error) => { + logger.error({ + message: error.message, + service: SERVICE_NAME, + method: "issueInvitation", + stack: error.stack, + }); + }); + + return res + .status(200) + .json({ success: true, msg: "Invite sent", data: inviteToken }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "inviteController")); + } + }; + + inviteVerifyController = async (req, res, next) => { + try { + await inviteVerificationBodyValidation.validateAsync(req.body); } catch (error) { next(handleValidationError(error, SERVICE_NAME)); return; } - const inviteToken = await req.db.requestInviteToken({ ...req.body }); - const { clientHost } = req.settingsService.getSettings(); - req.emailService - .buildAndSendEmail( - "employeeActivationTemplate", - { - name: firstname, - link: `${clientHost}/register/${inviteToken.token}`, - }, - req.body.email, - "Welcome to Uptime Monitor" - ) - .catch((error) => { - logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "issueInvitation", - stack: error.stack, - }); - }); + try { + const invite = await this.db.getInviteToken(req.body.token); + res.status(200).json({ status: "success", msg: "Invite verified", data: invite }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "inviteVerifyController")); + } + }; +} - return res.status(200).json({ success: true, msg: "Invite sent", data: inviteToken }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "inviteController")); - } -}; - -const inviteVerifyController = async (req, res, next) => { - try { - await inviteVerificationBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { - const invite = await req.db.getInviteToken(req.body.token); - res.status(200).json({ status: "success", msg: "Invite verified", data: invite }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "inviteVerifyController")); - } -}; - -export { issueInvitation, inviteVerifyController }; +export default InviteController; diff --git a/Server/controllers/maintenanceWindowController.js b/Server/controllers/maintenanceWindowController.js index 1700a483e..72b8dc27b 100644 --- a/Server/controllers/maintenanceWindowController.js +++ b/Server/controllers/maintenanceWindowController.js @@ -11,157 +11,156 @@ import jwt from "jsonwebtoken"; import { getTokenFromHeaders } from "../utils/utils.js"; import { successMessages } from "../utils/messages.js"; import { handleValidationError, handleError } from "./controllerUtils.js"; - const SERVICE_NAME = "maintenanceWindowController"; -const createMaintenanceWindows = async (req, res, next) => { - try { - await createMaintenanceWindowBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; +class MaintenanceWindowController { + constructor(db, settingsService) { + this.db = db; + this.settingsService = settingsService; } - try { - const token = getTokenFromHeaders(req.headers); - const { jwtSecret } = req.settingsService.getSettings(); - const { teamId } = jwt.verify(token, jwtSecret); - const monitorIds = req.body.monitors; - const dbTransactions = monitorIds.map((monitorId) => { - return req.db.createMaintenanceWindow({ - teamId, - monitorId, - name: req.body.name, - active: req.body.active ? req.body.active : true, - repeat: req.body.repeat, - start: req.body.start, - end: req.body.end, + + createMaintenanceWindows = async (req, res, next) => { + try { + await createMaintenanceWindowBodyValidation.validateAsync(req.body); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } + try { + const token = getTokenFromHeaders(req.headers); + const { jwtSecret } = this.settingsService.getSettings(); + const { teamId } = jwt.verify(token, jwtSecret); + const monitorIds = req.body.monitors; + const dbTransactions = monitorIds.map((monitorId) => { + return this.db.createMaintenanceWindow({ + teamId, + monitorId, + name: req.body.name, + active: req.body.active ? req.body.active : true, + repeat: req.body.repeat, + start: req.body.start, + end: req.body.end, + }); }); - }); - await Promise.all(dbTransactions); - return res.status(201).json({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_CREATE, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createMaintenanceWindow")); - } -}; + await Promise.all(dbTransactions); + return res.status(201).json({ + success: true, + msg: successMessages.MAINTENANCE_WINDOW_CREATE, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "createMaintenanceWindow")); + } + }; -const getMaintenanceWindowById = async (req, res, next) => { - try { - await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const maintenanceWindow = await req.db.getMaintenanceWindowById(req.params.id); - return res.status(200).json({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_GET_BY_ID, - data: maintenanceWindow, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMaintenanceWindowById")); - } -}; + getMaintenanceWindowById = async (req, res, next) => { + try { + await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } + try { + const maintenanceWindow = await this.db.getMaintenanceWindowById(req.params.id); + return res.status(200).json({ + success: true, + msg: successMessages.MAINTENANCE_WINDOW_GET_BY_ID, + data: maintenanceWindow, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getMaintenanceWindowById")); + } + }; -const getMaintenanceWindowsByTeamId = async (req, res, next) => { - try { - await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + getMaintenanceWindowsByTeamId = async (req, res, next) => { + try { + await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } - try { - const token = getTokenFromHeaders(req.headers); - const { jwtSecret } = req.settingsService.getSettings(); - const { teamId } = jwt.verify(token, jwtSecret); - const maintenanceWindows = await req.db.getMaintenanceWindowsByTeamId( - teamId, - req.query - ); + try { + const token = getTokenFromHeaders(req.headers); + const { jwtSecret } = this.settingsService.getSettings(); + const { teamId } = jwt.verify(token, jwtSecret); + const maintenanceWindows = await this.db.getMaintenanceWindowsByTeamId( + teamId, + req.query + ); - return res.status(200).json({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_GET_BY_TEAM, - data: maintenanceWindows, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByUserId")); - } -}; + return res.status(200).json({ + success: true, + msg: successMessages.MAINTENANCE_WINDOW_GET_BY_TEAM, + data: maintenanceWindows, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByUserId")); + } + }; -const getMaintenanceWindowsByMonitorId = async (req, res, next) => { - try { - await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + getMaintenanceWindowsByMonitorId = async (req, res, next) => { + try { + await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } - try { - const maintenanceWindows = await req.db.getMaintenanceWindowsByMonitorId( - req.params.monitorId - ); + try { + const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId( + req.params.monitorId + ); - return res.status(200).json({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_GET_BY_USER, - data: maintenanceWindows, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByMonitorId")); - } -}; + return res.status(200).json({ + success: true, + msg: successMessages.MAINTENANCE_WINDOW_GET_BY_USER, + data: maintenanceWindows, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByMonitorId")); + } + }; -const deleteMaintenanceWindow = async (req, res, next) => { - try { - await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - await req.db.deleteMaintenanceWindowById(req.params.id); - return res.status(200).json({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_DELETE, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteMaintenanceWindow")); - } -}; + deleteMaintenanceWindow = async (req, res, next) => { + try { + await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } + try { + await this.db.deleteMaintenanceWindowById(req.params.id); + return res.status(200).json({ + success: true, + msg: successMessages.MAINTENANCE_WINDOW_DELETE, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "deleteMaintenanceWindow")); + } + }; -const editMaintenanceWindow = async (req, res, next) => { - try { - await editMaintenanceWindowByIdParamValidation.validateAsync(req.params); - await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - const editedMaintenanceWindow = await req.db.editMaintenanceWindowById( - req.params.id, - req.body - ); - return res.status(200).json({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_EDIT, - data: editedMaintenanceWindow, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "editMaintenanceWindow")); - } -}; + editMaintenanceWindow = async (req, res, next) => { + try { + await editMaintenanceWindowByIdParamValidation.validateAsync(req.params); + await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } + try { + const editedMaintenanceWindow = await this.db.editMaintenanceWindowById( + req.params.id, + req.body + ); + return res.status(200).json({ + success: true, + msg: successMessages.MAINTENANCE_WINDOW_EDIT, + data: editedMaintenanceWindow, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "editMaintenanceWindow")); + } + }; +} -export { - createMaintenanceWindows, - getMaintenanceWindowById, - getMaintenanceWindowsByTeamId, - getMaintenanceWindowsByMonitorId, - deleteMaintenanceWindow, - editMaintenanceWindow, -}; +export default MaintenanceWindowController; diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 625c1a0f7..613027f53 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -14,7 +14,7 @@ import { getCertificateParamValidation, } from "../validation/joi.js"; import sslChecker from "ssl-checker"; -import { errorMessages, successMessages } from "../utils/messages.js"; +import { successMessages } from "../utils/messages.js"; import jwt from "jsonwebtoken"; import { getTokenFromHeaders } from "../utils/utils.js"; import logger from "../utils/logger.js"; @@ -23,510 +23,503 @@ import axios from "axios"; const SERVICE_NAME = "monitorController"; -/** - * Returns all monitors - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @param {function} next - * @returns {Promise} - * @throws {Error} - */ -const getAllMonitors = async (req, res, next) => { - try { - const monitors = await req.db.getAllMonitors(); - return res.status(200).json({ - success: true, - msg: successMessages.MONITOR_GET_ALL, - data: monitors, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAllMonitors")); - } -}; - -/** - * Returns all monitors with uptime stats for 1,7,30, and 90 days - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @param {function} next - * @returns {Promise} - * @throws {Error} - */ -const getAllMonitorsWithUptimeStats = async (req, res, next) => { - try { - const monitors = await req.db.getAllMonitorsWithUptimeStats(); - return res.status(200).json({ - success: true, - msg: successMessages.MONITOR_GET_ALL, - data: monitors, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAllMonitorsWithUptimeStats")); - } -}; - -/** - * Returns monitor stats for monitor with matching ID - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @param {function} next - * @returns {Promise} - * @throws {Error} - */ -const getMonitorStatsById = async (req, res, next) => { - try { - await getMonitorStatsByIdParamValidation.validateAsync(req.params); - await getMonitorStatsByIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; +class MonitorController { + constructor(db, settingsService, jobQueue) { + this.db = db; + this.settingsService = settingsService; + this.jobQueue = jobQueue; } - try { - const monitorStats = await req.db.getMonitorStatsById(req); - return res.status(200).json({ - success: true, - msg: successMessages.MONITOR_STATS_BY_ID, - data: monitorStats, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorStatsById")); - } -}; + /** + * Returns all monitors + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @param {function} next + * @returns {Promise} + * @throws {Error} + */ + getAllMonitors = async (req, res, next) => { + try { + const monitors = await this.db.getAllMonitors(); + return res.status(200).json({ + success: true, + msg: successMessages.MONITOR_GET_ALL, + data: monitors, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getAllMonitors")); + } + }; -const getMonitorCertificate = async (req, res, next, fetchMonitorCertificate) => { - try { - await getCertificateParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - } + /** + * Returns all monitors with uptime stats for 1,7,30, and 90 days + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @param {function} next + * @returns {Promise} + * @throws {Error} + */ + getAllMonitorsWithUptimeStats = async (req, res, next) => { + try { + const monitors = await this.db.getAllMonitorsWithUptimeStats(); + return res.status(200).json({ + success: true, + msg: successMessages.MONITOR_GET_ALL, + data: monitors, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getAllMonitorsWithUptimeStats")); + } + }; - try { - const { monitorId } = req.params; - const monitor = await req.db.getMonitorById(monitorId); - const certificate = await fetchMonitorCertificate(sslChecker, monitor); + /** + * Returns monitor stats for monitor with matching ID + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @param {function} next + * @returns {Promise} + * @throws {Error} + */ + getMonitorStatsById = async (req, res, next) => { + try { + await getMonitorStatsByIdParamValidation.validateAsync(req.params); + await getMonitorStatsByIdQueryValidation.validateAsync(req.query); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } - return res.status(200).json({ - success: true, - msg: successMessages.MONITOR_CERTIFICATE, - data: { - certificateDate: new Date(certificate.validTo), - }, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorCertificate")); - } -}; + try { + const monitorStats = await this.db.getMonitorStatsById(req); + return res.status(200).json({ + success: true, + msg: successMessages.MONITOR_STATS_BY_ID, + data: monitorStats, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getMonitorStatsById")); + } + }; -/** - * Retrieves a monitor by its ID. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.monitorId - The ID of the monitor to be retrieved. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message, and the retrieved monitor data. - * @throws {Error} If there is an error during the process, especially if the monitor is not found (404) or if there is a validation error (422). - */ -const getMonitorById = async (req, res, next) => { - try { - await getMonitorByIdParamValidation.validateAsync(req.params); - await getMonitorByIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + getMonitorCertificate = async (req, res, next, fetchMonitorCertificate) => { + try { + await getCertificateParamValidation.validateAsync(req.params); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + } - try { - const monitor = await req.db.getMonitorById(req.params.monitorId); - return res.status(200).json({ - success: true, - msg: successMessages.MONITOR_GET_BY_ID, - data: monitor, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorById")); - } -}; + try { + const { monitorId } = req.params; + const monitor = await this.db.getMonitorById(monitorId); + const certificate = await fetchMonitorCertificate(sslChecker, monitor); -/** - * Retrieves all monitors and a summary for a team based on the team ID. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.teamId - The ID of the team. - * @property {Object} req.query - The query parameters of the request. - * @property {string} req.query.type - The type of the request. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message, and the data containing the monitors and summary for the team. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ -const getMonitorsAndSummaryByTeamId = async (req, res, next) => { - try { - await getMonitorsAndSummaryByTeamIdParamValidation.validateAsync(req.params); - await getMonitorsAndSummaryByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + return res.status(200).json({ + success: true, + msg: successMessages.MONITOR_CERTIFICATE, + data: { + certificateDate: new Date(certificate.validTo), + }, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getMonitorCertificate")); + } + }; - try { - const { teamId } = req.params; - const { type } = req.query; - const monitorsSummary = await req.db.getMonitorsAndSummaryByTeamId(teamId, type); - return res.status(200).json({ - success: true, - msg: successMessages.MONITOR_GET_BY_USER_ID(teamId), - data: monitorsSummary, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorsAndSummaryByTeamId")); - } -}; + /** + * Retrieves a monitor by its ID. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.params - The parameters of the request. + * @property {string} req.params.monitorId - The ID of the monitor to be retrieved. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message, and the retrieved monitor data. + * @throws {Error} If there is an error during the process, especially if the monitor is not found (404) or if there is a validation error (422). + */ + getMonitorById = async (req, res, next) => { + try { + await getMonitorByIdParamValidation.validateAsync(req.params); + await getMonitorByIdQueryValidation.validateAsync(req.query); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } -/** - * Retrieves all monitors associated with a team by the team's ID. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.teamId - The ID of the team. - * @property {Object} req.query - The query parameters of the request. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message, and the data containing the monitors for the team. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ -const getMonitorsByTeamId = async (req, res, next) => { - try { - await getMonitorsByTeamIdValidation.validateAsync(req.params); - await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + try { + const monitor = await this.db.getMonitorById(req.params.monitorId); + return res.status(200).json({ + success: true, + msg: successMessages.MONITOR_GET_BY_ID, + data: monitor, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getMonitorById")); + } + }; - try { - const teamId = req.params.teamId; - const monitors = await req.db.getMonitorsByTeamId(req, res); - return res.status(200).json({ - success: true, - msg: successMessages.MONITOR_GET_BY_USER_ID(teamId), - data: monitors, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId")); - next(error); - } -}; + /** + * Retrieves all monitors and a summary for a team based on the team ID. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.params - The parameters of the request. + * @property {string} req.params.teamId - The ID of the team. + * @property {Object} req.query - The query parameters of the request. + * @property {string} req.query.type - The type of the request. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message, and the data containing the monitors and summary for the team. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + */ + getMonitorsAndSummaryByTeamId = async (req, res, next) => { + try { + await getMonitorsAndSummaryByTeamIdParamValidation.validateAsync(req.params); + await getMonitorsAndSummaryByTeamIdQueryValidation.validateAsync(req.query); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } -/** - * Creates a new monitor and adds it to the job queue. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {Array} req.body.notifications - The notifications associated with the monitor. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the creation of the monitor, and the created monitor data. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ -const createMonitor = async (req, res, next) => { - try { - await createMonitorBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + try { + const { teamId } = req.params; + const { type } = req.query; + const monitorsSummary = await this.db.getMonitorsAndSummaryByTeamId(teamId, type); + return res.status(200).json({ + success: true, + msg: successMessages.MONITOR_GET_BY_USER_ID(teamId), + data: monitorsSummary, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getMonitorsAndSummaryByTeamId")); + } + }; - try { - const notifications = req.body.notifications; - const monitor = await req.db.createMonitor(req, res); + /** + * Retrieves all monitors associated with a team by the team's ID. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.params - The parameters of the request. + * @property {string} req.params.teamId - The ID of the team. + * @property {Object} req.query - The query parameters of the request. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message, and the data containing the monitors for the team. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + */ + getMonitorsByTeamId = async (req, res, next) => { + try { + await getMonitorsByTeamIdValidation.validateAsync(req.params); + await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } - if (notifications && notifications.length > 0) { - monitor.notifications = await Promise.all( - notifications.map(async (notification) => { - notification.monitorId = monitor._id; - return await req.db.createNotification(notification); + try { + const teamId = req.params.teamId; + const monitors = await this.db.getMonitorsByTeamId(req, res); + return res.status(200).json({ + success: true, + msg: successMessages.MONITOR_GET_BY_USER_ID(teamId), + data: monitors, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId")); + next(error); + } + }; + + /** + * Creates a new monitor and adds it to the job queue. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.body - The body of the request. + * @property {Array} req.body.notifications - The notifications associated with the monitor. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message indicating the creation of the monitor, and the created monitor data. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + */ + createMonitor = async (req, res, next) => { + try { + await createMonitorBodyValidation.validateAsync(req.body); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } + + try { + const notifications = req.body.notifications; + const monitor = await this.db.createMonitor(req, res); + + if (notifications && notifications.length > 0) { + monitor.notifications = await Promise.all( + notifications.map(async (notification) => { + notification.monitorId = monitor._id; + return await this.db.createNotification(notification); + }) + ); + } + + await monitor.save(); + // Add monitor to job queue + this.jobQueue.addJob(monitor._id, monitor); + return res.status(201).json({ + success: true, + msg: successMessages.MONITOR_CREATE, + data: monitor, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "createMonitor")); + } + }; + + /** + * Checks if the endpoint can be resolved + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.query - The query parameters of the request. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message, and the resolution result. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + */ + checkEndpointResolution = async (req, res, next) => { + try { + await getMonitorURLByQueryValidation.validateAsync(req.query); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } + + try { + const { monitorURL } = req.query; + const parsedUrl = new URL(monitorURL); + const response = await axios.get(parsedUrl, { + timeout: 5000, + validateStatus: () => true, + }); + return res.status(200).json({ + success: true, + code: response.status, + statusText: response.statusText, + msg: `URL resolved successfully`, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "checkEndpointResolution")); + } + }; + + /** + * Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.params - The parameters of the request. + * @property {string} req.params.monitorId - The ID of the monitor to be deleted. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status and a message indicating the deletion of the monitor. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422) or an error in deleting associated records. + */ + deleteMonitor = async (req, res, next) => { + try { + await getMonitorByIdParamValidation.validateAsync(req.params); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } + + try { + const monitor = await this.db.deleteMonitor(req, res, next); + // Delete associated checks,alerts,and notifications + try { + await this.jobQueue.deleteJob(monitor); + await this.db.deleteChecks(monitor._id); + await this.db.deletePageSpeedChecksByMonitorId(monitor._id); + await this.db.deleteNotificationsByMonitorId(monitor._id); + await this.db.deleteHardwareChecksByMonitorId(monitor._id); + } catch (error) { + logger.error({ + message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`, + service: SERVICE_NAME, + method: "deleteMonitor", + stack: error.stack, + }); + } + return res.status(200).json({ success: true, msg: successMessages.MONITOR_DELETE }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "deleteMonitor")); + } + }; + + /** + * Deletes all monitors associated with a team. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.headers - The headers of the request. + * @property {string} req.headers.authorization - The authorization header containing the JWT token. + * @param {Object} res - The Express response object. + * @param {function} next + * @returns {Object} The response object with a success status and a message indicating the number of deleted monitors. + * @throws {Error} If there is an error during the deletion process. + */ + deleteAllMonitors = async (req, res, next) => { + try { + const token = getTokenFromHeaders(req.headers); + const { jwtSecret } = this.settingsService.getSettings(); + const { teamId } = jwt.verify(token, jwtSecret); + const { monitors, deletedCount } = await this.db.deleteAllMonitors(teamId); + await Promise.all( + monitors.map(async (monitor) => { + try { + await this.jobQueue.deleteJob(monitor); + await this.db.deleteChecks(monitor._id); + await this.db.deletePageSpeedChecksByMonitorId(monitor._id); + await this.db.deleteNotificationsByMonitorId(monitor._id); + } catch (error) { + logger.error({ + message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`, + service: SERVICE_NAME, + method: "deleteAllMonitors", + stack: error.stack, + }); + } }) ); - } - - await monitor.save(); - // Add monitor to job queue - req.jobQueue.addJob(monitor._id, monitor); - return res.status(201).json({ - success: true, - msg: successMessages.MONITOR_CREATE, - data: monitor, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createMonitor")); - } -}; - -/** - * Checks if the endpoint can be resolved - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.query - The query parameters of the request. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message, and the resolution result. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ -const checkEndpointResolution = async (req, res, next) => { - try { - await getMonitorURLByQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { - const { monitorURL } = req.query; - const parsedUrl = new URL(monitorURL); - const response = await axios.get(parsedUrl, { - timeout: 5000, - validateStatus: () => true, - }); - return res.status(200).json({ - success: true, - code: response.status, - statusText: response.statusText, - msg: `URL resolved successfully`, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "checkEndpointResolution")); - } -}; - -/** - * Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.monitorId - The ID of the monitor to be deleted. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status and a message indicating the deletion of the monitor. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422) or an error in deleting associated records. - */ -const deleteMonitor = async (req, res, next) => { - try { - await getMonitorByIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { - const monitor = await req.db.deleteMonitor(req, res, next); - // Delete associated checks,alerts,and notifications - try { - await req.jobQueue.deleteJob(monitor); - await req.db.deleteChecks(monitor._id); - await req.db.deletePageSpeedChecksByMonitorId(monitor._id); - await req.db.deleteNotificationsByMonitorId(monitor._id); - await req.db.deleteHardwareChecksByMonitorId(monitor._id); + return res + .status(200) + .json({ success: true, msg: `Deleted ${deletedCount} monitors` }); } catch (error) { - logger.error({ - message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`, - service: SERVICE_NAME, - method: "deleteMonitor", - stack: error.stack, - }); + next(handleError(error, SERVICE_NAME, "deleteAllMonitors")); } - return res.status(200).json({ success: true, msg: successMessages.MONITOR_DELETE }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteMonitor")); - } -}; + }; -/** - * Deletes all monitors associated with a team. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.headers - The headers of the request. - * @property {string} req.headers.authorization - The authorization header containing the JWT token. - * @param {Object} res - The Express response object. - * @param {function} next - * @returns {Object} The response object with a success status and a message indicating the number of deleted monitors. - * @throws {Error} If there is an error during the deletion process. - */ -const deleteAllMonitors = async (req, res, next) => { - try { - const token = getTokenFromHeaders(req.headers); - const { jwtSecret } = req.settingsService.getSettings(); - const { teamId } = jwt.verify(token, jwtSecret); - const { monitors, deletedCount } = await req.db.deleteAllMonitors(teamId); - await Promise.all( - monitors.map(async (monitor) => { - try { - await req.jobQueue.deleteJob(monitor); - await req.db.deleteChecks(monitor._id); - await req.db.deletePageSpeedChecksByMonitorId(monitor._id); - await req.db.deleteNotificationsByMonitorId(monitor._id); - } catch (error) { - logger.error({ - message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`, - service: SERVICE_NAME, - method: "deleteAllMonitors", - stack: error.stack, - }); - } - }) - ); - return res - .status(200) - .json({ success: true, msg: `Deleted ${deletedCount} monitors` }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteAllMonitors")); - } -}; + /** + * Edits a monitor by its ID, updates its notifications, and updates its job in the job queue. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.params - The parameters of the request. + * @property {string} req.params.monitorId - The ID of the monitor to be edited. + * @property {Object} req.body - The body of the request. + * @property {Array} req.body.notifications - The notifications to be associated with the monitor. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message indicating the editing of the monitor, and the edited monitor data. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + */ + editMonitor = async (req, res, next) => { + try { + await getMonitorByIdParamValidation.validateAsync(req.params); + await editMonitorBodyValidation.validateAsync(req.body); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } -/** - * Edits a monitor by its ID, updates its notifications, and updates its job in the job queue. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.monitorId - The ID of the monitor to be edited. - * @property {Object} req.body - The body of the request. - * @property {Array} req.body.notifications - The notifications to be associated with the monitor. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the editing of the monitor, and the edited monitor data. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ -const editMonitor = async (req, res, next) => { - try { - await getMonitorByIdParamValidation.validateAsync(req.params); - await editMonitorBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + try { + const { monitorId } = req.params; + const monitorBeforeEdit = await this.db.getMonitorById(monitorId); - try { - const { monitorId } = req.params; - const monitorBeforeEdit = await req.db.getMonitorById(monitorId); + // Get notifications from the request body + const notifications = req.body.notifications; - // Get notifications from the request body - const notifications = req.body.notifications; + const editedMonitor = await this.db.editMonitor(monitorId, req.body); - const editedMonitor = await req.db.editMonitor(monitorId, req.body); + await this.db.deleteNotificationsByMonitorId(editedMonitor._id); - await req.db.deleteNotificationsByMonitorId(editedMonitor._id); + await Promise.all( + notifications && + notifications.map(async (notification) => { + notification.monitorId = editedMonitor._id; + await this.db.createNotification(notification); + }) + ); - await Promise.all( - notifications && - notifications.map(async (notification) => { - notification.monitorId = editedMonitor._id; - await req.db.createNotification(notification); - }) - ); + // Delete the old job(editedMonitor has the same ID as the old monitor) + await this.jobQueue.deleteJob(monitorBeforeEdit); + // Add the new job back to the queue + await this.jobQueue.addJob(editedMonitor._id, editedMonitor); + return res.status(200).json({ + success: true, + msg: successMessages.MONITOR_EDIT, + data: editedMonitor, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "editMonitor")); + } + }; - // Delete the old job(editedMonitor has the same ID as the old monitor) - await req.jobQueue.deleteJob(monitorBeforeEdit); - // Add the new job back to the queue - await req.jobQueue.addJob(editedMonitor._id, editedMonitor); - return res.status(200).json({ - success: true, - msg: successMessages.MONITOR_EDIT, - data: editedMonitor, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "editMonitor")); - } -}; + /** + * Pauses or resumes a monitor based on its current state. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.params - The parameters of the request. + * @property {string} req.params.monitorId - The ID of the monitor to be paused or resumed. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message indicating the new state of the monitor, and the updated monitor data. + * @throws {Error} If there is an error during the process. + */ + pauseMonitor = async (req, res, next) => { + try { + await pauseMonitorParamValidation.validateAsync(req.params); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + } -/** - * Pauses or resumes a monitor based on its current state. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.monitorId - The ID of the monitor to be paused or resumed. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the new state of the monitor, and the updated monitor data. - * @throws {Error} If there is an error during the process. - */ -const pauseMonitor = async (req, res, next) => { - try { - await pauseMonitorParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - } + try { + const monitor = await this.db.getMonitorById(req.params.monitorId); + monitor.isActive === true + ? await this.jobQueue.deleteJob(monitor) + : await this.jobQueue.addJob(monitor._id, monitor); - try { - const monitor = await req.db.getMonitorById(req.params.monitorId); - monitor.isActive === true - ? await req.jobQueue.deleteJob(monitor) - : await req.jobQueue.addJob(monitor._id, monitor); + monitor.isActive = !monitor.isActive; + monitor.status = undefined; + monitor.save(); + return res.status(200).json({ + success: true, + msg: monitor.isActive + ? successMessages.MONITOR_RESUME + : successMessages.MONITOR_PAUSE, + data: monitor, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "pauseMonitor")); + } + }; - monitor.isActive = !monitor.isActive; - monitor.status = undefined; - monitor.save(); - return res.status(200).json({ - success: true, - msg: monitor.isActive - ? successMessages.MONITOR_RESUME - : successMessages.MONITOR_PAUSE, - data: monitor, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "pauseMonitor")); - } -}; + /** + * Adds demo monitors for a team. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.headers - The headers of the request. + * @property {string} req.headers.authorization - The authorization header containing the JWT token. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message indicating the addition of demo monitors, and the number of demo monitors added. + * @throws {Error} If there is an error during the process. + */ + addDemoMonitors = async (req, res, next) => { + try { + const token = getTokenFromHeaders(req.headers); + const { jwtSecret } = this.settingsService.getSettings(); + const { _id, teamId } = jwt.verify(token, jwtSecret); + const demoMonitors = await this.db.addDemoMonitors(_id, teamId); + await Promise.all( + demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor)) + ); -/** - * Adds demo monitors for a team. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.headers - The headers of the request. - * @property {string} req.headers.authorization - The authorization header containing the JWT token. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the addition of demo monitors, and the number of demo monitors added. - * @throws {Error} If there is an error during the process. - */ -const addDemoMonitors = async (req, res, next) => { - try { - const token = getTokenFromHeaders(req.headers); - const { jwtSecret } = req.settingsService.getSettings(); - const { _id, teamId } = jwt.verify(token, jwtSecret); - const demoMonitors = await req.db.addDemoMonitors(_id, teamId); - await Promise.all( - demoMonitors.map((monitor) => req.jobQueue.addJob(monitor._id, monitor)) - ); + return res.status(200).json({ + success: true, + msg: successMessages.MONITOR_DEMO_ADDED, + data: demoMonitors.length, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "addDemoMonitors")); + } + }; +} - return res.status(200).json({ - success: true, - msg: successMessages.MONITOR_DEMO_ADDED, - data: demoMonitors.length, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "addDemoMonitors")); - } -}; - -export { - getAllMonitors, - getAllMonitorsWithUptimeStats, - getMonitorStatsById, - getMonitorCertificate, - getMonitorById, - getMonitorsAndSummaryByTeamId, - getMonitorsByTeamId, - createMonitor, - checkEndpointResolution, - deleteMonitor, - deleteAllMonitors, - editMonitor, - pauseMonitor, - addDemoMonitors, -}; +export default MonitorController; diff --git a/Server/controllers/queueController.js b/Server/controllers/queueController.js index 5e525de58..501074d42 100644 --- a/Server/controllers/queueController.js +++ b/Server/controllers/queueController.js @@ -1,57 +1,64 @@ import { handleError } from "./controllerUtils.js"; -import { errorMessages, successMessages } from "../utils/messages.js"; +import { successMessages } from "../utils/messages.js"; const SERVICE_NAME = "JobQueueController"; -const getMetrics = async (req, res, next) => { - try { - const metrics = await req.jobQueue.getMetrics(); - res.status(200).json({ - success: true, - msg: successMessages.QUEUE_GET_METRICS, - data: metrics, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMetrics")); - return; +class JobQueueController { + constructor(jobQueue) { + this.jobQueue = jobQueue; } -}; -const getJobs = async (req, res, next) => { - try { - const jobs = await req.jobQueue.getJobStats(); - return res.status(200).json({ - success: true, - msg: successMessages.QUEUE_GET_METRICS, - data: jobs, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getJobs")); - return; - } -}; + getMetrics = async (req, res, next) => { + try { + const metrics = await this.jobQueue.getMetrics(); + res.status(200).json({ + success: true, + msg: successMessages.QUEUE_GET_METRICS, + data: metrics, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getMetrics")); + return; + } + }; -const addJob = async (req, res, next) => { - try { - await req.jobQueue.addJob(Math.random().toString(36).substring(7)); - return res.status(200).json({ - success: true, - msg: successMessages.QUEUE_ADD_JOB, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "addJob")); - return; - } -}; + getJobs = async (req, res, next) => { + try { + const jobs = await this.jobQueue.getJobStats(); + return res.status(200).json({ + success: true, + msg: successMessages.QUEUE_GET_METRICS, + data: jobs, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getJobs")); + return; + } + }; -const obliterateQueue = async (req, res, next) => { - try { - await req.jobQueue.obliterate(); - return res.status(200).json({ success: true, msg: successMessages.QUEUE_OBLITERATE }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "obliterateQueue")); - return; - } -}; + addJob = async (req, res, next) => { + try { + await this.jobQueue.addJob(Math.random().toString(36).substring(7)); + return res.status(200).json({ + success: true, + msg: successMessages.QUEUE_ADD_JOB, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "addJob")); + return; + } + }; -export { getMetrics, getJobs, addJob, obliterateQueue }; + obliterateQueue = async (req, res, next) => { + try { + await this.jobQueue.obliterate(); + return res + .status(200) + .json({ success: true, msg: successMessages.QUEUE_OBLITERATE }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "obliterateQueue")); + return; + } + }; +} +export default JobQueueController; diff --git a/Server/controllers/settingsController.js b/Server/controllers/settingsController.js index 3e232ac7a..06a446dec 100644 --- a/Server/controllers/settingsController.js +++ b/Server/controllers/settingsController.js @@ -3,40 +3,47 @@ import { updateAppSettingsBodyValidation } from "../validation/joi.js"; import { handleValidationError, handleError } from "./controllerUtils.js"; const SERVICE_NAME = "SettingsController"; -const getAppSettings = async (req, res, next) => { - try { - const settings = { ...(await req.settingsService.getSettings()) }; - delete settings.jwtSecret; - return res.status(200).json({ - success: true, - msg: successMessages.GET_APP_SETTINGS, - data: settings, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAppSettings")); - } -}; - -const updateAppSettings = async (req, res, next) => { - try { - await updateAppSettingsBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; +class SettingsController { + constructor(db, settingsService) { + this.db = db; + this.settingsService = settingsService; } - try { - await req.db.updateAppSettings(req.body); - const updatedSettings = { ...(await req.settingsService.reloadSettings()) }; - delete updatedSettings.jwtSecret; - return res.status(200).json({ - success: true, - msg: successMessages.UPDATE_APP_SETTINGS, - data: updatedSettings, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "updateAppSettings")); - } -}; + getAppSettings = async (req, res, next) => { + try { + const settings = { ...(await this.settingsService.getSettings()) }; + delete settings.jwtSecret; + return res.status(200).json({ + success: true, + msg: successMessages.GET_APP_SETTINGS, + data: settings, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getAppSettings")); + } + }; -export { getAppSettings, updateAppSettings }; + updateAppSettings = async (req, res, next) => { + try { + await updateAppSettingsBodyValidation.validateAsync(req.body); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } + + try { + await this.db.updateAppSettings(req.body); + const updatedSettings = { ...(await this.settingsService.reloadSettings()) }; + delete updatedSettings.jwtSecret; + return res.status(200).json({ + success: true, + msg: successMessages.UPDATE_APP_SETTINGS, + data: updatedSettings, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "updateAppSettings")); + } + }; +} + +export default SettingsController; diff --git a/Server/controllers/statusPageController.js b/Server/controllers/statusPageController.js index facac7c4a..bbb7a18ee 100644 --- a/Server/controllers/statusPageController.js +++ b/Server/controllers/statusPageController.js @@ -7,45 +7,50 @@ import { successMessages } from "../utils/messages.js"; const SERVICE_NAME = "statusPageController"; -const createStatusPage = async (req, res, next) => { - try { - await createStatusPageBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; +class StatusPageController { + constructor(db) { + this.db = db; } - try { - const statusPage = await req.db.createStatusPage(req.body); - return res.status(200).json({ - success: true, - msg: successMessages.STATUS_PAGE_CREATE, - data: statusPage, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createStatusPage")); - } -}; + createStatusPage = async (req, res, next) => { + try { + await createStatusPageBodyValidation.validateAsync(req.body); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } -const getStatusPageByUrl = async (req, res, next) => { - try { - await getStatusPageParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + try { + const statusPage = await this.db.createStatusPage(req.body); + return res.status(200).json({ + success: true, + msg: successMessages.STATUS_PAGE_CREATE, + data: statusPage, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "createStatusPage")); + } + }; + getStatusPageByUrl = async (req, res, next) => { + try { + await getStatusPageParamValidation.validateAsync(req.params); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } - try { - const { url } = req.params; - const statusPage = await req.db.getStatusPageByUrl(url); - return res.status(200).json({ - success: true, - msg: successMessages.STATUS_PAGE_BY_URL, - data: statusPage, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getStatusPage")); - } -}; + try { + const { url } = req.params; + const statusPage = await this.db.getStatusPageByUrl(url); + return res.status(200).json({ + success: true, + msg: successMessages.STATUS_PAGE_BY_URL, + data: statusPage, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getStatusPage")); + } + }; +} -export { createStatusPage, getStatusPageByUrl }; +export default StatusPageController; From 0861595d39c158da3606032d25b3f57d1356e2a3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 24 Dec 2024 11:58:56 -0800 Subject: [PATCH 10/32] Refactor routers to be class based --- Server/routes/authRoute.js | 74 +++++++++++------ Server/routes/checkRoute.js | 52 ++++++++---- Server/routes/inviteRoute.js | 29 +++++-- Server/routes/maintenanceWindowRoute.js | 46 ++++++----- Server/routes/monitorRoute.js | 104 +++++++++++++++--------- Server/routes/queueRoute.js | 34 ++++---- Server/routes/settingsRoute.js | 25 ++++-- Server/routes/statusPageRoute.js | 31 ++++--- 8 files changed, 255 insertions(+), 140 deletions(-) diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index e19f855db..cebe9a211 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -5,34 +5,56 @@ import { isAllowed } from "../middleware/isAllowed.js"; import multer from "multer"; import User from "../db/models/User.js"; -const router = Router(); const upload = multer(); -import { - registerUser, - loginUser, - refreshAuthToken, - editUser, - requestRecovery, - validateRecovery, - resetPassword, - checkSuperadminExists, - getAllUsers, - deleteUser, -} from "../controllers/authController.js"; +class AuthRoutes { + constructor(authController) { + this.router = Router(); + this.authController = authController; + this.initRoutes(); + } -//Auth routes -router.post("/register", upload.single("profileImage"), registerUser); -router.post("/login", loginUser); -router.post("/refresh", refreshAuthToken); -router.put("/user/:userId", upload.single("profileImage"), verifyJWT, editUser); -router.get("/users/superadmin", checkSuperadminExists); -router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), getAllUsers); -router.delete("/user/:userId", verifyJWT, verifyOwnership(User, "userId"), deleteUser); + initRoutes() { + this.router.post( + "/register", + upload.single("profileImage"), + this.authController.registerUser + ); + this.router.post("/login", this.authController.loginUser); + this.router.post("/refresh", this.authController.refreshAuthToken); -//Recovery routes -router.post("/recovery/request", requestRecovery); -router.post("/recovery/validate", validateRecovery); -router.post("/recovery/reset/", resetPassword); + this.router.put( + "/user/:userId", + upload.single("profileImage"), + verifyJWT, + this.authController.editUser + ); -export default router; + this.router.get("/users/superadmin", this.authController.checkSuperadminExists); + + this.router.get( + "/users", + verifyJWT, + isAllowed(["admin", "superadmin"]), + this.authController.getAllUsers + ); + + this.router.delete( + "/user/:userId", + verifyJWT, + verifyOwnership(User, "userId"), + this.authController.deleteUser + ); + + // Recovery routes + this.router.post("/recovery/request", this.authController.requestRecovery); + this.router.post("/recovery/validate", this.authController.validateRecovery); + this.router.post("/recovery/reset/", this.authController.resetPassword); + } + + getRouter() { + return this.router; + } +} + +export default AuthRoutes; diff --git a/Server/routes/checkRoute.js b/Server/routes/checkRoute.js index a57b7410d..289e0bc2f 100644 --- a/Server/routes/checkRoute.js +++ b/Server/routes/checkRoute.js @@ -1,26 +1,46 @@ import { Router } from "express"; -import { - createCheck, - getChecks, - deleteChecks, - getTeamChecks, - deleteChecksByTeamId, - updateChecksTTL, -} from "../controllers/checkController.js"; import { verifyOwnership } from "../middleware/verifyOwnership.js"; import { isAllowed } from "../middleware/isAllowed.js"; import Monitor from "../db/models/Monitor.js"; -const router = Router(); +class CheckRoutes { + constructor(checkController) { + this.router = Router(); + this.checkController = checkController; + this.initRoutes(); + } -router.get("/:monitorId", getChecks); -router.post("/:monitorId", verifyOwnership(Monitor, "monitorId"), createCheck); -router.delete("/:monitorId", verifyOwnership(Monitor, "monitorId"), deleteChecks); + initRoutes() { + this.router.get("/:monitorId", this.checkController.getChecks); + this.router.post( + "/:monitorId", + verifyOwnership(Monitor, "monitorId"), + this.checkController.createCheck + ); + this.router.delete( + "/:monitorId", + verifyOwnership(Monitor, "monitorId"), + this.checkController.deleteChecks + ); -router.get("/team/:teamId", getTeamChecks); + this.router.get("/team/:teamId", this.checkController.getTeamChecks); -router.delete("/team/:teamId", isAllowed(["admin", "superadmin"]), deleteChecksByTeamId); + this.router.delete( + "/team/:teamId", + isAllowed(["admin", "superadmin"]), + this.checkController.deleteChecksByTeamId + ); -router.put("/team/ttl", isAllowed(["admin", "superadmin"]), updateChecksTTL); + this.router.put( + "/team/ttl", + isAllowed(["admin", "superadmin"]), + this.checkController.updateChecksTTL + ); + } -export default router; + getRouter() { + return this.router; + } +} + +export default CheckRoutes; diff --git a/Server/routes/inviteRoute.js b/Server/routes/inviteRoute.js index 36d30de5a..c66c573f1 100644 --- a/Server/routes/inviteRoute.js +++ b/Server/routes/inviteRoute.js @@ -1,14 +1,27 @@ import { Router } from "express"; import { verifyJWT } from "../middleware/verifyJWT.js"; import { isAllowed } from "../middleware/isAllowed.js"; -import { - issueInvitation, - inviteVerifyController, -} from "../controllers/inviteController.js"; -const router = Router(); +class InviteRoutes { + constructor(inviteController) { + this.router = Router(); + this.inviteController = inviteController; + this.initRoutes(); + } -router.post("/", isAllowed(["admin", "superadmin"]), verifyJWT, issueInvitation); -router.post("/verify", inviteVerifyController); + initRoutes() { + this.router.post( + "/", + isAllowed(["admin", "superadmin"]), + verifyJWT, + this.inviteController.issueInvitation + ); + this.router.post("/verify", this.inviteController.inviteVerifyController); + } -export default router; + getRouter() { + return this.router; + } +} + +export default InviteRoutes; diff --git a/Server/routes/maintenanceWindowRoute.js b/Server/routes/maintenanceWindowRoute.js index e21e4b9ec..2665c2bb8 100644 --- a/Server/routes/maintenanceWindowRoute.js +++ b/Server/routes/maintenanceWindowRoute.js @@ -1,31 +1,37 @@ import { Router } from "express"; -import { - createMaintenanceWindows, - getMaintenanceWindowById, - getMaintenanceWindowsByTeamId, - getMaintenanceWindowsByMonitorId, - deleteMaintenanceWindow, - editMaintenanceWindow, -} from "../controllers/maintenanceWindowController.js"; import { verifyOwnership } from "../middleware/verifyOwnership.js"; import Monitor from "../db/models/Monitor.js"; -const router = Router(); +class MaintenanceWindowRoutes { + constructor(maintenanceWindowController) { + this.router = Router(); + this.maintenanceWindowController = maintenanceWindowController; + this.initRoutes(); + } + initRoutes() { + this.router.post("/", this.maintenanceWindowController.createMaintenanceWindows); -router.post("/", createMaintenanceWindows); + this.router.get( + "/monitor/:monitorId", + verifyOwnership(Monitor, "monitorId"), + this.maintenanceWindowController.getMaintenanceWindowsByMonitorId + ); -router.get( - "/monitor/:monitorId", - verifyOwnership(Monitor, "monitorId"), - getMaintenanceWindowsByMonitorId -); + this.router.get( + "/team/", + this.maintenanceWindowController.getMaintenanceWindowsByTeamId + ); -router.get("/team/", getMaintenanceWindowsByTeamId); + this.router.get("/:id", this.maintenanceWindowController.getMaintenanceWindowById); -router.get("/:id", getMaintenanceWindowById); + this.router.put("/:id", this.maintenanceWindowController.editMaintenanceWindow); -router.put("/:id", editMaintenanceWindow); + this.router.delete("/:id", this.maintenanceWindowController.deleteMaintenanceWindow); + } -router.delete("/:id", deleteMaintenanceWindow); + getRouter() { + return this.router; + } +} -export default router; +export default MaintenanceWindowRoutes; diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index 056d81ed6..c3257372b 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -1,51 +1,79 @@ import { Router } from "express"; -import { - getAllMonitors, - getAllMonitorsWithUptimeStats, - getMonitorStatsById, - getMonitorCertificate, - getMonitorById, - getMonitorsAndSummaryByTeamId, - getMonitorsByTeamId, - createMonitor, - checkEndpointResolution, - deleteMonitor, - deleteAllMonitors, - editMonitor, - pauseMonitor, - addDemoMonitors, -} from "../controllers/monitorController.js"; import { isAllowed } from "../middleware/isAllowed.js"; import { fetchMonitorCertificate } from "../controllers/controllerUtils.js"; -const router = Router(); +class MonitorRoutes { + constructor(monitorController) { + this.router = Router(); + this.monitorController = monitorController; + this.initRoutes(); + } -router.get("/", getAllMonitors); -router.get("/uptime", getAllMonitorsWithUptimeStats); -router.get("/stats/:monitorId", getMonitorStatsById); -router.get("/certificate/:monitorId", (req, res, next) => { - getMonitorCertificate(req, res, next, fetchMonitorCertificate); -}); -router.get("/:monitorId", getMonitorById); -router.get("/team/summary/:teamId", getMonitorsAndSummaryByTeamId); -router.get("/team/:teamId", getMonitorsByTeamId); + initRoutes() { + this.router.get("/", this.monitorController.getAllMonitors); + this.router.get("/uptime", this.monitorController.getAllMonitorsWithUptimeStats); + this.router.get("/stats/:monitorId", this.monitorController.getMonitorStatsById); + this.router.get("/certificate/:monitorId", (req, res, next) => { + this.monitorController.getMonitorCertificate( + req, + res, + next, + fetchMonitorCertificate + ); + }); + this.router.get("/:monitorId", this.monitorController.getMonitorById); + this.router.get( + "/team/summary/:teamId", + this.monitorController.getMonitorsAndSummaryByTeamId + ); + this.router.get("/team/:teamId", this.monitorController.getMonitorsByTeamId); -router.get( - "/resolution/url", - isAllowed(["admin", "superadmin"]), - checkEndpointResolution -); + this.router.get( + "/resolution/url", + isAllowed(["admin", "superadmin"]), + this.monitorController.checkEndpointResolution + ); -router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), deleteMonitor); + this.router.delete( + "/:monitorId", + isAllowed(["admin", "superadmin"]), + this.monitorController.deleteMonitor + ); -router.post("/", isAllowed(["admin", "superadmin"]), createMonitor); + this.router.post( + "/", + isAllowed(["admin", "superadmin"]), + this.monitorController.createMonitor + ); -router.put("/:monitorId", isAllowed(["admin", "superadmin"]), editMonitor); + this.router.put( + "/:monitorId", + isAllowed(["admin", "superadmin"]), + this.monitorController.editMonitor + ); -router.delete("/", isAllowed(["superadmin"]), deleteAllMonitors); + this.router.delete( + "/", + isAllowed(["superadmin"]), + this.monitorController.deleteAllMonitors + ); -router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), pauseMonitor); + this.router.post( + "/pause/:monitorId", + isAllowed(["admin", "superadmin"]), + this.monitorController.pauseMonitor + ); -router.post("/demo", isAllowed(["admin", "superadmin"]), addDemoMonitors); + this.router.post( + "/demo", + isAllowed(["admin", "superadmin"]), + this.monitorController.addDemoMonitors + ); + } -export default router; + getRouter() { + return this.router; + } +} + +export default MonitorRoutes; diff --git a/Server/routes/queueRoute.js b/Server/routes/queueRoute.js index addd9dab8..af0e9d08e 100644 --- a/Server/routes/queueRoute.js +++ b/Server/routes/queueRoute.js @@ -1,21 +1,21 @@ import { Router } from "express"; -import { - getMetrics, - getJobs, - addJob, - obliterateQueue, -} from "../controllers/queueController.js"; -const router = Router(); -router.get("/metrics", getMetrics); +class QueueRoutes { + constructor(queueController) { + this.router = Router(); + this.queueController = queueController; + this.initRoutes(); + } + initRoutes() { + this.router.get("/metrics", this.queueController.getMetrics); + this.router.get("/jobs", this.queueController.getJobs); + this.router.post("/jobs", this.queueController.addJob); + this.router.post("/obliterate", this.queueController.obliterateQueue); + } -// Get Jobs -router.get("/jobs", getJobs); + getRouter() { + return this.router; + } +} -// Add Job -router.post("/jobs", addJob); - -// Obliterate Queue -router.post("/obliterate", obliterateQueue); - -export default router; +export default QueueRoutes; diff --git a/Server/routes/settingsRoute.js b/Server/routes/settingsRoute.js index 1b8dc19ad..8da262427 100644 --- a/Server/routes/settingsRoute.js +++ b/Server/routes/settingsRoute.js @@ -1,10 +1,25 @@ import { Router } from "express"; -import { getAppSettings, updateAppSettings } from "../controllers/settingsController.js"; import { isAllowed } from "../middleware/isAllowed.js"; -const router = Router(); +class SettingsRoutes { + constructor(settingsController) { + this.router = Router(); + this.settingsController = settingsController; + this.initRoutes(); + } -router.get("/", getAppSettings); -router.put("/", isAllowed(["superadmin"]), updateAppSettings); + initRoutes() { + this.router.get("/", this.settingsController.getAppSettings); + this.router.put( + "/", + isAllowed(["superadmin"]), + this.settingsController.updateAppSettings + ); + } -export default router; + getRouter() { + return this.router; + } +} + +export default SettingsRoutes; diff --git a/Server/routes/statusPageRoute.js b/Server/routes/statusPageRoute.js index 6300723f4..ff2a2018a 100644 --- a/Server/routes/statusPageRoute.js +++ b/Server/routes/statusPageRoute.js @@ -1,11 +1,22 @@ -import express from "express"; -import { verifyJWT } from "../middleware/verifyJWT.js"; -import { - createStatusPage, - getStatusPageByUrl, -} from "../controllers/statusPageController.js"; -const router = express.Router(); +import { Router } from "express"; -router.get("/:url", getStatusPageByUrl); -router.post("/:url", verifyJWT, createStatusPage); -export default router; +import { verifyJWT } from "../middleware/verifyJWT.js"; + +class StatusPageRoutes { + constructor(statusPageController) { + this.router = Router(); + this.statusPageController = statusPageController; + this.initRoutes(); + } + + initRoutes() { + this.router.get("/:url", this.statusPageController.getStatusPageByUrl); + this.router.post("/:url", verifyJWT, this.statusPageController.createStatusPage); + } + + getRouter() { + return this.router; + } +} + +export default StatusPageRoutes; From 7f6e0964918426fd7162bf5f8623364a5854eaac Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 24 Dec 2024 11:59:18 -0800 Subject: [PATCH 11/32] refacgtor MongoDB to be class based --- Server/db/mongo/MongoDB.js | 257 +++++++++++-------------------------- 1 file changed, 76 insertions(+), 181 deletions(-) diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index 3bdde7d69..882c862e0 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -2,234 +2,129 @@ import mongoose from "mongoose"; import UserModel from "../models/User.js"; import AppSettings from "../models/AppSettings.js"; import logger from "../../utils/logger.js"; -const SERVICE_NAME = "MongoDB"; - -//**************************************** -// DB Connection -//**************************************** - -const connect = async () => { - try { - const connectionString = - process.env.DB_CONNECTION_STRING || "mongodb://localhost:27017/uptime_db"; - await mongoose.connect(connectionString); - // If there are no AppSettings, create one - let appSettings = await AppSettings.find(); - if (appSettings.length === 0) { - appSettings = new AppSettings({}); - await appSettings.save(); - } - logger.info({ message: "Connected to MongoDB" }); - } catch (error) { - logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "connect", - stack: error.stack, - }); - throw error; - } -}; - -const disconnect = async () => { - try { - logger.info({ message: "Disconnecting from MongoDB" }); - await mongoose.disconnect(); - logger.info({ message: "Disconnected from MongoDB" }); - return; - } catch (error) { - logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "disconnect", - stack: error.stack, - }); - } -}; - -const checkSuperadmin = async (req, res) => { - try { - const superAdmin = await UserModel.findOne({ role: "superadmin" }); - if (superAdmin !== null) { - return true; - } - return false; - } catch (error) { - throw error; - } -}; //**************************************** // User Operations //**************************************** -import { - insertUser, - getUserByEmail, - updateUser, - deleteUser, - deleteTeam, - deleteAllOtherUsers, - getAllUsers, - logoutUser, -} from "./modules/userModule.js"; +import * as userModule from "./modules/userModule.js"; //**************************************** // Invite Token Operations //**************************************** -import { - requestInviteToken, - getInviteToken, - getInviteTokenAndDelete, -} from "./modules/inviteModule.js"; +import * as inviteModule from "./modules/inviteModule.js"; //**************************************** // Recovery Operations //**************************************** -import { - requestRecoveryToken, - validateRecoveryToken, - resetPassword, -} from "./modules/recoveryModule.js"; +import * as recoveryModule from "./modules/recoveryModule.js"; //**************************************** // Monitors //**************************************** -import { - getAllMonitors, - getAllMonitorsWithUptimeStats, - getMonitorStatsById, - getMonitorById, - getMonitorsAndSummaryByTeamId, - getMonitorsByTeamId, - createMonitor, - deleteMonitor, - deleteAllMonitors, - deleteMonitorsByUserId, - editMonitor, - addDemoMonitors, -} from "./modules/monitorModule.js"; +import * as monitorModule from "./modules/monitorModule.js"; //**************************************** // Page Speed Checks //**************************************** -import { - createPageSpeedCheck, - deletePageSpeedChecksByMonitorId, -} from "./modules/pageSpeedCheckModule.js"; +import * as pageSpeedCheckModule from "./modules/pageSpeedCheckModule.js"; //**************************************** // Hardware Checks //**************************************** -import { - createHardwareCheck, - deleteHardwareChecksByMonitorId, -} from "./modules/hardwareCheckModule.js"; +import * as hardwareCheckModule from "./modules/hardwareCheckModule.js"; //**************************************** // Checks //**************************************** -import { - createCheck, - getChecksCount, - getChecks, - getTeamChecks, - deleteChecks, - deleteChecksByTeamId, - updateChecksTTL, -} from "./modules/checkModule.js"; +import * as checkModule from "./modules/checkModule.js"; //**************************************** // Maintenance Window //**************************************** -import { - createMaintenanceWindow, - getMaintenanceWindowById, - getMaintenanceWindowsByTeamId, - getMaintenanceWindowsByMonitorId, - deleteMaintenanceWindowById, - deleteMaintenanceWindowByMonitorId, - deleteMaintenanceWindowByUserId, - editMaintenanceWindowById, -} from "./modules/maintenanceWindowModule.js"; +import * as maintenanceWindowModule from "./modules/maintenanceWindowModule.js"; //**************************************** // Notifications //**************************************** -import { - createNotification, - getNotificationsByMonitorId, - deleteNotificationsByMonitorId, -} from "./modules/notificationModule.js"; +import * as notificationModule from "./modules/notificationModule.js"; //**************************************** // AppSettings //**************************************** -import { getAppSettings, updateAppSettings } from "./modules/settingsModule.js"; +import * as settingsModule from "./modules/settingsModule.js"; //**************************************** // Status Page //**************************************** -import { createStatusPage, getStatusPageByUrl } from "./modules/statusPageModule.js"; +import * as statusPageModule from "./modules/statusPageModule.js"; -export default { - connect, - disconnect, - insertUser, - getUserByEmail, - updateUser, - deleteUser, - deleteTeam, - deleteAllOtherUsers, - getAllUsers, - logoutUser, - requestInviteToken, - getInviteToken, - getInviteTokenAndDelete, - requestRecoveryToken, - validateRecoveryToken, - resetPassword, - checkSuperadmin, - getAllMonitors, - getAllMonitorsWithUptimeStats, - getMonitorStatsById, - getMonitorById, - getMonitorsAndSummaryByTeamId, - getMonitorsByTeamId, - createMonitor, - deleteMonitor, - deleteAllMonitors, - editMonitor, - addDemoMonitors, - createCheck, - getChecksCount, - getChecks, - getTeamChecks, - deleteChecks, - deleteChecksByTeamId, - updateChecksTTL, - deleteMonitorsByUserId, - createPageSpeedCheck, - deletePageSpeedChecksByMonitorId, - createHardwareCheck, - deleteHardwareChecksByMonitorId, - createMaintenanceWindow, - getMaintenanceWindowsByTeamId, - getMaintenanceWindowById, - getMaintenanceWindowsByMonitorId, - deleteMaintenanceWindowById, - deleteMaintenanceWindowByMonitorId, - deleteMaintenanceWindowByUserId, - editMaintenanceWindowById, - createNotification, - getNotificationsByMonitorId, - deleteNotificationsByMonitorId, - getAppSettings, - updateAppSettings, - createStatusPage, - getStatusPageByUrl, -}; +class MongoDB { + static SERVICE_NAME = "MongoDB"; + + constructor() { + Object.assign(this, userModule); + Object.assign(this, inviteModule); + Object.assign(this, recoveryModule); + Object.assign(this, monitorModule); + Object.assign(this, pageSpeedCheckModule); + Object.assign(this, hardwareCheckModule); + Object.assign(this, checkModule); + Object.assign(this, maintenanceWindowModule); + Object.assign(this, notificationModule); + Object.assign(this, settingsModule); + Object.assign(this, statusPageModule); + } + + connect = async () => { + try { + const connectionString = + process.env.DB_CONNECTION_STRING || "mongodb://localhost:27017/uptime_db"; + await mongoose.connect(connectionString); + // If there are no AppSettings, create one + let appSettings = await AppSettings.find(); + if (appSettings.length === 0) { + appSettings = new AppSettings({}); + await appSettings.save(); + } + logger.info({ message: "Connected to MongoDB" }); + } catch (error) { + logger.error({ + message: error.message, + service: this.SERVICE_NAME, + method: "connect", + stack: error.stack, + }); + throw error; + } + }; + + disconnect = async () => { + try { + logger.info({ message: "Disconnecting from MongoDB" }); + await mongoose.disconnect(); + logger.info({ message: "Disconnected from MongoDB" }); + return; + } catch (error) { + logger.error({ + message: error.message, + service: this.SERVICE_NAME, + method: "disconnect", + stack: error.stack, + }); + } + }; + checkSuperadmin = async (req, res) => { + const superAdmin = await UserModel.findOne({ role: "superadmin" }); + if (superAdmin !== null) { + return true; + } + return false; + }; +} + +export default MongoDB; From 139c1fd215c8699fb2d87630833f30972c16f47c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 24 Dec 2024 12:00:36 -0800 Subject: [PATCH 12/32] refactor middleware to use service registry --- Server/middleware/isAllowed.js | 7 ++++++- Server/middleware/verifyJWT.js | 9 ++++++--- Server/middleware/verifySuperAdmin.js | 4 +++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Server/middleware/isAllowed.js b/Server/middleware/isAllowed.js index 4aa1a0d81..bd75b00ad 100644 --- a/Server/middleware/isAllowed.js +++ b/Server/middleware/isAllowed.js @@ -1,6 +1,9 @@ import jwt from "jsonwebtoken"; const TOKEN_PREFIX = "Bearer "; const SERVICE_NAME = "allowedRoles"; +import ServiceRegistry from "../service/serviceRegistry.js"; +import SettingsService from "../service/settingsService.js"; + import { errorMessages } from "../utils/messages.js"; const isAllowed = (allowedRoles) => { @@ -27,7 +30,9 @@ const isAllowed = (allowedRoles) => { // Parse the token try { const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); - const { jwtSecret } = req.settingsService.getSettings(); + const { jwtSecret } = ServiceRegistry.get( + SettingsService.SERVICE_NAME + ).getSettings(); var decoded = jwt.verify(parsedToken, jwtSecret); const userRoles = decoded.role; diff --git a/Server/middleware/verifyJWT.js b/Server/middleware/verifyJWT.js index cb52ef297..87fa53bd6 100644 --- a/Server/middleware/verifyJWT.js +++ b/Server/middleware/verifyJWT.js @@ -1,6 +1,7 @@ import jwt from "jsonwebtoken"; import { errorMessages } from "../utils/messages.js"; - +import ServiceRegistry from "../service/serviceRegistry.js"; +import SettingsService from "../service/settingsService.js"; const SERVICE_NAME = "verifyJWT"; const TOKEN_PREFIX = "Bearer "; @@ -34,7 +35,7 @@ const verifyJWT = (req, res, next) => { const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); // Verify the token's authenticity - const { jwtSecret } = req.settingsService.getSettings(); + const { jwtSecret } = ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings(); jwt.verify(parsedToken, jwtSecret, (err, decoded) => { if (err) { if (err.name === "TokenExpiredError") { @@ -67,7 +68,9 @@ function handleExpiredJwtToken(req, res, next) { } // Verify refresh token - const { refreshTokenSecret } = req.settingsService.getSettings(); + const { refreshTokenSecret } = ServiceRegistry.get( + SettingsService.SERVICE_NAME + ).getSettings(); jwt.verify(refreshToken, refreshTokenSecret, (refreshErr, refreshDecoded) => { if (refreshErr) { // Invalid or expired refresh token, trigger logout diff --git a/Server/middleware/verifySuperAdmin.js b/Server/middleware/verifySuperAdmin.js index a8d9d1b6e..bf4c780a7 100644 --- a/Server/middleware/verifySuperAdmin.js +++ b/Server/middleware/verifySuperAdmin.js @@ -3,6 +3,8 @@ const logger = require("../utils/logger"); const SERVICE_NAME = "verifyAdmin"; const TOKEN_PREFIX = "Bearer "; const { errorMessages } = require("../utils/messages"); +import ServiceRegistry from "../service/serviceRegistry.js"; +import SettingsService from "../service/settingsService.js"; /** * Verifies the JWT token * @function @@ -33,7 +35,7 @@ const verifySuperAdmin = (req, res, next) => { const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); // verify admin role is present - const { jwtSecret } = req.settingsService.getSettings(); + const { jwtSecret } = ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings(); jwt.verify(parsedToken, jwtSecret, (err, decoded) => { if (err) { From 83e489cfe56161dbc3406fef3567502650390382 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 24 Dec 2024 12:00:52 -0800 Subject: [PATCH 13/32] Use service registry instead of adding services to request --- Server/index.js | 262 +++++++++++++++++++++++++++--------------------- 1 file changed, 149 insertions(+), 113 deletions(-) diff --git a/Server/index.js b/Server/index.js index 996c4fd07..8f7d845ac 100644 --- a/Server/index.js +++ b/Server/index.js @@ -8,16 +8,31 @@ import cors from "cors"; import logger from "./utils/logger.js"; import { verifyJWT } from "./middleware/verifyJWT.js"; import { handleErrors } from "./middleware/handleErrors.js"; -import authRouter from "./routes/authRoute.js"; -import inviteRouter from "./routes/inviteRoute.js"; -import monitorRouter from "./routes/monitorRoute.js"; -import checkRouter from "./routes/checkRoute.js"; -import maintenanceWindowRouter from "./routes/maintenanceWindowRoute.js"; -import settingsRouter from "./routes/settingsRoute.js"; -import statusPageRouter from "./routes/statusPageRoute.js"; import { fileURLToPath } from "url"; -import queueRouter from "./routes/queueRoute.js"; +import AuthRoutes from "./routes/authRoute.js"; +import AuthController from "./controllers/authController.js"; + +import InviteRoutes from "./routes/inviteRoute.js"; +import InviteController from "./controllers/inviteController.js"; + +import MonitorRoutes from "./routes/monitorRoute.js"; +import MonitorController from "./controllers/monitorController.js"; + +import CheckRoutes from "./routes/checkRoute.js"; +import CheckController from "./controllers/checkController.js"; + +import MaintenanceWindowRoutes from "./routes/maintenanceWindowRoute.js"; +import MaintenanceWindowController from "./controllers/maintenanceWindowController.js"; + +import SettingsRoutes from "./routes/settingsRoute.js"; +import SettingsController from "./controllers/settingsController.js"; + +import StatusPageRoutes from "./routes/statusPageRoute.js"; +import StatusPageController from "./controllers/statusPageController.js"; + +import QueueRoutes from "./routes/queueRoute.js"; +import QueueController from "./controllers/queueController.js"; //JobQueue service and dependencies import JobQueue from "./service/jobQueue.js"; @@ -41,102 +56,68 @@ import mjml2html from "mjml"; import SettingsService from "./service/settingsService.js"; import AppSettings from "./db/models/AppSettings.js"; +// Status Service and dependencies import StatusService from "./service/statusService.js"; + +// Notification Service and dependencies import NotificationService from "./service/notificationService.js"; -import db from "./db/mongo/MongoDB.js"; +// Service Registry +import ServiceRegistry from "./service/serviceRegistry.js"; + +import MongoDB from "./db/mongo/MongoDB.js"; + const SERVICE_NAME = "Server"; const SHUTDOWN_TIMEOUT = 1000; - let isShuttingDown = false; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); - const openApiSpec = JSON.parse( fs.readFileSync(path.join(__dirname, "openapi.json"), "utf8") ); +let server; + const PORT = 5000; +const shutdown = async () => { + if (isShuttingDown) { + return; + } + isShuttingDown = true; + logger.info({ message: "Attempting graceful shutdown" }); + setTimeout(() => { + logger.error({ + message: "Could not shut down in time, forcing shutdown", + service: SERVICE_NAME, + method: "shutdown", + }); + process.exit(1); + }, SHUTDOWN_TIMEOUT); + try { + server.close(); + await ServiceRegistry.get(JobQueue.SERVICE_NAME).obliterate(); + await ServiceRegistry.get(MongoDB.SERVICE_NAME).disconnect(); + logger.info({ message: "Graceful shutdown complete" }); + process.exit(0); + } catch (error) { + logger.error({ + message: error.message, + service: SERVICE_NAME, + method: "shutdown", + stack: error.stack, + }); + } +}; // Need to wrap server setup in a function to handle async nature of JobQueue const startApp = async () => { const app = express(); - // middlewares - app.use( - cors() - //We will add configuration later - ); - app.use(express.json()); - app.use(helmet()); - - // Add db, jobQueue, emailService, and settingsService to request object for easy access - app.use((req, res, next) => { - req.db = db; - req.jobQueue = jobQueue; - req.emailService = emailService; - req.settingsService = settingsService; - next(); - }); - - // Swagger UI - app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); - - //routes - app.use("/api/v1/auth", authRouter); - app.use("/api/v1/settings", verifyJWT, settingsRouter); - app.use("/api/v1/invite", inviteRouter); - app.use("/api/v1/monitors", verifyJWT, monitorRouter); - app.use("/api/v1/checks", verifyJWT, checkRouter); - app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRouter); - app.use("/api/v1/queue", verifyJWT, queueRouter); - app.use("/api/v1/status-page", statusPageRouter); - - app.use("/api/v1/dummy-data", async (req, res) => { - try { - const response = await axios.get( - "https://gist.githubusercontent.com/ajhollid/9afa39410c7bbf52cc905f285a2225bf/raw/429a231a3559ebc95f6f488ed2c766bd7d6f46e5/dummyData.json", - { - headers: { - "Content-Type": "application/json", - "Cache-Control": "no-cache", - }, - } - ); - return res.status(200).json(response.data); - } catch (error) { - return res.status(500).json({ message: error.message }); - } - }); - - //health check - app.use("/api/v1/healthy", (req, res) => { - try { - logger.info({ message: "Checking Health of the server." }); - return res.status(200).json({ message: "Healthy" }); - } catch (error) { - logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "healthCheck", - stack: error.stack, - }); - return res.status(500).json({ message: error.message }); - } - }); - - /** - * Error handler middleware - * Should be called last - */ - app.use(handleErrors); + // Create DB + const db = new MongoDB(); + await db.connect(); // Create services - await db.connect(); - const server = app.listen(PORT, () => { - logger.info({ message: `server started on port:${PORT}` }); - }); - const settingsService = new SettingsService(AppSettings); await settingsService.loadSettings(); const emailService = new EmailService( @@ -162,39 +143,94 @@ const startApp = async () => { Worker ); - const shutdown = async () => { - if (isShuttingDown) { - return; - } - isShuttingDown = true; - logger.info({ message: "Attempting graceful shutdown" }); - setTimeout(() => { - logger.error({ - message: "Could not shut down in time, forcing shutdown", - service: SERVICE_NAME, - method: "shutdown", - }); - process.exit(1); - }, SHUTDOWN_TIMEOUT); - try { - server.close(); - await jobQueue.obliterate(); - await db.disconnect(); - logger.info({ message: "Graceful shutdown complete" }); - process.exit(0); - } catch (error) { - logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "shutdown", - stack: error.stack, - }); - } - }; + // Register services + ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue); + ServiceRegistry.register(MongoDB.SERVICE_NAME, db); + ServiceRegistry.register(SettingsService.SERVICE_NAME, settingsService); + ServiceRegistry.register(EmailService.SERVICE_NAME, emailService); + ServiceRegistry.register(NetworkService.SERVICE_NAME, networkService); + ServiceRegistry.register(StatusService.SERVICE_NAME, statusService); + ServiceRegistry.register(NotificationService.SERVICE_NAME, notificationService); + server = app.listen(PORT, () => { + logger.info({ message: `server started on port:${PORT}` }); + }); process.on("SIGUSR2", shutdown); process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); + + //Create controllers + const authController = new AuthController( + ServiceRegistry.get(MongoDB.SERVICE_NAME), + ServiceRegistry.get(SettingsService.SERVICE_NAME), + ServiceRegistry.get(EmailService.SERVICE_NAME), + ServiceRegistry.get(JobQueue.SERVICE_NAME) + ); + + const monitorController = new MonitorController( + ServiceRegistry.get(MongoDB.SERVICE_NAME), + ServiceRegistry.get(SettingsService.SERVICE_NAME), + ServiceRegistry.get(JobQueue.SERVICE_NAME) + ); + + const settingsController = new SettingsController( + ServiceRegistry.get(MongoDB.SERVICE_NAME), + ServiceRegistry.get(SettingsService.SERVICE_NAME) + ); + + const checkController = new CheckController( + ServiceRegistry.get(MongoDB.SERVICE_NAME), + ServiceRegistry.get(SettingsService.SERVICE_NAME) + ); + + const inviteController = new InviteController( + ServiceRegistry.get(MongoDB.SERVICE_NAME), + ServiceRegistry.get(SettingsService.SERVICE_NAME), + ServiceRegistry.get(EmailService.SERVICE_NAME) + ); + + const maintenanceWindowController = new MaintenanceWindowController( + ServiceRegistry.get(MongoDB.SERVICE_NAME), + ServiceRegistry.get(SettingsService.SERVICE_NAME) + ); + + const queueController = new QueueController(ServiceRegistry.get(JobQueue.SERVICE_NAME)); + + const statusPageController = new StatusPageController( + ServiceRegistry.get(MongoDB.SERVICE_NAME) + ); + + //Create routes + const authRoutes = new AuthRoutes(authController); + const monitorRoutes = new MonitorRoutes(monitorController); + const settingsRoutes = new SettingsRoutes(settingsController); + const checkRoutes = new CheckRoutes(checkController); + const inviteRoutes = new InviteRoutes(inviteController); + const maintenanceWindowRoutes = new MaintenanceWindowRoutes( + maintenanceWindowController + ); + const queueRoutes = new QueueRoutes(queueController); + const statusPageRoutes = new StatusPageRoutes(statusPageController); + // Middleware + app.use( + cors() + //We will add configuration later + ); + app.use(express.json()); + app.use(helmet()); + // Swagger UI + app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); + + //routes + app.use("/api/v1/auth", authRoutes.getRouter()); + app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter()); + app.use("/api/v1/invite", inviteRoutes.getRouter()); + app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter()); + app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter()); + app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter()); + app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter()); + app.use("/api/v1/status-page", statusPageRoutes.getRouter()); + app.use(handleErrors); }; startApp().catch((error) => { From 9182e2a3a363fb6a312676fff968295cc6906f98 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 25 Dec 2024 13:58:22 +0000 Subject: [PATCH 14/32] fix(deps): update dependency bullmq to v5.34.5 --- Server/package-lock.json | 104 +++++++++++++++++++++++++++++++-------- Server/package.json | 2 +- 2 files changed, 84 insertions(+), 22 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 1e24e0686..57b8d2c6e 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.34.4", + "bullmq": "5.34.5", "cors": "^2.8.5", "dockerode": "4.0.2", "dotenv": "^16.4.5", @@ -179,6 +179,17 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/config-array/node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -196,6 +207,19 @@ } } }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/config-array/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -237,6 +261,17 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -266,6 +301,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/eslintrc/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1493,12 +1541,12 @@ } }, "node_modules/bullmq": { - "version": "5.34.4", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.4.tgz", - "integrity": "sha512-FPTN5eqsYO5/Blm6vh/bVJ0eADrVLjTBDjMkPCqZN3xx/5GqHov9NwxM6A6M3m6n2Vg+gNAoN98t7bTij5/UEA==", + "version": "5.34.5", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.5.tgz", + "integrity": "sha512-MHho9EOhLCTY3ZF+dd0wHv0VlY2FtpBcopMRsvj0kPra4TAwBFh2pik/s4WbX56cIfCE+VzfHIHy4xvqp3g1+Q==", "license": "MIT", "dependencies": { - "cron-parser": "^4.6.0", + "cron-parser": "^4.9.0", "ioredis": "^5.4.1", "msgpackr": "^1.11.2", "node-abort-controller": "^3.1.1", @@ -2357,22 +2405,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2894,6 +2926,17 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2978,6 +3021,19 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -8051,6 +8107,12 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, "node_modules/workerpool": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", diff --git a/Server/package.json b/Server/package.json index 740f1be4b..c8460aa03 100644 --- a/Server/package.json +++ b/Server/package.json @@ -18,7 +18,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.34.4", + "bullmq": "5.34.5", "cors": "^2.8.5", "dockerode": "4.0.2", "dotenv": "^16.4.5", From b708e7aa7d4f2e4c0228fb74ecc5637edfe9409b Mon Sep 17 00:00:00 2001 From: Anand Roy Date: Thu, 26 Dec 2024 08:59:56 +0000 Subject: [PATCH 15/32] added year in the date --- Client/src/Pages/Uptime/Details/index.jsx | 65 +++++------------------ 1 file changed, 14 insertions(+), 51 deletions(-) diff --git a/Client/src/Pages/Uptime/Details/index.jsx b/Client/src/Pages/Uptime/Details/index.jsx index 28b198b69..ec722032a 100644 --- a/Client/src/Pages/Uptime/Details/index.jsx +++ b/Client/src/Pages/Uptime/Details/index.jsx @@ -58,7 +58,7 @@ const DetailsPage = () => { setAnchorEl(null); }; - const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D"; + const dateFormat = dateRange === "day" ? "MMM D, YYYY h A" : "MMM D"; const uiTimezone = useSelector((state) => state.ui.timezone); const fetchMonitor = useCallback(async () => { @@ -210,56 +210,7 @@ const DetailsPage = () => { alignSelf: "flex-end", }} > - - - - - Certificate Expiry - - {certificateExpiry} - - + {isAdmin && (