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 ( +
+
+ +
+ Monitors +
+
+ +
+ +
+ Incidents +
+
+ +
+ +
+ Status Pages +
+
+ +
+ +
+ Integrations +
+
+ +
+ +
+ Settings +
+
+
+ ); +} + +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, +};