diff --git a/Client/package-lock.json b/Client/package-lock.json index fe3d684f5..e4a8fd7a8 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -13,6 +13,7 @@ "@fontsource/roboto": "^5.0.13", "@mui/icons-material": "^5.15.17", "@mui/material": "^5.15.16", + "@mui/x-charts": "^7.5.1", "@mui/x-data-grid": "7.3.2", "@mui/x-date-pickers": "7.3.2", "dayjs": "1.11.11", @@ -989,6 +990,44 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, + "node_modules/@mui/x-charts": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.5.1.tgz", + "integrity": "sha512-8vMQyJL13cGXMbp+gm2QdABzFuOpP4hxEa/o6nL3ZpVNp7hja6h9F5bc0FNGR8h/eRN+4+swDnrEebnR0w4cbA==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "@mui/base": "^5.0.0-beta.40", + "@mui/system": "^5.15.14", + "@mui/utils": "^5.15.14", + "@react-spring/rafz": "^9.7.3", + "@react-spring/web": "^9.7.3", + "clsx": "^2.1.1", + "d3-color": "^3.1.0", + "d3-delaunay": "^6.0.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, "node_modules/@mui/x-data-grid": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.3.2.tgz", @@ -1123,6 +1162,71 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-spring/animated": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", + "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", + "dependencies": { + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", + "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.3.tgz", + "integrity": "sha512-9vzW1zJPcC4nS3aCV+GgcsK/WLaB520Iyvm55ARHfM5AuyBqycjvh1wbmWmgCyJuX4VPoWigzemq1CaaeRSHhQ==" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", + "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", + "dependencies": { + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", + "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" + }, + "node_modules/@react-spring/web": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", + "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@remix-run/router": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", @@ -1674,6 +1778,111 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -1787,6 +1996,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2739,6 +2956,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -3805,6 +4030,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/rollup": { "version": "4.17.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", diff --git a/Client/package.json b/Client/package.json index cca88fb31..213119f38 100644 --- a/Client/package.json +++ b/Client/package.json @@ -15,6 +15,7 @@ "@fontsource/roboto": "^5.0.13", "@mui/icons-material": "^5.15.17", "@mui/material": "^5.15.16", + "@mui/x-charts": "^7.5.1", "@mui/x-data-grid": "7.3.2", "@mui/x-date-pickers": "7.3.2", "dayjs": "1.11.11", diff --git a/Client/src/Components/DashboardProgressBars/index.css b/Client/src/Components/DashboardProgressBars/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/Client/src/Components/DashboardProgressBars/index.jsx b/Client/src/Components/DashboardProgressBars/index.jsx new file mode 100644 index 000000000..8215a0d17 --- /dev/null +++ b/Client/src/Components/DashboardProgressBars/index.jsx @@ -0,0 +1,19 @@ +import { BarChart } from '@mui/x-charts/BarChart'; + +const ChartsOverviewDemo = () => { + return ( + + ); +}; + +export default ChartsOverviewDemo; diff --git a/Client/src/Components/DashboardSettingsIcon/index.css b/Client/src/Components/DashboardSettingsIcon/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/Client/src/Components/DashboardSettingsIcon/index.jsx b/Client/src/Components/DashboardSettingsIcon/index.jsx new file mode 100644 index 000000000..e1f1d59da --- /dev/null +++ b/Client/src/Components/DashboardSettingsIcon/index.jsx @@ -0,0 +1,78 @@ +import { useState } from 'react'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import Menu from '@mui/material/Menu'; +import Tooltip from '@mui/material/Tooltip'; +import MenuItem from '@mui/material/MenuItem'; +import Typography from '@mui/material/Typography'; +import SvgIcon from '@mui/material/SvgIcon'; +import SettingsIcon from '@mui/icons-material/Settings'; +import { useTheme } from '@mui/material/styles'; + +const settings = ['Open Site', 'Detailed View', 'Incidents', 'Configure', 'Remove']; + +/** + * DashboardSettings component + * + * A component that provides a settings menu accessible from an icon. + * + * @component + * @example + * return ( + * + * ) + */ +function DashboardSettings() { + const theme = useTheme(); + const [anchorElUser, setAnchorElUser] = useState(null); + + /** + * Handles opening the user menu. + * + * @param {React.MouseEvent} event - The event triggered by clicking the user menu button. + */ + const handleOpenUserMenu = (event) => { + setAnchorElUser(event.currentTarget); + }; + + /** + * Handles closing the user menu. + */ + const handleCloseUserMenu = () => { + setAnchorElUser(null); + }; + + return ( + + + + + + + + {settings.map((setting) => ( + + {setting} + + ))} + + + ); +} + +export default DashboardSettings; diff --git a/Client/src/Layouts/HomeLayout/index.jsx b/Client/src/Layouts/HomeLayout/index.jsx index 454f1452a..ed1d06fe6 100644 --- a/Client/src/Layouts/HomeLayout/index.jsx +++ b/Client/src/Layouts/HomeLayout/index.jsx @@ -1,8 +1,10 @@ -import React from 'react' import NavBar from '../../Components/NavBar' import { Outlet } from 'react-router' import Home from '../../Pages/Home' import './index.css' +import ChartsOverviewDemo from '../../Components/DashboardProgressBars' +import DashboardSettings from '../../Components/DashboardSettingsIcon' + const HomeLayout = () => { return ( @@ -10,6 +12,8 @@ const HomeLayout = () => { + + ) } diff --git a/README.md b/README.md index 44a30ea94..8767a3633 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,20 @@ BlueWave uptime monitoring application - Clone this repository to your local machine 1. [Installation (Client)](#client) -1. [Installation (Server)](#server) -1. [Configuration(Server)](#config-server) -1. [Endpoints](#endpoints) +2. [Installation (Server)](#server) +3. [Configuration(Server)](#config-server) +4. [Endpoints](#endpoints) + - POST [/api/v1/auth/register](#post-register) + - POST [/api/v1/auth/login](#post-login) + - POST [/api/v1/auth/user/{userId}](#post-auth-user-edit-id) + - GET [/api/v1/monitors](#get-monitors) + - GET [/api/v1/monitor/{id}](#get-monitor-id) + - GET [/api/v1/monitors/user/{userId}](#get-monitors-user-userid) + - POST [/api/v1/monitors](#post-monitors) + - POST [/api/v1/monitors/delete/{monitorId}](#post-monitors-del-id) + - POST [/api/v1/monitors/edit/{monitorId}](#post-monitors-edit-id) +5. [Error Handling](#error-handling) +6. [Contributors](#contributors) --- @@ -108,10 +119,39 @@ Example: +
+Check + +| Name | Type | Notes | +| ----------- | --------- | ------------------------------------------------| +| monitorId | `string` | Unique ID for the monitor | +| status | `boolean` | Indicates the service is Up or Down | +| responseTime| `integer` | Indicates the response time of the service (ms) | +| statusCode | `integer` | Status Code returned from the service | +| message | `string` | Message returned from the service | +| updatedAt | `Date` | Last time the check was updated | +| CreatedAt | `Date` | When the check was created | + +
+ +
+Alert + +| Name | Type | Notes | +| ----------- | --------- | --------------------------------------------------| +| checkId | `string` | Unique ID for the check | +| status | `boolean` | Indicates the service is Up or Down | +| message | `string` | Message for the user about the down service | +| notifiedStatus | `boolean` | Indicates whether the user is notified | +| acknowledgeStatus | `boolean` | Indicates whether the user acknowledged the alert | +| updatedAt | `Date` | Last time the alert was updated | +| CreatedAt | `Date` | When the alert was created | + +
---
-POST /api/v1/auth/register +POST /api/v1/auth/register ##### Method/Headers @@ -164,7 +204,7 @@ curl --request POST \ ---
-POST /api/v1/auth/login +POST /api/v1/auth/login ##### Method/Headers @@ -213,7 +253,68 @@ curl --request POST \ ---
-GET /api/v1/monitors +POST/api/v1/auth/user/{userId} + +###### Method/Headers + +> | Method/Headers | Value | +> | -------------- | ----- | +> | Method | POST | + +##### Body + +> | Name | Type | Notes | +> | ------------- | -------- | ----------- | +> | firstname | `string` | | +> | lastname | `string` | | +> | profilePicUrl | `string` | | +> | password | `string` | Min 8 chars | + +###### Response Payload + +> | Type | Notes | +> | ------ | ------------------------ | +> | `User` | Returns the updated user | + +##### Sample CURL request + +``` +curl --request POST \ + --url http://localhost:5000/api/v1/auth/user/6654d156634754f789e1f10e \ + --header 'Authorization: ' \ + --header 'Content-Type: application/json' \ + --data '{ + "firstname": "First Name", + "lastname: "Last Name" +}' +``` + +###### Sample Response + +```json +{ + "success": true, + "msg": "User updated", + "data": { + "_id": "6654d156634754f789e1f10e", + "firstname": "First Name", + "lastname": "Last Name", + "email": "me@gmail.com", + "isActive": true, + "isVerified": false, + "createdAt": "2024-05-27T18:30:46.358Z", + "updatedAt": "2024-05-27T19:21:51.747Z", + "__v": 0 + } +} +``` + +
+ +--- + +
+GET /api/v1/monitors ##### Method/Headers @@ -275,7 +376,7 @@ curl --request GET \ ---
-GET /api/v1/monitor/{id} +GET /api/v1/monitor/{id} ###### Method/Headers @@ -323,7 +424,7 @@ curl --request GET \ ---
-GET /api/v1/monitors/user/{userId} +GET /api/v1/monitors/user/{userId} ###### Method/Headers @@ -385,7 +486,7 @@ curl --request GET \ ---
-POST/api/v1/monitors +POST/api/v1/monitors ###### Method/Headers @@ -440,7 +541,7 @@ curl --request POST \ ---
-POST/api/v1/monitors/delete/{monitorId} +POST/api/v1/monitors/delete/{monitorId} ###### Method/Headers @@ -477,7 +578,7 @@ curl --request POST \ ---
-POST/api/v1/monitors/edit/{monitorId} +POST/api/v1/monitors/edit/{monitorId} ###### Method/Headers @@ -531,6 +632,44 @@ curl --request POST \ } ``` +
+ +--- + +### Error handling {#error-handling} + +Errors are returned in a standard format: + +`{"success": false, "msg": "No token provided"}` + +Errors are handled by error handling middleware and should be thrown with the following parameters + +| Name | Type | Default | Notes | +| ------- | --------- | ---------------------- | ------------------------------------ | +| status | `integer` | 500 | Standard HTTP codes | +| message | `string` | "Something went wrong" | An error message | +| service | `string` | "Unknown Service" | Name of service that threw the error | + +Example: + +``` +const myRoute = async(req, res, next) => { + try{ + const result = myRiskyOperationHere(); + } + catch(error){ + error.status = 404 + error.message = "Resource not found" + error.service = service name + next(error) + return; + } +} + +``` + +Errors should not be handled at the controller level and should be left to the middleware to handle. + ## Contributors diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 1285726e1..cc476ab54 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -1,5 +1,10 @@ const express = require("express"); -const { registerValidation, loginValidation } = require("../validation/joi"); +const { + registerValidation, + loginValidation, + editUserParamValidation, + editUserBodyValidation, +} = require("../validation/joi"); const logger = require("../utils/logger"); require("dotenv").config(); var jwt = require("jsonwebtoken"); @@ -21,35 +26,32 @@ const issueToken = (payload) => { * @param {express.Response} res * @returns {{success: Boolean, msg: String}} */ -const registerController = async (req, res) => { +const registerController = async (req, res, next) => { // joi validation try { await registerValidation.validateAsync(req.body); } catch (error) { - return res - .status(400) - .json({ success: false, msg: error.details[0].message }); + error.status = 400; + error.service = SERVICE_NAME; + error.message = error.details[0].message; + next(error); + return; } // Check if the user exists try { const isUser = await req.db.getUserByEmail(req, res); if (isUser) { - logger.error("User already exists", { - service: SERVICE_NAME, - userId: isUser._id, - }); - return res - .status(400) - .json({ success: false, msg: "User already exists" }); + throw new Error("User already exists"); } } catch (error) { - logger.error(error.message, { service: SERVICE_NAME }); - return res.status(500).json({ success: false, msg: error.message }); + error.service = SERVICE_NAME; + next(error); + return; } + // Create a new user try { - // Create a new user const newUser = await req.db.insertUser(req, res); // TODO: Send an email to user // Will add this later @@ -63,8 +65,8 @@ const registerController = async (req, res) => { .status(200) .json({ success: true, msg: "User created", data: token }); } catch (error) { - logger.error(error.message, { service: SERVICE_NAME }); - return res.status(500).json({ success: false, msg: error.message }); + error.service = SERVICE_NAME; + next(error); } }; @@ -76,7 +78,7 @@ const registerController = async (req, res) => { * @returns {Promise} * @throws {Error} */ -const loginController = async (req, res) => { +const loginController = async (req, res, next) => { try { // Validate input await loginValidation.validateAsync(req.body); @@ -104,10 +106,42 @@ const loginController = async (req, res) => { .status(200) .json({ success: true, msg: "Found user", data: token }); } catch (error) { + error.status = 500; // Anything else should be an error - logger.error(error.message, { service: SERVICE_NAME }); - return res.status(500).json({ success: false, msg: error.message }); + next(error); } }; -module.exports = { registerController, loginController }; +const userEditController = async (req, res, next) => { + try { + await editUserParamValidation.validateAsync(req.params); + await editUserBodyValidation.validateAsync(req.body); + } catch (error) { + error.status = 422; + error.service = SERVICE_NAME; + error.message = error.details[0].message; + next(error); + return; + } + + if (req.params.userId !== req.user._id.toString()) { + const error = new Error("Unauthorized access"); + error.status = 401; + error.service = SERVICE_NAME; + next(error); + return; + } + + try { + const updatedUser = await req.db.updateUser(req, res); + res + .status(200) + .json({ success: true, msg: "User updated", data: updatedUser }); + } catch (error) { + error.service = SERVICE_NAME; + next(error); + return; + } +}; + +module.exports = { registerController, loginController, userEditController }; diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index f004f98f3..250dc55b8 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -4,7 +4,7 @@ const { monitorValidation, } = require("../validation/joi"); -const logger = require("../utils/logger"); +const SERVICE_NAME = "monitorController"; /** * Returns all monitors @@ -14,13 +14,13 @@ const logger = require("../utils/logger"); * @returns {Promise} * @throws {Error} */ -const getAllMonitors = async (req, res) => { +const getAllMonitors = async (req, res, next) => { try { const monitors = await req.db.getAllMonitors(); return res.json({ success: true, msg: "Monitors found", data: monitors }); } catch (error) { - logger.error(error.message, { service: "monitor" }); - return res.status(500).json({ success: false, msg: error.message }); + error.service = SERVICE_NAME; + next(error); } }; @@ -32,20 +32,28 @@ const getAllMonitors = async (req, res) => { * @returns {Promise} * @throws {Error} */ -const getMonitorById = async (req, res) => { - const { error } = getMonitorByIdValidation.validate(req.params); - if (error) { - return res - .status(422) - .json({ success: false, msg: error.details[0].message }); +const getMonitorById = async (req, res, next) => { + try { + await getMonitorByIdValidation.validateAsync(req.params); + } catch (error) { + error.status = 422; + error.message = error.details[0].message; + next(error); + return; } try { const monitor = await req.db.getMonitorById(req, res); + if (!monitor) { + const error = new Error("Monitor not found"); + error.status = 404; + throw error; + } + return res.json({ success: true, msg: "Monitor found", data: monitor }); } catch (error) { - logger.error(error.message, { service: "monitor" }); - return res.status(500).json({ success: false, msg: error.message }); + error.service = SERVICE_NAME; + next(error); } }; @@ -57,7 +65,7 @@ const getMonitorById = async (req, res) => { * @returns {Promise} * @throws {Error} */ -const getMonitorsByUserId = async (req, res) => { +const getMonitorsByUserId = async (req, res, next) => { const { error } = getMonitorsByUserIdValidation.validate(req.params); if (error) { return res @@ -68,18 +76,21 @@ const getMonitorsByUserId = async (req, res) => { try { const userId = req.params.userId; const monitors = await req.db.getMonitorsByUserId(req, res); - logger.info(`Monitors for user ${userId} found`, { - service: "monitor", - userId: userId, - }); + + if (monitors && monitors.length === 0) { + const err = new Error("No monitors found"); + err.status = 404; + throw err; + } + return res.json({ success: true, msg: `Monitors for user ${userId} found`, data: monitors, }); } catch (error) { - logger.error(error.message, { service: "monitor" }); - return res.status(500).json({ success: false, msg: error.message }); + error.service = SERVICE_NAME; + next(error); } }; @@ -92,12 +103,15 @@ const getMonitorsByUserId = async (req, res) => { * @throws {Error} */ -const createMonitor = async (req, res) => { - const { error } = monitorValidation.validate(req.body); - if (error) { - return res - .status(422) - .json({ success: false, msg: error.details[0].message }); +const createMonitor = async (req, res, next) => { + try { + await monitorValidation.validateAsync(req.body); + } catch (error) { + error.status = 422; + error.service = SERVICE_NAME; + error.message = error.details[0].message; + next(error); + return; } try { @@ -106,8 +120,8 @@ const createMonitor = async (req, res) => { .status(201) .json({ success: true, msg: "Monitor created", data: monitor }); } catch (error) { - logger.error(error.message, { service: "monitor" }); - return res.status(500).json({ success: false, msg: error.message }); + error.service = SERVICE_NAME; + next(error); } }; @@ -120,15 +134,19 @@ const createMonitor = async (req, res) => { * @throws {Error} */ -const deleteMonitor = async (req, res) => { - const { error } = getMonitorByIdValidation.validate(req.params); - if (error) { - return res - .status(422) - .json({ success: false, msg: error.details[0].message }); - } +const deleteMonitor = async (req, res, next) => { try { - const monitor = await req.db.deleteMonitor(req, res); + await getMonitorByIdValidation.validateAsync(req.params); + } catch (error) { + error.status = 422; + error.service = SERVICE_NAME; + error.message = error.details[0].message; + next(error); + return; + } + + try { + const monitor = await req.db.deleteMonitor(req, res, next); /** * TODO * We should remove all checks and alerts associated with this monitor @@ -137,8 +155,8 @@ const deleteMonitor = async (req, res) => { */ return res.status(200).json({ success: true, msg: "Monitor deleted" }); } catch (error) { - logger.error(error.message, { service: "monitor" }); - return res.status(500).json({ success: false, msg: error.message }); + error.service = SERVICE_NAME; + next(error); } }; @@ -150,19 +168,16 @@ const deleteMonitor = async (req, res) => { * @returns {Promise} * @throws {Error} */ -const editMonitor = async (req, res) => { - let paramError = getMonitorByIdValidation.validate(req.params); - if (paramError.error) { - return res - .status(422) - .json({ success: false, msg: paramError.error.details[0].message }); - } - - let { error } = monitorValidation.validate(req.body); - if (error) { - return res - .status(422) - .json({ success: false, msg: error.details[0].message }); +const editMonitor = async (req, res, next) => { + try { + await getMonitorByIdValidation.validateAsync(req.params); + await monitorValidation.validateAsync(req.body); + } catch (error) { + error.status = 422; + error.service = SERVICE_NAME; + error.message = error.details[0].message; + next(error); + return; } try { @@ -171,8 +186,8 @@ const editMonitor = async (req, res) => { .status(200) .json({ success: true, msg: "Monitor edited", data: editedMonitor }); } catch (error) { - logger.error(error.message, { service: "monitor" }); - return res.status(500).json({ success: false, msg: error.message }); + error.service = SERVICE_NAME; + next(error); } }; diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js index 8fcc1dced..71b6fe44a 100644 --- a/Server/db/MongoDB.js +++ b/Server/db/MongoDB.js @@ -2,6 +2,10 @@ const Monitor = require("../models/Monitor"); const mongoose = require("mongoose"); const UserModel = require("../models/user"); +const verifyId = (userId, monitorId) => { + return userId.toString() === monitorId.toString(); +}; + const connect = async () => { try { await mongoose.connect(process.env.DB_CONNECTION_STRING); @@ -51,6 +55,31 @@ const getUserByEmail = async (req, res) => { } }; +/** + * Update a user by ID + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ + +const updateUser = async (req, res) => { + const candidateUserId = req.params.userId; + const candidateUser = req.body; + + try { + const updatedUser = await UserModel.findByIdAndUpdate( + candidateUserId, + candidateUser, + { new: true } // Returns updated user instead of pre-update user + ).select("-password"); + return updatedUser; + } catch (error) { + throw error; + } +}; + /** * Get all monitors * @async @@ -113,6 +142,7 @@ const getMonitorsByUserId = async (req, res) => { const createMonitor = async (req, res) => { try { const monitor = new Monitor({ ...req.body }); + monitor.userId = req.user._id; await monitor.save(); return monitor; } catch (error) { @@ -168,6 +198,7 @@ module.exports = { connect, insertUser, getUserByEmail, + updateUser, getAllMonitors, getMonitorById, getMonitorsByUserId, diff --git a/Server/index.js b/Server/index.js index 5068c98d8..63d919b8d 100644 --- a/Server/index.js +++ b/Server/index.js @@ -6,7 +6,8 @@ const monitorRouter = require("./routes/monitorRoute"); const { connectDbAndRunServer } = require("./configs/db"); require("dotenv").config(); const logger = require("./utils/logger"); -var { verifyJWT } = require("./middleware/verifyJWT"); +const { verifyJWT } = require("./middleware/verifyJWT"); +const { handleErrors } = require("./middleware/handleErrors"); // const { sendEmail } = require('./utils/sendEmail') @@ -79,4 +80,10 @@ app.use("/api/v1/healthy", (req, res) => { } }); +/** + * Error handler middleware + * Should be called last + */ +app.use(handleErrors); + connectDbAndRunServer(app, db); diff --git a/Server/middleware/handleErrors.js b/Server/middleware/handleErrors.js new file mode 100644 index 000000000..a20db8571 --- /dev/null +++ b/Server/middleware/handleErrors.js @@ -0,0 +1,12 @@ +const logger = require("../utils/logger"); + +const handleErrors = (error, req, res, next) => { + const status = error.status || 500; + const message = error.message || "Something went wrong"; + const service = error.service || "Unknown service"; + + logger.error(error.message, { service: service }); + res.status(status).json({ success: false, msg: message }); +}; + +module.exports = { handleErrors }; diff --git a/Server/middleware/verifyJWT.js b/Server/middleware/verifyJWT.js index 53464c606..2e8895623 100644 --- a/Server/middleware/verifyJWT.js +++ b/Server/middleware/verifyJWT.js @@ -15,8 +15,11 @@ const verifyJWT = (req, res, next) => { const token = req.headers["authorization"]; // Make sure a token is provided if (!token) { - logger.error("No token provided", { service: SERVICE_NAME }); - return res.status(401).json({ success: false, msg: "No token provided" }); + const error = new Error("No token provided"); + error.status = 401; + error.service = SERVICE_NAME; + next(error); + return; } // Make sure it is properly formatted if (token.startsWith(TOKEN_PREFIX)) { @@ -32,10 +35,9 @@ const verifyJWT = (req, res, next) => { next(); }); } else { - logger.error("Invalid token format", { service: SERVICE_NAME }); - return res - .status(400) - .json({ success: false, msg: "Invalid token format" }); + error.status = 400; + error.service = SERVICE_NAME; + next(error); } }; diff --git a/Server/middleware/verifyOwnership.js b/Server/middleware/verifyOwnership.js new file mode 100644 index 000000000..409303fa4 --- /dev/null +++ b/Server/middleware/verifyOwnership.js @@ -0,0 +1,35 @@ +const logger = require("../utils/logger"); +const SERVICE_NAME = "verifyOwnership"; + +const verifyOwnership = (Model, paramName) => { + return async (req, res, next) => { + const userId = req.user._id; + const documentId = req.params[paramName]; + try { + const doc = await Model.findById(documentId); + //If the document is not found, return a 404 error + if (!doc) { + logger.error("Document not found", { + service: SERVICE_NAME, + }); + const error = new Error("Document not found"); + error.status = 404; + throw error; + } + + // If the userID does not match the document's userID, return a 403 error + if (userId.toString() !== doc.userId.toString()) { + console.log("boom"); + const error = new Error("Unauthorized access"); + error.status = 403; + throw error; + } + next(); + } catch (error) { + error.service = SERVICE_NAME; + next(error); + } + }; +}; + +module.exports = { verifyOwnership }; diff --git a/Server/models/Alert.js b/Server/models/Alert.js new file mode 100644 index 000000000..72705d947 --- /dev/null +++ b/Server/models/Alert.js @@ -0,0 +1,28 @@ +const mongoose = require('mongoose') + +const AlertSchema = mongoose.Schema( + { + checkId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Check', + immutable: true, + }, + status: { + type: Boolean, + }, + message: { + type: String, + }, + notifiedStatus: { + type: Boolean, + }, + acknowledgeStatus: { + type: Boolean, + } + }, + { + timestamps: true, + } +); + +module.exports = mongoose.model('Alert', AlertSchema); diff --git a/Server/models/Check.js b/Server/models/Check.js new file mode 100644 index 000000000..eedc2a28e --- /dev/null +++ b/Server/models/Check.js @@ -0,0 +1,28 @@ +const mongoose = require('mongoose') + +const CheckSchema = mongoose.Schema( + { + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Monitor', + immutable: true, + }, + status: { + type: Boolean, + }, + responseTime: { + type: Number, // milliseconds + }, + statusCode: { + type: Number, // 200, ... , 500 + }, + message: { + type: String, + } + }, + { + timestamps: true, + } +); + +module.exports = mongoose.model('Check', CheckSchema); diff --git a/Server/models/user.js b/Server/models/user.js index 02d0d53b3..7f3945354 100644 --- a/Server/models/user.js +++ b/Server/models/user.js @@ -1,44 +1,41 @@ const mongoose = require("mongoose"); const bcrypt = require("bcrypt"); -const UserSchema = mongoose.Schema({ - firstname: { - type: String, - required: true, +const UserSchema = mongoose.Schema( + { + firstname: { + type: String, + required: true, + }, + lastname: { + type: String, + required: true, + }, + email: { + type: String, + required: true, + unique: true, + }, + password: { + type: String, + required: true, + }, + profilePicUrl: { + type: String, + }, + isActive: { + type: Boolean, + default: true, + }, + isVerified: { + type: Boolean, + default: false, + }, }, - lastname: { - type: String, - required: true, - }, - email: { - type: String, - required: true, - unique: true, - }, - password: { - type: String, - required: true, - }, - profilePicUrl: { - type: String, - }, - isActive: { - type: Boolean, - default: true, - }, - isVerified: { - type: Boolean, - default: false, - }, - updated_at: { - type: Date, - default: Date.now, - }, - created_at: { - type: Date, - default: Date.now, - }, -}); + { + timestamps: true, + } +); UserSchema.pre("save", async function (next) { if (!this.isModified("password")) { @@ -50,7 +47,6 @@ UserSchema.pre("save", async function (next) { }); UserSchema.methods.comparePassword = function (submittedPassword) { - console.log(submittedPassword, this.password); return bcrypt.compare(submittedPassword, this.password); }; diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index 48a173b92..637e4bf5e 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -1,10 +1,14 @@ const router = require("express").Router(); +const { verifyJWT } = require("../middleware/verifyJWT"); + const { registerController, loginController, + userEditController, } = require("../controllers/authController"); router.post("/register", registerController); router.post("/login", loginController); +router.post("/user/:userId", verifyJWT, userEditController); module.exports = router; diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index 5f2ce4bb5..ffdce1d4d 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -1,11 +1,21 @@ const router = require("express").Router(); const monitorController = require("../controllers/monitorController"); +const { verifyOwnership } = require("../middleware/verifyOwnership"); +const Monitor = require("../models/Monitor"); router.get("/", monitorController.getAllMonitors); router.get("/:monitorId", monitorController.getMonitorById); router.get("/user/:userId", monitorController.getMonitorsByUserId); router.post("/", monitorController.createMonitor); -router.post("/delete/:monitorId", monitorController.deleteMonitor); -router.post("/edit/:monitorId", monitorController.editMonitor); +router.post( + "/delete/:monitorId", + verifyOwnership(Monitor, "monitorId"), + monitorController.deleteMonitor +); +router.post( + "/edit/:monitorId", + verifyOwnership(Monitor, "monitorId"), + monitorController.editMonitor +); module.exports = router; diff --git a/Server/validation/joi.js b/Server/validation/joi.js index ecc19253e..399c243dc 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -1,5 +1,4 @@ const joi = require("joi"); -const user = require("../models/user"); const loginValidation = joi.object({ email: joi.string().email().required(), @@ -30,10 +29,22 @@ const monitorValidation = joi.object({ interval: joi.number(), }); +const editUserParamValidation = joi.object({ + userId: joi.string().required(), +}); + +const editUserBodyValidation = joi.object({ + firstname: joi.string(), + lastname: joi.string(), + profilePicUrl: joi.string(), +}); + module.exports = { loginValidation, registerValidation, getMonitorByIdValidation, getMonitorsByUserIdValidation, monitorValidation, + editUserParamValidation, + editUserBodyValidation, }; diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index f71a78c1f..000000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "bluewave-uptime", - "lockfileVersion": 3, - "requires": true, - "packages": {} -}