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 (
+
+
+
+
+
+
+
+
+ );
+}
+
+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": {}
-}