Merge branch 'bluewave-labs:master' into forgot-password

This commit is contained in:
Mohammad Khalilzadeh authored and GitHub committed 2024-05-28 21:31:50 +03:30
commit fea867557e
22 files changed
+817 -139

No files matched your search

+230
View File
@@ -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",
+1
View File
@@ -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",
Whitespace-only changes.
@@ -0,0 +1,19 @@
import { BarChart } from '@mui/x-charts/BarChart';
const ChartsOverviewDemo = () => {
return (
<BarChart
series={[
{ data: [35, 44, 24, 34] },
{ data: [51, 6, 49, 30] },
{ data: [15, 25, 30, 50] },
{ data: [60, 50, 15, 25] },
]}
height={290}
xAxis={[{ data: ['Q1', 'Q2', 'Q3', 'Q4'], scaleType: 'band' }]}
margin={{ top: 10, bottom: 30, left: 40, right: 10 }}
/>
);
};
export default ChartsOverviewDemo;
Whitespace-only changes.
@@ -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 (
* <DashboardSettings />
* )
*/
function DashboardSettings() {
const theme = useTheme();
const [anchorElUser, setAnchorElUser] = useState(null);
/**
* Handles opening the user menu.
*
* @param {React.MouseEvent<HTMLElement>} 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 (
<Box sx={{ flexGrow: 0 }}>
<Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<SvgIcon component={SettingsIcon} />
</IconButton>
</Tooltip>
<Menu
sx={{ mt: theme.spacing(5.5) }}
id="menu-appbar"
anchorEl={anchorElUser}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
>
{settings.map((setting) => (
<MenuItem key={setting} onClick={handleCloseUserMenu}>
<Typography textAlign="center">{setting}</Typography>
</MenuItem>
))}
</Menu>
</Box>
);
}
export default DashboardSettings;
+5 -1
View File
@@ -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 = () => {
<NavBar />
<Home />
<Outlet />
<ChartsOverviewDemo />
<DashboardSettings />
</>
)
}
+150 -11
View File
@@ -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)
- <code>POST</code> [/api/v1/auth/register](#post-register)
- <code>POST</code> [/api/v1/auth/login](#post-login)
- <code>POST</code> [/api/v1/auth/user/{userId}](#post-auth-user-edit-id)
- <code>GET</code> [/api/v1/monitors](#get-monitors)
- <code>GET</code> [/api/v1/monitor/{id}](#get-monitor-id)
- <code>GET</code> [/api/v1/monitors/user/{userId}](#get-monitors-user-userid)
- <code>POST</code> [/api/v1/monitors](#post-monitors)
- <code>POST</code> [/api/v1/monitors/delete/{monitorId}](#post-monitors-del-id)
- <code>POST</code> [/api/v1/monitors/edit/{monitorId}](#post-monitors-edit-id)
5. [Error Handling](#error-handling)
6. [Contributors](#contributors)
---
@@ -108,10 +119,39 @@ Example:
</details>
<details>
<summary><code>Check</code></summary>
| 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 |
</details>
<details>
<summary><code>Alert</code></summary>
| 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 |
</details>
---
<details>
<summary><code>POST</code> <b>/api/v1/auth/register</b></summary>
<summary id='post-register'><code>POST</code> <b>/api/v1/auth/register</b></summary>
##### Method/Headers
@@ -164,7 +204,7 @@ curl --request POST \
---
<details>
<summary><code>POST</code> <b>/api/v1/auth/login</b></summary>
<summary id='post-login'><code>POST</code> <b>/api/v1/auth/login</b></summary>
##### Method/Headers
@@ -213,7 +253,68 @@ curl --request POST \
---
<details>
<summary><code>GET</code> <b>/api/v1/monitors</b></summary>
<summary id='post-auth-user-edit-id'><code>POST</code><b>/api/v1/auth/user/{userId}</b></summary>
###### 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: <bearer_token>' \
--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
}
}
```
</details>
---
<details>
<summary id='get-monitors'><code>GET</code> <b>/api/v1/monitors</b></summary>
##### Method/Headers
@@ -275,7 +376,7 @@ curl --request GET \
---
<details>
<summary><code>GET</code> <b>/api/v1/monitor/{id}</b></summary>
<summary id='get-monitor-id'><code>GET</code> <b>/api/v1/monitor/{id}</b></summary>
###### Method/Headers
@@ -323,7 +424,7 @@ curl --request GET \
---
<details>
<summary><code>GET</code> <b>/api/v1/monitors/user/{userId}</b></summary>
<summary id='get-monitors-user-userid'><code>GET</code> <b>/api/v1/monitors/user/{userId}</b></summary>
###### Method/Headers
@@ -385,7 +486,7 @@ curl --request GET \
---
<details>
<summary><code>POST</code><b>/api/v1/monitors</b></summary>
<summary id='post-monitors'><code>POST</code><b>/api/v1/monitors</b></summary>
###### Method/Headers
@@ -440,7 +541,7 @@ curl --request POST \
---
<details>
<summary><code>POST</code><b>/api/v1/monitors/delete/{monitorId}</b></summary>
<summary id='post-monitors-del-id'><code>POST</code><b>/api/v1/monitors/delete/{monitorId}</b></summary>
###### Method/Headers
@@ -477,7 +578,7 @@ curl --request POST \
---
<details>
<summary><code>POST</code><b>/api/v1/monitors/edit/{monitorId}</b></summary>
<summary id='post-monitors-edit-id'><code>POST</code><b>/api/v1/monitors/edit/{monitorId}</b></summary>
###### Method/Headers
@@ -531,6 +632,44 @@ curl --request POST \
}
```
</details>
---
### 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
<a href="https://github.com/bluewave-labs/bluewave-uptime/graphs/contributors">
+55 -21
View File
@@ -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<Express.Response>}
* @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 };
+67 -52
View File
@@ -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<Express.Response>}
* @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<Express.Response>}
* @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<Express.Response>}
* @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<Express.Response>}
* @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);
}
};
+31
View File
@@ -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<UserModel>}
* @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,
+8 -1
View File
@@ -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);
+12
View File
@@ -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 };
+8 -6
View File
@@ -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);
}
};
+35
View File
@@ -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 };
+28
View File
@@ -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);
+28
View File
@@ -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);
+34 -38
View File
@@ -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);
};
+4
View File
@@ -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;
+12 -2
View File
@@ -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;
+12 -1
View File
@@ -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,
};
-6
View File
@@ -1,6 +0,0 @@
{
"name": "bluewave-uptime",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}