diff --git a/Client/package-lock.json b/Client/package-lock.json
index e17e455b2..fe3d684f5 100644
--- a/Client/package-lock.json
+++ b/Client/package-lock.json
@@ -11,7 +11,7 @@
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@fontsource/roboto": "^5.0.13",
- "@mui/icons-material": "^5.15.16",
+ "@mui/icons-material": "^5.15.17",
"@mui/material": "^5.15.16",
"@mui/x-data-grid": "7.3.2",
"@mui/x-date-pickers": "7.3.2",
@@ -775,9 +775,9 @@
}
},
"node_modules/@mui/icons-material": {
- "version": "5.15.16",
- "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.16.tgz",
- "integrity": "sha512-s8vYbyACzTNZRKv+20fCfVXJwJqNcVotns2EKnu1wmAga6wv2LAo5kB1d5yqQqZlMFtp34EJvRXf7cy8X0tJVA==",
+ "version": "5.15.17",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.17.tgz",
+ "integrity": "sha512-xVzl2De7IY36s/keHX45YMiCpsIx3mNv2xwDgtBkRSnZQtVk+Gqufwj1ktUxEyjzEhBl0+PiNJqYC31C+n1n6A==",
"dependencies": {
"@babel/runtime": "^7.23.9"
},
diff --git a/Client/package.json b/Client/package.json
index 7d5c47a8d..cca88fb31 100644
--- a/Client/package.json
+++ b/Client/package.json
@@ -13,7 +13,7 @@
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@fontsource/roboto": "^5.0.13",
- "@mui/icons-material": "^5.15.16",
+ "@mui/icons-material": "^5.15.17",
"@mui/material": "^5.15.16",
"@mui/x-data-grid": "7.3.2",
"@mui/x-date-pickers": "7.3.2",
diff --git a/Client/src/Components/Avatar/avatar.css b/Client/src/Components/Avatar/index.css
similarity index 100%
rename from Client/src/Components/Avatar/avatar.css
rename to Client/src/Components/Avatar/index.css
diff --git a/Client/src/Components/Avatar/Avatar.jsx b/Client/src/Components/Avatar/index.jsx
similarity index 100%
rename from Client/src/Components/Avatar/Avatar.jsx
rename to Client/src/Components/Avatar/index.jsx
diff --git a/Client/src/Components/Button/Button.css b/Client/src/Components/Button/index.css
similarity index 100%
rename from Client/src/Components/Button/Button.css
rename to Client/src/Components/Button/index.css
diff --git a/Client/src/Components/Button/Button.jsx b/Client/src/Components/Button/index.jsx
similarity index 100%
rename from Client/src/Components/Button/Button.jsx
rename to Client/src/Components/Button/index.jsx
diff --git a/Client/src/Components/DashboardMenu/index.css b/Client/src/Components/DashboardMenu/index.css
new file mode 100644
index 000000000..37c84655b
--- /dev/null
+++ b/Client/src/Components/DashboardMenu/index.css
@@ -0,0 +1,15 @@
+.icon-container {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+ }
+
+ .text-container {
+ display: flex;
+ align-items: center;
+ }
+
+ .dashboard-menu-text {
+ margin-left: 8px;
+ }
+
\ No newline at end of file
diff --git a/Client/src/Components/DashboardMenu/index.jsx b/Client/src/Components/DashboardMenu/index.jsx
new file mode 100644
index 000000000..716d68497
--- /dev/null
+++ b/Client/src/Components/DashboardMenu/index.jsx
@@ -0,0 +1,58 @@
+import LanguageIcon from "@mui/icons-material/Language";
+import ReportGmailerrorredIcon from "@mui/icons-material/ReportGmailerrorred";
+import SensorsIcon from "@mui/icons-material/Sensors";
+import SettingsIcon from "@mui/icons-material/Settings";
+import AllInclusiveIcon from "@mui/icons-material/AllInclusive";
+import SvgIcon from '@mui/material/SvgIcon';
+import './index.css';
+
+/**
+ * @component
+ * DashboardMenu component displays a menu with icons and corresponding text.
+ * Each menu item consists of an icon and a text label.
+ *
+ * @returns {JSX.Element} The JSX element representing the DashboardMenu component.
+ */
+
+function DashboardMenu() {
+ return (
+
+ );
+}
+
+export default DashboardMenu;
diff --git a/Client/src/Components/Label/BaseLabel.jsx b/Client/src/Components/Label/BaseLabel.jsx
deleted file mode 100644
index aa23aeab3..000000000
--- a/Client/src/Components/Label/BaseLabel.jsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import PropTypes from "prop-types";
-import "./BaseLabel.css";
-import { useTheme } from "@mui/material";
-
-/**
- * @typedef {Object} Styles
- * @param {string} [color] - The text color
- * @param {string} [backgroundColor] - The background color
- * @param {string} [borderColor] - The border color
- */
-
-/**
- * @component
- * @param {Object} props
- * @param {string} props.label - The label of the label
- * @param {Styles} props.styles - CSS Styles passed from parent component
- * @param {React.ReactNode} children - Children passed from parent component
- * @returns {JSX.Element}
- */
-
-const BaseLabel = ({ label, styles, children }) => {
- const theme = useTheme();
- // Grab the default borderRadius from the theme to match button style
- const { borderRadius } = theme.shape;
- // Calculate padding for the label to mimic button. Appears to scale correctly, not 100% sure though.
- const padding = theme.spacing(1 * 0.75, 2);
-
- return (
-
- {children}
- {label}
-
- );
-};
-
-BaseLabel.propTypes = {
- label: PropTypes.string.isRequired,
- styles: PropTypes.shape({
- color: PropTypes.string,
- backgroundColor: PropTypes.string,
- }),
- children: PropTypes.node,
-};
-
-export default BaseLabel;
diff --git a/Client/src/Components/Label/ColoredLabel.jsx b/Client/src/Components/Label/ColoredLabel.jsx
deleted file mode 100644
index 744652050..000000000
--- a/Client/src/Components/Label/ColoredLabel.jsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { useTheme } from "@mui/material";
-import { PropTypes } from "prop-types";
-import BaseLabel from "./BaseLabel";
-
-// Produces a lighter color based on a hex color and a percent
-// lightenColor("#067647", 20) will produce a color 20% lighter than #067647
-const lightenColor = (color, percent) => {
- let r = parseInt(color.substring(1, 3), 16);
- let g = parseInt(color.substring(3, 5), 16);
- let b = parseInt(color.substring(5, 7), 16);
-
- const amt = Math.round((255 * percent) / 100);
-
- r = r + amt <= 255 ? r + amt : 255;
- g = g + amt <= 255 ? g + amt : 255;
- b = b + amt <= 255 ? b + amt : 255;
-
- r = r.toString(16).padStart(2, "0");
- g = g.toString(16).padStart(2, "0");
- b = b.toString(16).padStart(2, "0");
-
- return `#${r}${g}${b}`;
-};
-
-/**
- * @component
- * @param {Object} props
- * @param {string} props.label - The label of the label
- * @param {string} props.color - The color of the label, specified in #RRGGBB format
- * @returns {JSX.Element}
- * @example
- * // Render a red label
- *
- */
-
-const ColoredLabel = ({ label, color }) => {
- const theme = useTheme();
- // If an invalid color is passed, default to the labelGray color
- if (
- typeof color !== "string" ||
- !/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)
- ) {
- color = theme.palette.labelGray.color;
- }
-
- // Calculate lighter shades for border and bg
- const borderColor = lightenColor(color, 20);
- const bgColor = lightenColor(color, 75);
-
- return (
-
- );
-};
-
-ColoredLabel.propTypes = {
- label: PropTypes.string.isRequired,
- color: PropTypes.string.isRequired,
-};
-
-export default ColoredLabel;
diff --git a/Client/src/Components/Label/StautsLabel.jsx b/Client/src/Components/Label/StautsLabel.jsx
deleted file mode 100644
index 108ca0f2f..000000000
--- a/Client/src/Components/Label/StautsLabel.jsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import PropTypes from "prop-types";
-import BaseLabel from "./BaseLabel";
-import { Box } from "@mui/material";
-import { useTheme } from "@mui/material";
-
-/**
- * @component
- * @param {Object} props
- * @param {'Seen' | 'Waiting' | 'New' | 'Active'} props.status - The status for the label
- * @returns {JSX.Element}
- * @example
- * // Render an active label
- *
- */
-
-const StatusLabel = ({ status }) => {
- const theme = useTheme();
-
- const colorLookup = {
- Seen: theme.palette.labelGray.color,
- Waiting: theme.palette.labelRed.color,
- New: theme.palette.labelOrange.color,
- Active: theme.palette.labelGreen.color,
- };
-
- // Look up the color for the status, default to labelGray if not found
- const color = colorLookup[status] || theme.palette.labelGray.color;
-
- return (
-
-
-
- );
-};
-
-StatusLabel.propTypes = {
- status: PropTypes.oneOf(["Seen", "Waiting", "New", "Active"]),
-};
-
-export default StatusLabel;
diff --git a/Client/src/Components/Label/BaseLabel.css b/Client/src/Components/Label/index.css
similarity index 100%
rename from Client/src/Components/Label/BaseLabel.css
rename to Client/src/Components/Label/index.css
diff --git a/Client/src/Components/Label/index.jsx b/Client/src/Components/Label/index.jsx
new file mode 100644
index 000000000..c47b53cb8
--- /dev/null
+++ b/Client/src/Components/Label/index.jsx
@@ -0,0 +1,157 @@
+import PropTypes from "prop-types";
+import { Box } from "@mui/material";
+import { useTheme } from "@mui/material";
+import "./index.css";
+
+/**
+ * @typedef {Object} Styles
+ * @param {string} [color] - The text color
+ * @param {string} [backgroundColor] - The background color
+ * @param {string} [borderColor] - The border color
+ */
+
+/**
+ * @component
+ * @param {Object} props
+ * @param {string} props.label - The label of the label
+ * @param {Styles} props.styles - CSS Styles passed from parent component
+ * @param {React.ReactNode} children - Children passed from parent component
+ * @returns {JSX.Element}
+ */
+
+const BaseLabel = ({ label, styles, children }) => {
+ const theme = useTheme();
+ // Grab the default borderRadius from the theme to match button style
+ const { borderRadius } = theme.shape;
+ // Calculate padding for the label to mimic button. Appears to scale correctly, not 100% sure though.
+ const padding = theme.spacing(1 * 0.75, 2);
+
+ return (
+
+ {children}
+ {label}
+
+ );
+};
+
+BaseLabel.propTypes = {
+ label: PropTypes.string.isRequired,
+ styles: PropTypes.shape({
+ color: PropTypes.string,
+ backgroundColor: PropTypes.string,
+ }),
+ children: PropTypes.node,
+};
+
+// Produces a lighter color based on a hex color and a percent
+// lightenColor("#067647", 20) will produce a color 20% lighter than #067647
+const lightenColor = (color, percent) => {
+ let r = parseInt(color.substring(1, 3), 16);
+ let g = parseInt(color.substring(3, 5), 16);
+ let b = parseInt(color.substring(5, 7), 16);
+
+ const amt = Math.round((255 * percent) / 100);
+
+ r = r + amt <= 255 ? r + amt : 255;
+ g = g + amt <= 255 ? g + amt : 255;
+ b = b + amt <= 255 ? b + amt : 255;
+
+ r = r.toString(16).padStart(2, "0");
+ g = g.toString(16).padStart(2, "0");
+ b = b.toString(16).padStart(2, "0");
+
+ return `#${r}${g}${b}`;
+};
+
+/**
+ * @component
+ * @param {Object} props
+ * @param {string} props.label - The label of the label
+ * @param {string} props.color - The color of the label, specified in #RRGGBB format
+ * @returns {JSX.Element}
+ * @example
+ * // Render a red label
+ *
+ */
+
+const ColoredLabel = ({ label, color }) => {
+ const theme = useTheme();
+ // If an invalid color is passed, default to the labelGray color
+ if (
+ typeof color !== "string" ||
+ !/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)
+ ) {
+ color = theme.palette.labelGray.color;
+ }
+
+ // Calculate lighter shades for border and bg
+ const borderColor = lightenColor(color, 20);
+ const bgColor = lightenColor(color, 75);
+
+ return (
+
+ );
+};
+
+ColoredLabel.propTypes = {
+ label: PropTypes.string.isRequired,
+ color: PropTypes.string.isRequired,
+};
+
+/**
+ * @component
+ * @param {Object} props
+ * @param {'Seen' | 'Waiting' | 'New' | 'Active'} props.status - The status for the label
+ * @returns {JSX.Element}
+ * @example
+ * // Render an active label
+ *
+ */
+
+const StatusLabel = ({ status }) => {
+ const theme = useTheme();
+
+ const colorLookup = {
+ Seen: theme.palette.labelGray.color,
+ Waiting: theme.palette.labelRed.color,
+ New: theme.palette.labelOrange.color,
+ Active: theme.palette.labelGreen.color,
+ };
+
+ // Look up the color for the status, default to labelGray if not found
+ const color = colorLookup[status] || theme.palette.labelGray.color;
+
+ return (
+
+
+
+ );
+};
+
+StatusLabel.propTypes = {
+ status: PropTypes.oneOf(["Seen", "Waiting", "New", "Active"]),
+};
+
+export { ColoredLabel, StatusLabel };
diff --git a/Client/src/Components/Link/Link.jsx b/Client/src/Components/Link/index.jsx
similarity index 100%
rename from Client/src/Components/Link/Link.jsx
rename to Client/src/Components/Link/index.jsx
diff --git a/Client/src/Components/Link/Link.css b/Client/src/Components/Link/link.css
similarity index 100%
rename from Client/src/Components/Link/Link.css
rename to Client/src/Components/Link/link.css
diff --git a/Client/src/Components/ProgressStepper/ProgressStepper.css b/Client/src/Components/ProgressStepper/index.css
similarity index 100%
rename from Client/src/Components/ProgressStepper/ProgressStepper.css
rename to Client/src/Components/ProgressStepper/index.css
diff --git a/Client/src/Components/ProgressStepper/ProgressStepper.jsx b/Client/src/Components/ProgressStepper/index.jsx
similarity index 100%
rename from Client/src/Components/ProgressStepper/ProgressStepper.jsx
rename to Client/src/Components/ProgressStepper/index.jsx
diff --git a/Client/src/Components/Section/Section.jsx b/Client/src/Components/Section/index.jsx
similarity index 98%
rename from Client/src/Components/Section/Section.jsx
rename to Client/src/Components/Section/index.jsx
index 3ce773c81..9c5415385 100644
--- a/Client/src/Components/Section/Section.jsx
+++ b/Client/src/Components/Section/index.jsx
@@ -10,7 +10,7 @@ import {
} from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
-import Button from "../Button/Button";
+import Button from "../Button/";
import AddIcon from "@mui/icons-material/Add";
/**
diff --git a/Client/src/Pages/Demo/Demo.jsx b/Client/src/Pages/Demo/Demo.jsx
index 1c294ba5c..8ee00818b 100644
--- a/Client/src/Pages/Demo/Demo.jsx
+++ b/Client/src/Pages/Demo/Demo.jsx
@@ -1,7 +1,6 @@
import React from "react";
-import Button from "../../Components/Button/Button";
-import Link from "../../Components/Link/Link";
-import ColoredLabel from "../../Components/Label/ColoredLabel";
+import Button from "../../Components/Button/";
+import Link from "../../Components/Link";
import {
useTheme,
Switch,
@@ -14,13 +13,12 @@ import {
Tab,
Tabs,
} from "@mui/material";
-import StatusLabel from "../../Components/Label/StautsLabel";
-import Avatar from "../../Components/Avatar/Avatar";
-import ProgressStepper from "../../Components/ProgressStepper/ProgressStepper";
+import { ColoredLabel, StatusLabel } from "../../Components/Label/";
+import Avatar from "../../Components/Avatar/";
+import ProgressStepper from "../../Components/ProgressStepper";
import avatarImage from "../../assets/Images/avatar_placeholder.png";
-import Section from "../../Components/Section/Section";
+import Section from "../../Components/Section";
import { DataGrid } from "@mui/x-data-grid";
-
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
diff --git a/Client/src/Pages/Home/index.jsx b/Client/src/Pages/Home/index.jsx
index 8742da6f1..ff5cb454e 100644
--- a/Client/src/Pages/Home/index.jsx
+++ b/Client/src/Pages/Home/index.jsx
@@ -1,6 +1,7 @@
import DropdownTeamMember from "../../Components/DropdownTeamMember";
import Search from "../../Components/Search";
import "./index.css";
+import DashboardMenu from "../../Components/DashboardMenu";
const Home = () => {
return (
@@ -8,6 +9,7 @@ const Home = () => {
Home
+
>
);
};
diff --git a/README.md b/README.md
index 19a0670e1..3397a324d 100644
--- a/README.md
+++ b/README.md
@@ -28,11 +28,105 @@ BlueWave uptime monitoring application
1. Change directory to the `Server` directory
2. Install all depencies by running `npm install`
+---
+
+#### Configuration
+
+Configure the server with the following environmental variables
+
+| ENV Varialbe Name | Required/Optional | Type | Description |
+| -------------------- | ----------------- | ------ | --------------------------------- |
+| DB_CONNECTION_STRING | Required | string | Specfies URL for MongoDB Database |
+| MAILERSEND_API_KEY | Required | string | Specfies API KEY for MailerSend service |
+| SYSTEM_EMAIL_ADDRESS | Required | string | Specfies System email to be used in emailing service |
+
+---
+
#### Starting the Development Server
1. run `npm run dev` to start the development server
+---
+
+#### Endpoints
+
+All endpoints return a response in this format:
+
+| Name | Type | Notes |
+| ------- | --------- | ----------------------------- |
+| success | `boolean` | Success or failure of request |
+| msg | `string` | Message describing response |
+| data | `Object` | Arbitrary Payload |
+
+Example:
+
+```
+{success: true, msg: "Successful Request", data: {test: testData}}
+```
+
+##### Data Types
+
+###### Monitor
+
+| Name | Type | Notes |
+| ----------- | --------- | ---------------------------------------- |
+| userId | `string` | Unique ID identifying monitor creator |
+| name | `string` | Name of the monitor |
+| description | `string` | Description of the monitor |
+| url | `string` | Url the monitor will ping |
+| isActive | `boolean` | Whether or not the monitor is active |
+| interval | `integer` | Interval with which to ping monitor (ms) |
+| updatedAt | `Date` | Last time the monitor was updated |
+| CreatedAt | `Date` | When the monitor was updated |
+
+---
+
+##### GET /api/v1/monitors
+
+###### Response
+
+| Status Code | Type | Description |
+| ----------- | --------------------- | -------------------------------- |
+| 200 | Response with Payload | Response with a list of monitors |
+
+###### Payload
+
+| Type | Notes |
+| ---------------- | ----------------- |
+| `Array[Monitor]` | Array of monitors |
+
+---
+
+##### GET /api/v1/monitor/:id
+
+###### Response
+
+| Status Code | Type | Description |
+| ----------- | --------------------- | ------------------------------ |
+| 200 | Response with Payload | Response with a single monitor |
+
+###### Payload
+
+| Type | Notes |
+| --------- | --------------------------------------------------- |
+| `Monitor` | Single monitor with the id in the request parameter |
+
+---
+
+##### GET /api/v1/monitors/user/:userId
+
+| Status Code | Type | Description |
+| ----------- | --------------------- | -------------------------------- |
+| 200 | Response with Payload | Response with a list of monitors |
+
+###### Payload
+
+| Type | Notes |
+| ---------------- | ------------------------------------------------------------------ |
+| `Array[Monitor]` | Array of monitors created by user with userId specified in request |
+
## Contributors
+
diff --git a/Server/.gitignore b/Server/.gitignore
index f469421d1..3fdb59050 100644
--- a/Server/.gitignore
+++ b/Server/.gitignore
@@ -1,3 +1,3 @@
node_modules
.env
-utils
\ No newline at end of file
+*.log
\ No newline at end of file
diff --git a/Server/configs/db.js b/Server/configs/db.js
index 174978ca3..ba3c8b660 100644
--- a/Server/configs/db.js
+++ b/Server/configs/db.js
@@ -1,18 +1,15 @@
-const mongoose = require('mongoose')
+const PORT = process.env.PORT || 5000;
-const PORT = process.env.PORT || 5000
+const connectDbAndRunServer = async (app, db) => {
+ try {
+ await db.connect();
+ app.listen(PORT, () => {
+ console.log(`server started on port:${PORT}`);
+ });
+ } catch (error) {
+ console.log("Failed to connect to DB");
+ console.error(error);
+ }
+};
-const connectDbAndRunServer = async (app) => {
- await mongoose.connect(process.env.DB_CONNECTION_STRING).then(() => {
- console.log("DB connected")
- // run server
- app.listen(PORT, () => {
- console.log(`server started on port:${PORT}`)
- })
- }).catch((err) => {
- console.log(err)
- })
-}
-
-
-module.exports = { connectDbAndRunServer }
\ No newline at end of file
+module.exports = { connectDbAndRunServer };
diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js
index f1a2c6ecd..eba12db7d 100644
--- a/Server/controllers/authController.js
+++ b/Server/controllers/authController.js
@@ -1,32 +1,50 @@
-const express = require('express')
-const UserModel = require('../models/user')
-const { authValidation } = require('../validation/joi')
+const express = require("express");
+const UserModel = require("../models/user");
+const { registerValidation } = require("../validation/joi");
+const logger = require('../utils/logger')
/**
* @function
- * @param {express.Request} req
- * @param {express.Response} res
+ * @param {express.Request} req
+ * @param {express.Response} res
* @returns {{success: Boolean, msg: String}}
*/
const registerController = async (req, res) => {
- // joi validation
- const { error } = await authValidation.validateAsync(req.body);
- if (error) return res.status(400).json({ success: false, msg: error.details[0].message });
+ // joi validation
+ try {
+ await registerValidation.validateAsync(req.body);
+ } catch (error) {
+ return res
+ .status(400)
+ .json({ success: false, msg: error.details[0].message });
+ }
- // Check if the user exists
- const isUser = await UserModel.findOne({ email: req.body.email });
- if (isUser) return res.status(400).json({ success: false, msg: "User already exists!" })
-
- try {
- // Create a new user
- const newUser = await UserModel.create({ ...req.body });
-
- // Send an email to user
- // Will add this later
- } catch (error) {
- return res.status(500).json({success:false,msg:'Something went wrong!'})
+ // Check if the user exists
+ try {
+ const isUser = await req.db.getUserByEmail(req, res);
+ if (isUser) {
+ logger.warning("User already exists!", { "service": "auth", "userId": isUser._id });
+ return res
+ .status(400)
+ .json({ success: false, msg: "User already exists!" });
}
+ } catch (error) {
+ logger.error(error.message, { "service": "auth" });
+ return res.status(500).json({ success: false, msg: error.message });
+ }
-}
-module.exports = {registerController}
\ No newline at end of file
+ try {
+ // Create a new user
+ const newUser = await req.db.insertUser(req, res);
+ // TODO: Send an email to user
+ // Will add this later
+ logger.info("New user created!", { "service": "auth", "userId": newUser._id });
+ return res.json({ success: true, msg: "User created}", data: newUser });
+ } catch (error) {
+ logger.error(error.message, { "service": "auth" });
+ return res.status(500).json({ success: false, msg: error.message });
+ }
+};
+
+module.exports = { registerController };
diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js
new file mode 100644
index 000000000..c85da0f9d
--- /dev/null
+++ b/Server/controllers/monitorController.js
@@ -0,0 +1,61 @@
+const {
+ getMonitorsByIdValidation,
+ getMonitorsByUserIdValidation,
+} = require("../validation/joi");
+const logger = require('../utils/logger')
+
+// Gets all monitors
+const getAllMonitors = async (req, res) => {
+ 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 });
+ }
+};
+
+// Get a monitor by ID
+const getMonitorById = async (req, res) => {
+ const { error } = getMonitorsByIdValidation.validate(req.params);
+ if (error) {
+ return res
+ .status(400)
+ .json({ success: false, msg: error.details[0].message });
+ }
+
+ try {
+ const monitorId = req.params.monitorId;
+ const monitor = await req.db.getMonitorById(monitorId);
+ 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 });
+ }
+};
+
+// Gets a monitor by user ID
+const getMonitorsByUserId = async (req, res) => {
+ const { error } = getMonitorsByUserIdValidation.validate(req.params);
+ if (error) {
+ return res
+ .status(400)
+ .json({ success: false, msg: error.details[0].message });
+ }
+
+ try {
+ const userId = req.params.userId;
+ const monitors = await req.db.getMonitorsByUserId(userId);
+ logger.info(`Monitors for user ${userId} found`, { "service": "monitor", "userId":userId });
+ 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 });
+ }
+};
+
+module.exports = { getAllMonitors, getMonitorById, getMonitorsByUserId };
diff --git a/Server/db/FakeDb.js b/Server/db/FakeDb.js
new file mode 100644
index 000000000..8cf05cf89
--- /dev/null
+++ b/Server/db/FakeDb.js
@@ -0,0 +1,110 @@
+// **************************
+// The idea here is to provide a layer of abstraction between the database and whoever is using it.
+// Instead of directly calling mongoose methods, we can call the methods on the DB object.
+// If this were Typescript or Java or Golang an interface would be implemented to ensure the methods are available.
+// But we do the best we can with Javascript.
+//
+// If the methods are consistent all we have to do to swap out one DB for another is simply change the import.
+//
+// Example:
+// We start with the fake DB:
+//
+// const db = require("../db/FakeDb");
+// const monitors = await db.getAllMonitors();
+//
+// And when we want to swtich to a real DB, all we have to do is swap the import
+//
+// const db = require("../db/MongoDb");
+// const monitors = await db.getAllMonitors();
+//
+// The rest of the code is the same, as all the `db` methods are standardized.
+// **************************
+
+const Monitor = require("../models/Monitor");
+const UserModel = require("../models/user");
+
+const FAKE_MONITOR_DATA = [];
+const USERS = [];
+
+for (let i = 0; i < 10; i++) {
+ FAKE_MONITOR_DATA.push(
+ new Monitor({
+ userId: i % 2 === 0 ? 1 : 2,
+ name: `Monitor ${i}`,
+ description: `Description for Monitor ${i}`,
+ url: `https://monitor${i}.com`,
+ isActive: true,
+ interval: 60000,
+ updated_at: new Date(),
+ created_at: new Date(),
+ })
+ );
+}
+
+const connect = async () => {
+ try {
+ await console.log("Connected to FakeDB");
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+const insertUser = async (req, res) => {
+ try {
+ const newUser = new UserModel({ ...req.body });
+ USERS.push(newUser);
+ return newUser;
+ } catch (error) {
+ throw error;
+ }
+};
+
+const getUserByEmail = async (req, res) => {
+ const email = req.body.email;
+ try {
+ const idx = USERS.findIndex((user) => {
+ return user.email === email;
+ });
+ if (idx === -1) {
+ return null;
+ }
+ return USERS[idx];
+ } catch (error) {
+ throw new Error(`User with email ${email} not found`);
+ }
+};
+
+const getAllMonitors = async () => {
+ return FAKE_MONITOR_DATA;
+};
+
+const getMonitorById = async (monitorId) => {
+ const idx = FAKE_MONITOR_DATA.findIndex((monitor) => {
+ return monitor.id === monitorId;
+ });
+ if (idx === -1) {
+ throw new Error(`Monitor with id ${monitorId} not found`);
+ }
+ return FAKE_MONITOR_DATA[idx];
+};
+
+const getMonitorsByUserId = async (userId) => {
+ const userMonitors = FAKE_MONITOR_DATA.filter((monitor) => {
+ return monitor.userId === userId;
+ });
+
+ if (userMonitors.length === 0) {
+ throw new Error(`Monitors for user ${userId} not found`);
+ }
+
+ return userMonitors;
+};
+
+module.exports = {
+ connect,
+ insertUser,
+ getUserByEmail,
+ getAllMonitors,
+ getMonitorById,
+ getMonitorsByUserId,
+};
diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js
new file mode 100644
index 000000000..e3edbb7e2
--- /dev/null
+++ b/Server/db/MongoDB.js
@@ -0,0 +1,71 @@
+const Monitor = require("../models/Monitor");
+const mongoose = require("mongoose");
+const UserModel = require("../models/user");
+
+const connect = async () => {
+ try {
+ await mongoose.connect(process.env.DB_CONNECTION_STRING);
+ console.log("Connected to MongoDB");
+ } catch (error) {
+ console.error("Failed to connect to MongoDB");
+ throw error;
+ }
+};
+
+const insertUser = async (req, res) => {
+ try {
+ const newUser = await UserModel.create({ ...req.body }).select('-password');
+ return newUser;
+ } catch (error) {
+ throw error;
+ }
+};
+
+const getUserByEmail = async (req, res) => {
+ try {
+ // Returns null if no user is found
+ const user = await UserModel.findOne({ email: req.body.email }).select('-password');
+ return user;
+ } catch (error) {
+ throw error;
+ }
+};
+
+// Gets all monitors
+const getAllMonitors = async (req, res) => {
+ try {
+ const monitors = await Monitor.find();
+ return monitors;
+ } catch (error) {
+ throw error;
+ }
+};
+
+// Get a monitor by ID
+const getMonitorById = async (req, res) => {
+ try {
+ const monitor = await Monitor.findById(req.params.monitorId);
+ return monitor;
+ } catch (error) {
+ throw error;
+ }
+};
+
+// Gets a monitor by user ID
+const getMonitorsByUserId = async (req, res) => {
+ try {
+ const monitors = await Monitor.find({ userId: req.params.userId });
+ return monitors;
+ } catch (error) {
+ throw error;
+ }
+};
+
+module.exports = {
+ connect,
+ insertUser,
+ getUserByEmail,
+ getAllMonitors,
+ getMonitorById,
+ getMonitorsByUserId,
+};
diff --git a/Server/index.js b/Server/index.js
index 173c2c78d..7e68c72f2 100644
--- a/Server/index.js
+++ b/Server/index.js
@@ -1,42 +1,72 @@
-const express = require('express')
-const helmet = require('helmet')
-const cors = require('cors')
-const authRouter = require('./routes/authRoute')
-const { connectDbAndRunServer } = require('./configs/db')
-require('dotenv').config()
+const express = require("express");
+const helmet = require("helmet");
+const cors = require("cors");
+const authRouter = require("./routes/authRoute");
+const monitorRouter = require("./routes/monitorRoute");
+const { connectDbAndRunServer } = require("./configs/db");
+require("dotenv").config();
+const logger = require("./utils/logger");
// const { sendEmail } = require('./utils/sendEmail')
+// **************************
+// Here is where we can swap out DBs easily. Spin up a mongoDB instance and try it out.
+// Simply comment out the FakeDB and uncomment the MongoDB or vice versa.
+// We can easily swap between any type of data source as long as the methods are implemented
+//
+// FakeDB
+// const db = require("./db/FakeDb");
+//
+// MongoDB
+const db = require("./db/MongoDB");
+//
+// **************************
+
/**
* NOTES
- * Email Service will be added
+ * Email Service will be added
* Logger Service will be added (Winston or similar)
*/
-const app = express()
+const app = express();
// middlewares
-app.use(cors(
- //We will add configuration later
-))
-app.use(express.json())
-app.use(helmet())
+app.use(
+ cors()
+ //We will add configuration later
+);
+app.use(express.json());
+app.use(helmet());
+
+// **************************
+// Make DB accessible anywhere we have a Request object
+// By adding the DB to the request object, we can access it in any route
+// Thus we do not need to import it in every route file, and we can easily swap out DBs as there is only one place to change it
+// **************************
+app.use((req, res, next) => {
+ req.db = db;
+ next();
+});
//routes
-app.use('/api/v1/auth', authRouter);
+app.use("/api/v1/auth", authRouter);
+
+app.use("/api/v1/monitors", monitorRouter);
// Testing email service
// app.use('/sendEmail', async (req, res) => {
-// const response = sendEmail('veysel.boybay@outlook.com', ['veysel.boybay@bluewavelabs.ca'], 'Testing email service', 'Testing Bluewavelabs
');
+// const response = sendEmail(['veysel.boybay@bluewavelabs.ca'], 'Testing email service', 'Testing Bluewavelabs
');
// console.log(response);
// })
//health check
-app.use('/api/v1/healthy', (req, res) => {
- try {
- return res.status(200).json({message:"Healthy"})
- } catch (error) {
- return res.status(500).json({message:error.message})
- }
-})
+app.use("/api/v1/healthy", (req, res) => {
+ try {
+ logger.info("Checking Health of the server.");
+ return res.status(200).json({ message: "Healthy" });
+ } catch (error) {
+ logger.error(error.message);
+ return res.status(500).json({ message: error.message });
+ }
+});
-connectDbAndRunServer(app);
+connectDbAndRunServer(app, db);
diff --git a/Server/models/Monitor.js b/Server/models/Monitor.js
new file mode 100644
index 000000000..513d4e2be
--- /dev/null
+++ b/Server/models/Monitor.js
@@ -0,0 +1,37 @@
+const mongoose = require("mongoose");
+
+const MonitorSchema = mongoose.Schema({
+ userId: {
+ type: String,
+ },
+ name: {
+ type: String,
+ required: true,
+ },
+ description: {
+ type: String,
+ },
+ url: {
+ type: String,
+ required: true,
+ },
+ isActive: {
+ type: Boolean,
+ default: true,
+ },
+ interval: {
+ // in milliseconds
+ type: Number,
+ default: 60000,
+ },
+ updated_at: {
+ type: Date,
+ default: Date.now,
+ },
+ created_at: {
+ type: Date,
+ default: Date.now,
+ },
+});
+
+module.exports = mongoose.model("Monitor", MonitorSchema);
diff --git a/Server/models/user.js b/Server/models/user.js
index 27d623179..822c303d8 100644
--- a/Server/models/user.js
+++ b/Server/models/user.js
@@ -1,45 +1,51 @@
-const mongoose = require('mongoose')
-const bcrypt = require('bcrypt')
+const mongoose = require("mongoose");
+const bcrypt = require("bcrypt");
const UserSchema = mongoose.Schema({
- username: {
- type: String
- },
- email: {
- type: String,
- required: 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
- }
-})
+ 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,
+ },
+ updated_at: {
+ type: Date,
+ default: Date.now,
+ },
+ created_at: {
+ type: Date,
+ default: Date.now,
+ },
+});
-UserSchema.pre('save', async function (next) {
- if (!this.isModified('password')) {
- next()
- }
- const salt = bcrypt.genSalt(10);
- this.password = await bcrypt.hash(this.password, salt);
-})
+UserSchema.pre("save", async function (next) {
+ if (!this.isModified("password")) {
+ next();
+ }
+ const salt = await bcrypt.genSalt(10); //genSalt is asynchronous, need to wait
+ this.password = bcrypt.hash(this.password, salt);
+});
-module.exports = mongoose.model('User',UserSchema)
\ No newline at end of file
+module.exports = mongoose.model("User", UserSchema);
diff --git a/Server/package-lock.json b/Server/package-lock.json
index 19995c3d5..495d5bc2e 100644
--- a/Server/package-lock.json
+++ b/Server/package-lock.json
@@ -16,12 +16,31 @@
"helmet": "^7.1.0",
"joi": "^17.13.1",
"mailersend": "^2.2.0",
- "mongoose": "^8.3.3"
+ "mongoose": "^8.3.3",
+ "winston": "^3.13.0"
},
"devDependencies": {
"nodemon": "3.1.0"
}
},
+ "node_modules/@colors/colors": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
+ "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/@dabh/diagnostics": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
+ "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
+ "dependencies": {
+ "colorspace": "1.1.x",
+ "enabled": "2.0.x",
+ "kuler": "^2.0.0"
+ }
+ },
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -94,6 +113,11 @@
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
},
+ "node_modules/@types/triple-beam": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
+ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="
+ },
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
@@ -199,6 +223,11 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
+ "node_modules/async": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
+ "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -339,6 +368,37 @@
"node": ">=10"
}
},
+ "node_modules/color": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
+ "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
+ "dependencies": {
+ "color-convert": "^1.9.3",
+ "color-string": "^1.6.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+ },
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
@@ -347,6 +407,15 @@
"color-support": "bin.js"
}
},
+ "node_modules/colorspace": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
+ "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
+ "dependencies": {
+ "color": "^3.1.3",
+ "text-hex": "1.0.x"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -476,6 +545,11 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
+ "node_modules/enabled": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
+ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
+ },
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -562,6 +636,11 @@
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
+ "node_modules/fecha": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
+ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
+ },
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -591,6 +670,11 @@
"node": ">= 0.8"
}
},
+ "node_modules/fn.name": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
+ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -902,6 +986,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -992,6 +1081,32 @@
"node": ">=12.0.0"
}
},
+ "node_modules/kuler": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
+ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
+ },
+ "node_modules/logform": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz",
+ "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==",
+ "dependencies": {
+ "@colors/colors": "1.6.0",
+ "@types/triple-beam": "^1.3.2",
+ "fecha": "^4.2.0",
+ "ms": "^2.1.1",
+ "safe-stable-stringify": "^2.3.1",
+ "triple-beam": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
+ "node_modules/logform/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -1441,6 +1556,14 @@
"wrappy": "1"
}
},
+ "node_modules/one-time": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
+ "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
+ "dependencies": {
+ "fn.name": "1.x.x"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1594,6 +1717,14 @@
}
]
},
+ "node_modules/safe-stable-stringify": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz",
+ "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -1708,6 +1839,14 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@@ -1728,6 +1867,14 @@
"memory-pager": "^1.0.2"
}
},
+ "node_modules/stack-trace": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
+ "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -1796,6 +1943,11 @@
"node": ">=10"
}
},
+ "node_modules/text-hex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
+ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -1839,6 +1991,14 @@
"node": ">=14"
}
},
+ "node_modules/triple-beam": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
+ "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -1919,6 +2079,40 @@
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
+ "node_modules/winston": {
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz",
+ "integrity": "sha512-rwidmA1w3SE4j0E5MuIufFhyJPBDG7Nu71RkZor1p2+qHvJSZ9GYDA81AyleQcZbh/+V6HjeBdfnTZJm9rSeQQ==",
+ "dependencies": {
+ "@colors/colors": "^1.6.0",
+ "@dabh/diagnostics": "^2.0.2",
+ "async": "^3.2.3",
+ "is-stream": "^2.0.0",
+ "logform": "^2.4.0",
+ "one-time": "^1.0.0",
+ "readable-stream": "^3.4.0",
+ "safe-stable-stringify": "^2.3.1",
+ "stack-trace": "0.0.x",
+ "triple-beam": "^1.3.0",
+ "winston-transport": "^4.7.0"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
+ "node_modules/winston-transport": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz",
+ "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==",
+ "dependencies": {
+ "logform": "^2.3.2",
+ "readable-stream": "^3.6.0",
+ "triple-beam": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/Server/package.json b/Server/package.json
index 53f0fe429..6a082cbdc 100644
--- a/Server/package.json
+++ b/Server/package.json
@@ -18,7 +18,8 @@
"helmet": "^7.1.0",
"joi": "^17.13.1",
"mailersend": "^2.2.0",
- "mongoose": "^8.3.3"
+ "mongoose": "^8.3.3",
+ "winston": "^3.13.0"
},
"devDependencies": {
"nodemon": "3.1.0"
diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js
new file mode 100644
index 000000000..e12982905
--- /dev/null
+++ b/Server/routes/monitorRoute.js
@@ -0,0 +1,7 @@
+const router = require("express").Router();
+const monitorController = require("../controllers/monitorController");
+router.get("/", monitorController.getAllMonitors);
+router.get("/:monitorId", monitorController.getMonitorById);
+router.get("/user/:userId", monitorController.getMonitorsByUserId);
+
+module.exports = router;
diff --git a/Server/utils/logger.js b/Server/utils/logger.js
new file mode 100644
index 000000000..a8f65527c
--- /dev/null
+++ b/Server/utils/logger.js
@@ -0,0 +1,26 @@
+const winston = require('winston');
+/**
+ * @module
+ * @example
+ * logger.info("Registered a new user!")
+ * logger.warn("User not found!")
+ * logger.error("Cannot save")
+ * @example
+ * "Specify service and ID in the log if applicable."
+ * logger.error("Descriptive Message",{"service":"monitor","monitorId":"123456"})
+ * logger.error("Incorrect Credentials",{"service":"Auth","userId":"654321"})
+ * logger.error("User not found!",{"service":"Auth"})
+ */
+const logger = winston.createLogger({
+ level: 'info',
+ format: winston.format.combine(
+ winston.format.timestamp(),
+ winston.format.json()
+ ),
+ transports: [
+ new winston.transports.Console(),
+ new winston.transports.File({ filename: 'app.log' }),
+ ],
+});
+
+module.exports = logger;
\ No newline at end of file
diff --git a/Server/utils/sendEmail.js b/Server/utils/sendEmail.js
new file mode 100644
index 000000000..712a651bd
--- /dev/null
+++ b/Server/utils/sendEmail.js
@@ -0,0 +1,43 @@
+const { MailerSend, EmailParams, Sender, Recipient } = require('mailersend')
+const logger = require('../utils/logger')
+
+
+const mailersend = new MailerSend({
+ apiKey: process.env.MAILERSEND_API_KEY,
+})
+/**
+ * @function
+ * @param {[string]} receivers - takes an array of strings
+ * @param {string} subject - takes a single string
+ * @param {string} contentHTML - takes a single string that contains HTML
+ * @returns {JSON}
+ * @example
+ * sendEmail(['veysel@bluewavelabs.ca','alex@bluewavelabs.ca','monzer@bluewavelabs.ca'],'Testing Email Servide','BlueWaveLabs
')
+ */
+// TODO: from email should be in .env file
+const sendEmail = async (receivers,subject,contentHTML) => {
+ // Sender
+ const from = process.env.SYSTEM_EMAIL_ADDRESS;
+ const sender = new Sender(from);
+ // receivers
+ let recipients = []
+ receivers.map(email => recipients.push(new Recipient(email)));
+ // Set params
+ const emailParams = new EmailParams()
+ .setFrom(sender)
+ .setTo(recipients)
+ .setSubject(subject)
+ .setHtml(contentHTML);
+
+ try {
+ const response = await mailersend.email.send(emailParams);
+ logger.info("Email sent to receivers!",{"service":"Email"})
+ return response;
+
+ } catch (error) {
+ logger.error(error.body,{"service":"email"})
+ console.log(error.body)
+ }
+}
+
+module.exports = {sendEmail}
\ No newline at end of file
diff --git a/Server/validation/joi.js b/Server/validation/joi.js
index d28f9b7da..874daff93 100644
--- a/Server/validation/joi.js
+++ b/Server/validation/joi.js
@@ -1,10 +1,28 @@
-const joi = require('joi')
+const joi = require("joi");
+const user = require("../models/user");
-
-const authValidation = joi.object({
- email: joi.string().email().required(),
- password: joi.string().min(8).required(),
+const loginValidation = joi.object({
+ email: joi.string().email().required(),
+ password: joi.string().min(8).required(),
+});
+const registerValidation = joi.object({
+ firstname: joi.string().required(),
+ lastname: joi.string().required(),
+ email: joi.string().email().required(),
+ password: joi.string().min(8).required(),
});
+const getMonitorsByIdValidation = joi.object({
+ monitorId: joi.string().required(),
+});
-module.exports = {authValidation}
\ No newline at end of file
+const getMonitorsByUserIdValidation = joi.object({
+ userId: joi.string().required(),
+});
+
+module.exports = {
+ loginValidation,
+ registerValidation,
+ getMonitorsByIdValidation,
+ getMonitorsByUserIdValidation,
+};