From 53bd2cacdc2b468100f7add679923c202e4b925a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 2 Jul 2024 10:50:18 -0700 Subject: [PATCH 01/19] Added local state --- Client/src/App.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 4a05c1de3..be667395a 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -19,8 +19,11 @@ import SetNewPassword from "./Pages/SetNewPassword"; import NewPasswordConfirmed from "./Pages/NewPasswordConfirmed"; import ToastComponent from "./Components/Toast"; import ProtectedRoute from "./Components/ProtectedRoute"; +import { useState, useEffect } from "react"; function App() { + const [adminExists, setAdminExists] = useState(false); // Assuming admin exists by default + return ( <> From cee88f63d9fef75c15786d283df86d88099227d6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 2 Jul 2024 11:14:12 -0700 Subject: [PATCH 02/19] Make axios request --- Client/src/App.jsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Client/src/App.jsx b/Client/src/App.jsx index be667395a..193fbcb36 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -20,10 +20,26 @@ import NewPasswordConfirmed from "./Pages/NewPasswordConfirmed"; import ToastComponent from "./Components/Toast"; import ProtectedRoute from "./Components/ProtectedRoute"; import { useState, useEffect } from "react"; +import axios from "axios"; function App() { + const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL; const [adminExists, setAdminExists] = useState(false); // Assuming admin exists by default + useEffect(() => { + const checkAdminExists = async () => { + try { + // Replace this URL with your actual API endpoint + const response = await axios.get(`${BASE_URL}/auth/users/admin`); + console.log(response); + } catch (error) { + console.error("Failed to check admin existence:", error); + } + }; + + checkAdminExists(); + }, []); + return ( <> From 299dadd24d8869c1f758662572bb14ffb6e589d0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 2 Jul 2024 15:54:49 -0700 Subject: [PATCH 03/19] Check for admin, redirect to register/login as appropriate --- Client/src/App.jsx | 22 +--------------------- Client/src/Pages/Login/index.jsx | 15 +++++++++++++++ Client/src/Pages/Register/index.jsx | 9 +++++++++ 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 193fbcb36..26f75cef8 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -1,4 +1,4 @@ -import { Routes, Route } from "react-router-dom"; +import { Routes, Route, Navigate } from "react-router-dom"; // import "./App.css"; import NotFound from "./Pages/NotFound"; import Login from "./Pages/Login"; @@ -17,29 +17,9 @@ import ForgotPassword from "./Pages/ForgotPassword"; import CheckEmail from "./Pages/CheckEmail"; import SetNewPassword from "./Pages/SetNewPassword"; import NewPasswordConfirmed from "./Pages/NewPasswordConfirmed"; -import ToastComponent from "./Components/Toast"; import ProtectedRoute from "./Components/ProtectedRoute"; -import { useState, useEffect } from "react"; -import axios from "axios"; function App() { - const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL; - const [adminExists, setAdminExists] = useState(false); // Assuming admin exists by default - - useEffect(() => { - const checkAdminExists = async () => { - try { - // Replace this URL with your actual API endpoint - const response = await axios.get(`${BASE_URL}/auth/users/admin`); - console.log(response); - } catch (error) { - console.error("Failed to check admin existence:", error); - } - }; - - checkAdminExists(); - }, []); - return ( <> diff --git a/Client/src/Pages/Login/index.jsx b/Client/src/Pages/Login/index.jsx index cebcaa39b..4beb4d2c8 100644 --- a/Client/src/Pages/Login/index.jsx +++ b/Client/src/Pages/Login/index.jsx @@ -13,6 +13,8 @@ import PasswordTextField from "../../Components/TextFields/Password/PasswordText import { loginValidation } from "../../Validation/validation"; import { login } from "../../Features/Auth/authSlice"; import { useDispatch } from "react-redux"; +import axios from "axios"; +const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL; const Login = () => { const dispatch = useDispatch(); @@ -30,6 +32,19 @@ const Login = () => { password: "", }); + useEffect(() => { + axios + .get(BASE_URL + "/auth/users/admin") + .then((response) => { + if (response.data.data === false) { + navigate("/register"); + } + }) + .catch((error) => { + console.log(error); + }); + }, [navigate]); + useEffect(() => { const { error } = loginValidation.validate(form, { abortEarly: false, diff --git a/Client/src/Pages/Register/index.jsx b/Client/src/Pages/Register/index.jsx index 5d08e0c0d..709f46ca9 100644 --- a/Client/src/Pages/Register/index.jsx +++ b/Client/src/Pages/Register/index.jsx @@ -14,6 +14,7 @@ import { registerValidation } from "../../Validation/validation"; import { useDispatch } from "react-redux"; import { register } from "../../Features/Auth/authSlice"; +import axios from "axios"; const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL; @@ -38,6 +39,14 @@ const Register = () => { password: "", }); + useEffect(() => { + axios.get(BASE_URL + "/auth/users/admin").then((response) => { + if (response.data.data === true) { + navigate("/login"); + } + }); + }); + useEffect(() => { const { error } = registerValidation.validate(form, { abortEarly: false, From 033101c8840f4fc78310426d2063c8a93a40cff4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 2 Jul 2024 15:57:53 -0700 Subject: [PATCH 04/19] Added dependency array and catch block --- Client/src/Pages/Register/index.jsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Client/src/Pages/Register/index.jsx b/Client/src/Pages/Register/index.jsx index 709f46ca9..4aaf5afc8 100644 --- a/Client/src/Pages/Register/index.jsx +++ b/Client/src/Pages/Register/index.jsx @@ -40,12 +40,17 @@ const Register = () => { }); useEffect(() => { - axios.get(BASE_URL + "/auth/users/admin").then((response) => { - if (response.data.data === true) { - navigate("/login"); - } - }); - }); + axios + .get(BASE_URL + "/auth/users/admin") + .then((response) => { + if (response.data.data === true) { + navigate("/login"); + } + }) + .catch((error) => { + console.log(error); + }); + }, [navigate]); useEffect(() => { const { error } = registerValidation.validate(form, { From c2b47e5cc41eb58102e2baad1332db774579910b Mon Sep 17 00:00:00 2001 From: M M Date: Tue, 2 Jul 2024 20:24:51 -0700 Subject: [PATCH 05/19] Placed the TableContainer in a box to better match figma design. --- Client/src/Pages/Incidents/index.jsx | 73 ++++++++++++++++------------ 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index a2cb59517..54e857b0d 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -1,4 +1,4 @@ -import { styled } from '@mui/material/styles'; +import { styled, useTheme } from '@mui/material/styles'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell, { tableCellClasses } from '@mui/material/TableCell'; @@ -6,12 +6,12 @@ import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; +import Box from '@mui/material/Box'; import { StatusLabel } from "../../Components/Label/"; - const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { - backgroundColor: theme.palette.action.hover, + backgroundColor: theme.palette.action.hover, color: theme.palette.common.black, }, [`&.${tableCellClasses.body}`]: { @@ -21,18 +21,18 @@ const StyledTableCell = styled(TableCell)(({ theme }) => ({ const StyledTableRow = styled(TableRow)(({ theme }) => ({ '&:nth-of-type(1)': { - backgroundColor: theme.palette.action.hover, + backgroundColor: theme.palette.action.hover, }, '&:nth-of-type(n+1)': { - backgroundColor: theme.palette.common.white, + backgroundColor: theme.palette.common.white, }, '&:last-child td, &:last-child th': { border: 0, }, })); -function createData(name, calories, fat, carbs, protein) { - return { name, calories, fat, carbs, protein }; +function createData(name, date, message) { + return { name, date, message }; } const rows = [ @@ -43,30 +43,43 @@ const rows = [ createData(, '2024-03-14 21:41:09', 'HTTP 350 - NOK'), ]; - export default function CustomizedTables() { + const theme = useTheme(); + return ( - - - - - Status - Date & Time - Message - - - - {rows.map((row, index) => ( - - - {row.name} - - {row.calories} - {row.fat} - - ))} - -
-
+ + + + + + Status + Date & Time + Message + + + + {rows.map((row, index) => ( + + + {row.name} + + {row.date} + {row.message} + + ))} + +
+
+
); } From c9ca639f237644e7bc902d9b27ecdd6e67d4bb98 Mon Sep 17 00:00:00 2001 From: M M Date: Tue, 2 Jul 2024 20:43:39 -0700 Subject: [PATCH 06/19] Added the filter by status dropdown to the page. --- Client/src/Pages/Incidents/index.jsx | 33 ++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 54e857b0d..1246962cd 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -1,3 +1,5 @@ +import TextField from '@mui/material/TextField'; +import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; import { styled, useTheme } from '@mui/material/styles'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; @@ -20,10 +22,10 @@ const StyledTableCell = styled(TableCell)(({ theme }) => ({ })); const StyledTableRow = styled(TableRow)(({ theme }) => ({ - '&:nth-of-type(1)': { + '&:nth-of-type(odd)': { backgroundColor: theme.palette.action.hover, }, - '&:nth-of-type(n+1)': { + '&:nth-of-type(even)': { backgroundColor: theme.palette.common.white, }, '&:last-child td, &:last-child th': { @@ -31,6 +33,17 @@ const StyledTableRow = styled(TableRow)(({ theme }) => ({ }, })); +const filterOptions = createFilterOptions({ + matchFrom: 'start', + stringify: (option) => option.title, +}); + +const top100Films = [ + { title: 'The Shawshank Redemption', year: 1994 }, + { title: 'The Dark Knight', year: 2008 }, + { title: '12 Angry Men', year: 1957 }, +]; + function createData(name, date, message) { return { name, date, message }; } @@ -58,6 +71,9 @@ export default function CustomizedTables() { }, }} > + + + @@ -83,3 +99,16 @@ export default function CustomizedTables() { ); } + +function Filter() { + return ( + option.title} + filterOptions={filterOptions} + sx={{ width: 300 }} + renderInput={(params) => } + /> + ); +} From 8453542777003bdec76fc0de2e62132c26c3bbf7 Mon Sep 17 00:00:00 2001 From: M M Date: Tue, 2 Jul 2024 20:46:28 -0700 Subject: [PATCH 07/19] Got rid of the adornment at the end. --- Client/src/Pages/Incidents/index.jsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 1246962cd..0e11bc237 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -1,3 +1,4 @@ +import * as React from 'react'; import TextField from '@mui/material/TextField'; import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; import { styled, useTheme } from '@mui/material/styles'; @@ -108,7 +109,14 @@ function Filter() { getOptionLabel={(option) => option.title} filterOptions={filterOptions} sx={{ width: 300 }} - renderInput={(params) => } + disableClearable // This disables the clearable (x) button + renderInput={(params) => ( + + )} /> ); } From f7e8e69168f11b9d326231674fde5a43f746597b Mon Sep 17 00:00:00 2001 From: M M Date: Tue, 2 Jul 2024 21:07:15 -0700 Subject: [PATCH 08/19] Added icon to filter by status dropdown. --- Client/src/Pages/Incidents/index.jsx | 35 ++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 0e11bc237..adaad7849 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -11,6 +11,7 @@ import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; import Box from '@mui/material/Box'; import { StatusLabel } from "../../Components/Label/"; +import TuneIcon from '@mui/icons-material/Tune'; // Import TuneIcon const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { @@ -39,10 +40,10 @@ const filterOptions = createFilterOptions({ stringify: (option) => option.title, }); -const top100Films = [ - { title: 'The Shawshank Redemption', year: 1994 }, - { title: 'The Dark Knight', year: 2008 }, - { title: '12 Angry Men', year: 1957 }, +const titles = [ + { title: 'Down' }, + { title: 'Cannot resolve'}, + { title: 'Clear / show all'}, ]; function createData(name, date, message) { @@ -72,7 +73,7 @@ export default function CustomizedTables() { }, }} > - + @@ -105,16 +106,30 @@ function Filter() { return ( option.title} filterOptions={filterOptions} - sx={{ width: 300 }} - disableClearable // This disables the clearable (x) button + sx={{ + width: 170, // Adjust width as needed + '& .MuiAutocomplete-inputRoot': { + height: '50px', // Adjust the height of the input field + }, + '& .MuiAutocomplete-listbox': { + maxHeight: '200px', // Adjust the max height of the dropdown list + }, + }} + disableClearable renderInput={(params) => ( + + Filter by status + + } + InputProps={{ ...params.InputProps, endAdornment: null }} /> )} /> From 99f460c80a75aa6ee6e2ca109ec6b9489bf29e26 Mon Sep 17 00:00:00 2001 From: M M Date: Tue, 2 Jul 2024 21:13:29 -0700 Subject: [PATCH 09/19] Added header. --- Client/src/Pages/Incidents/index.jsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index adaad7849..d645afc6d 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -10,8 +10,9 @@ import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; import { StatusLabel } from "../../Components/Label/"; -import TuneIcon from '@mui/icons-material/Tune'; // Import TuneIcon +import TuneIcon from '@mui/icons-material/Tune'; const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { @@ -73,7 +74,10 @@ export default function CustomizedTables() { }, }} > - + + + Incident History + @@ -110,12 +114,12 @@ function Filter() { getOptionLabel={(option) => option.title} filterOptions={filterOptions} sx={{ - width: 170, // Adjust width as needed + width: 170, '& .MuiAutocomplete-inputRoot': { - height: '50px', // Adjust the height of the input field + height: '50px', }, '& .MuiAutocomplete-listbox': { - maxHeight: '200px', // Adjust the max height of the dropdown list + maxHeight: '200px', }, }} disableClearable From 971518d70e0d8a4124bdb38b93f3f86eae15cb46 Mon Sep 17 00:00:00 2001 From: M M Date: Tue, 2 Jul 2024 21:19:44 -0700 Subject: [PATCH 10/19] Added JSDocs. --- Client/src/Pages/Incidents/index.jsx | 58 ++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index d645afc6d..b9778cbe9 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -10,10 +10,15 @@ import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; +import Typography from '@mui/material/Typography'; import { StatusLabel } from "../../Components/Label/"; -import TuneIcon from '@mui/icons-material/Tune'; +import TuneIcon from '@mui/icons-material/Tune'; +/** + * Creates a styled TableCell component. + * @param {object} theme - The theme object. + * @returns {JSX.Element} The styled TableCell component. + */ const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { backgroundColor: theme.palette.action.hover, @@ -24,6 +29,11 @@ const StyledTableCell = styled(TableCell)(({ theme }) => ({ }, })); +/** + * Creates a styled TableRow component. + * @param {object} theme - The theme object. + * @returns {JSX.Element} The styled TableRow component. + */ const StyledTableRow = styled(TableRow)(({ theme }) => ({ '&:nth-of-type(odd)': { backgroundColor: theme.palette.action.hover, @@ -41,27 +51,39 @@ const filterOptions = createFilterOptions({ stringify: (option) => option.title, }); + const titles = [ { title: 'Down' }, - { title: 'Cannot resolve'}, - { title: 'Clear / show all'}, + { title: 'Cannot resolve' }, + { title: 'Clear / show all' }, ]; +/** + * Creates a data object for the table row. + * @param {JSX.Element} name - The status label JSX element. + * @param {string} date - The date and time string. + * @param {string} message - The message string. + * @returns {object} The data object containing name, date, and message. + */ function createData(name, date, message) { return { name, date, message }; } -const rows = [ - createData(, '2024-03-14 21:41:09', 'HTTP 350 - NOK'), - createData(, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'), - createData(, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'), - createData(, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'), - createData(, '2024-03-14 21:41:09', 'HTTP 350 - NOK'), -]; - +/** + * Customized table component displaying incident history. + * @returns {JSX.Element} The JSX element representing the customized table. + */ export default function CustomizedTables() { const theme = useTheme(); + const rows = [ + createData(, '2024-03-14 21:41:09', 'HTTP 350 - NOK'), + createData(, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'), + createData(, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'), + createData(, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'), + createData(, '2024-03-14 21:41:09', 'HTTP 350 - NOK'), + ]; + return ( option.title} filterOptions={filterOptions} sx={{ - width: 170, + width: 170, '& .MuiAutocomplete-inputRoot': { height: '50px', }, '& .MuiAutocomplete-listbox': { - maxHeight: '200px', + maxHeight: '200px', }, }} disableClearable renderInput={(params) => ( - + Filter by status } From da1e8ee5d97b53ddc999f5c3d285d56f999d1423 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 3 Jul 2024 10:23:16 -0700 Subject: [PATCH 11/19] Added image bytes to update user profile --- Server/db/MongoDB.js | 9 ++++++++- Server/routes/authRoute.js | 7 ++++++- Server/validation/joi.js | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js index da9bf6cba..1dc26ce2c 100644 --- a/Server/db/MongoDB.js +++ b/Server/db/MongoDB.js @@ -88,9 +88,16 @@ const getUserByEmail = async (req, res) => { const updateUser = async (req, res) => { const candidateUserId = req.params.userId; - const candidateUser = req.body; try { + const candidateUser = { ...req.body }; + if (req.file) { + candidateUser.profileImage = { + data: req.file.buffer, + contentType: req.file.mimetype, + }; + } + const updatedUser = await UserModel.findByIdAndUpdate( candidateUserId, candidateUser, diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index f9142a395..775768c7e 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -16,7 +16,12 @@ const { //Auth routes router.post("/register", upload.single("profileImage"), registerController); router.post("/login", loginController); -router.post("/user/:userId", verifyJWT, userEditController); +router.post( + "/user/:userId", + upload.single("profileImage"), + verifyJWT, + userEditController +); router.get("/users/admin", checkAdminController); //Recovery routes diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 404e71357..264aa2e1e 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -25,7 +25,7 @@ const editUserParamValidation = joi.object({ const editUserBodyValidation = joi.object({ firstname: joi.string(), lastname: joi.string(), - profilePicUrl: joi.string(), + profileImage: joi.any(), role: joi.string(), }); From 9a382210b624c8fe05fdfdf402e1e9601ba65cd6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 3 Jul 2024 11:21:14 -0700 Subject: [PATCH 12/19] Added admin role to registration --- Client/src/Pages/Register/index.jsx | 8 +++++--- Client/src/Validation/validation.js | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Client/src/Pages/Register/index.jsx b/Client/src/Pages/Register/index.jsx index 4aaf5afc8..1760a5be2 100644 --- a/Client/src/Pages/Register/index.jsx +++ b/Client/src/Pages/Register/index.jsx @@ -37,6 +37,7 @@ const Register = () => { lastname: "", email: "", password: "", + role: "", }); useEffect(() => { @@ -50,7 +51,7 @@ const Register = () => { .catch((error) => { console.log(error); }); - }, [navigate]); + }, [form, navigate]); useEffect(() => { const { error } = registerValidation.validate(form, { @@ -76,8 +77,9 @@ const Register = () => { const handleSubmit = async (e) => { e.preventDefault(); try { - await registerValidation.validateAsync(form, { abortEarly: false }); - const action = await dispatch(register(form)); + const adminForm = { ...form, role: "admin" }; + await registerValidation.validateAsync(adminForm, { abortEarly: false }); + const action = await dispatch(register(adminForm)); if (action.meta.requestStatus === "fulfilled") { const token = action.payload.data; diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 00903fdb7..6284ea49d 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -22,6 +22,9 @@ const registerValidation = joi.object({ "string.min": "Password must be at least 8 characters", "string.empty": "Password is required", }), + role: joi.string().required().messages({ + "string.empty": "Role is required", + }), }); const loginValidation = joi.object({ From 9ccf42fe556fefd494ab65d1c55fab185aefee13 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 3 Jul 2024 11:41:12 -0700 Subject: [PATCH 13/19] Monitor now has a status, status will be updated on new check --- Server/models/Check.js | 17 +++++++++++++++++ Server/models/Monitor.js | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/Server/models/Check.js b/Server/models/Check.js index d57030202..7e46f418d 100644 --- a/Server/models/Check.js +++ b/Server/models/Check.js @@ -31,4 +31,21 @@ const CheckSchema = mongoose.Schema( } ); +CheckSchema.pre("save", async function (next) { + const monitor = await mongoose.model("Monitor").findById(this.monitorId); + + if (monitor.status === true && this.status === false) { + console.log("Monitor went down"); + } + + if (monitor.status === false && this.status === true) { + console.log("Monitor went up"); + } + + monitor.status = this.status; + await monitor.save(); + + next; +}); + module.exports = mongoose.model("Check", CheckSchema); diff --git a/Server/models/Monitor.js b/Server/models/Monitor.js index 8455aa312..4b3bd7c63 100644 --- a/Server/models/Monitor.js +++ b/Server/models/Monitor.js @@ -14,6 +14,10 @@ const MonitorSchema = mongoose.Schema( description: { type: String, }, + status: { + type: Boolean, + default: true, + }, type: { type: String, required: true, From 031ae06d417bc26adcbc0bee00290968ec80e3ca Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 3 Jul 2024 11:46:30 -0700 Subject: [PATCH 14/19] Add conditional to avoid extra db operatons --- Server/models/Check.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Server/models/Check.js b/Server/models/Check.js index 7e46f418d..b1cda5616 100644 --- a/Server/models/Check.js +++ b/Server/models/Check.js @@ -31,20 +31,26 @@ const CheckSchema = mongoose.Schema( } ); +/** + * When a check is created, we should update the stauts of the associated monitor + * if it has changed. Monitor starts with default status: true + */ + CheckSchema.pre("save", async function (next) { const monitor = await mongoose.model("Monitor").findById(this.monitorId); + if (monitor.status !== this.status) { + if (monitor.status === true && this.status === false) { + // TODO issue alert + console.log("Monitor went down"); + } - if (monitor.status === true && this.status === false) { - console.log("Monitor went down"); + if (monitor.status === false && this.status === true) { + // TODO issue alert + console.log("Monitor went up"); + } + await monitor.save(); } - if (monitor.status === false && this.status === true) { - console.log("Monitor went up"); - } - - monitor.status = this.status; - await monitor.save(); - next; }); From 59992b4ac8df7a16b941bed035c110a2e239c97e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 3 Jul 2024 11:59:24 -0700 Subject: [PATCH 15/19] Remove profileImage from returned user --- Server/db/MongoDB.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js index 1dc26ce2c..ab2d626f4 100644 --- a/Server/db/MongoDB.js +++ b/Server/db/MongoDB.js @@ -42,7 +42,9 @@ const insertUser = async (req, res) => { } const newUser = new UserModel(userData); await newUser.save(); - return await UserModel.findOne({ _id: newUser._id }).select("-password"); // .select() doesn't work with create, need to save then find + return await UserModel.findOne({ _id: newUser._id }) + .select("-password") + .select("-profileImage"); // .select() doesn't work with create, need to save then find } catch (error) { if (error.code === DUPLICATE_KEY_CODE) { throw new Error(errorMessages.DB_USER_EXISTS); @@ -65,8 +67,10 @@ const insertUser = async (req, res) => { const getUserByEmail = async (req, res) => { try { // Need the password to be able to compare, removed .select() - // We can strip the hash before returing the user - const user = await UserModel.findOne({ email: req.body.email }); + // PW hash is removed in controller + const user = await UserModel.findOne({ email: req.body.email }).select( + "-profileImage" + ); if (user) { return user; } else { @@ -102,7 +106,9 @@ const updateUser = async (req, res) => { candidateUserId, candidateUser, { new: true } // Returns updated user instead of pre-update user - ).select("-password"); + ) + .select("-password") + .select("-profileImage"); return updatedUser; } catch (error) { throw error; From f97aa1ca8ca095d321930d782a4fbf24b904e4c3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 3 Jul 2024 12:20:30 -0700 Subject: [PATCH 16/19] Return user and Token instead of decoding token. Added transform to not store image in redux store --- Client/src/Features/Auth/authSlice.js | 6 ++---- Client/src/store.js | 14 +++++++++++++- Server/controllers/authController.js | 10 +++++++--- Server/db/MongoDB.js | 14 ++++---------- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/Client/src/Features/Auth/authSlice.js b/Client/src/Features/Auth/authSlice.js index ef41c1b46..5358d7f4d 100644 --- a/Client/src/Features/Auth/authSlice.js +++ b/Client/src/Features/Auth/authSlice.js @@ -67,10 +67,8 @@ const handleAuthFulfilled = (state, action) => { state.isLoading = false; state.success = action.payload.success; state.msg = action.payload.msg; - state.authToken = action.payload.data; - const decodedToken = jwtDecode(action.payload.data); - const user = { ...decodedToken }; - state.user = user; + state.authToken = action.payload.data.token; + state.user = action.payload.data.user; }; const handleAuthRejected = (state, action) => { state.isLoading = false; diff --git a/Client/src/store.js b/Client/src/store.js index 54201761e..ebb6a325b 100644 --- a/Client/src/store.js +++ b/Client/src/store.js @@ -2,12 +2,24 @@ import { configureStore, combineReducers } from "@reduxjs/toolkit"; import monitorsReducer from "./Features/Monitors/monitorsSlice"; import authReducer from "./Features/Auth/authSlice"; import storage from "redux-persist/lib/storage"; -import { persistReducer, persistStore } from "redux-persist"; +import { persistReducer, persistStore, createTransform } from "redux-persist"; + +const authTransform = createTransform( + (inboundState) => { + const { profileImage, ...rest } = inboundState; + return rest; + }, + // No transformation on rehydration + null, + // Only applies to auth + { whitelist: ["auth"] } +); const persistConfig = { key: "root", storage, whitielist: ["auth", "monitors"], + transforms: [authTransform], }; const rootReducer = combineReducers({ diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index bbf676e8a..0e05853b6 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -55,7 +55,11 @@ const registerController = async (req, res, next) => { service: SERVICE_NAME, userId: newUser._id, }); - const token = issueToken(newUser._doc); + + const userForToken = { ...newUser._doc }; + delete userForToken.profileImage; + + const token = issueToken(userForToken); // Sending email to user with pre defined template const template = registerTemplate("https://www.bluewavelabs.ca"); @@ -69,7 +73,7 @@ const registerController = async (req, res, next) => { return res.status(200).json({ success: true, msg: successMessages.AUTH_CREATE_USER, - data: token, + data: { user: newUser, token: token }, }); } catch (error) { error.service = SERVICE_NAME; @@ -108,7 +112,7 @@ const loginController = async (req, res, next) => { return res.status(200).json({ success: true, msg: successMessages.AUTH_LOGIN_USER, - data: token, + data: { user: userWithoutPassword, token: token }, }); } catch (error) { error.status = 500; diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js index ab2d626f4..1dc26ce2c 100644 --- a/Server/db/MongoDB.js +++ b/Server/db/MongoDB.js @@ -42,9 +42,7 @@ const insertUser = async (req, res) => { } const newUser = new UserModel(userData); await newUser.save(); - return await UserModel.findOne({ _id: newUser._id }) - .select("-password") - .select("-profileImage"); // .select() doesn't work with create, need to save then find + return await UserModel.findOne({ _id: newUser._id }).select("-password"); // .select() doesn't work with create, need to save then find } catch (error) { if (error.code === DUPLICATE_KEY_CODE) { throw new Error(errorMessages.DB_USER_EXISTS); @@ -67,10 +65,8 @@ const insertUser = async (req, res) => { const getUserByEmail = async (req, res) => { try { // Need the password to be able to compare, removed .select() - // PW hash is removed in controller - const user = await UserModel.findOne({ email: req.body.email }).select( - "-profileImage" - ); + // We can strip the hash before returing the user + const user = await UserModel.findOne({ email: req.body.email }); if (user) { return user; } else { @@ -106,9 +102,7 @@ const updateUser = async (req, res) => { candidateUserId, candidateUser, { new: true } // Returns updated user instead of pre-update user - ) - .select("-password") - .select("-profileImage"); + ).select("-password"); return updatedUser; } catch (error) { throw error; From 505d9f28907bd7af7b03c5f7f59640f0ddce0136 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 3 Jul 2024 12:32:42 -0700 Subject: [PATCH 17/19] Added error checking and fixed typo --- Server/models/Check.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Server/models/Check.js b/Server/models/Check.js index b1cda5616..2bcb912f4 100644 --- a/Server/models/Check.js +++ b/Server/models/Check.js @@ -37,21 +37,25 @@ const CheckSchema = mongoose.Schema( */ CheckSchema.pre("save", async function (next) { - const monitor = await mongoose.model("Monitor").findById(this.monitorId); - if (monitor.status !== this.status) { - if (monitor.status === true && this.status === false) { - // TODO issue alert - console.log("Monitor went down"); - } + try { + const monitor = await mongoose.model("Monitor").findById(this.monitorId); + if (monitor && monitor.status !== this.status) { + if (monitor.status === true && this.status === false) { + // TODO issue alert + console.log("Monitor went down"); + } - if (monitor.status === false && this.status === true) { - // TODO issue alert - console.log("Monitor went up"); + if (monitor.status === false && this.status === true) { + // TODO issue alert + console.log("Monitor went up"); + } + await monitor.save(); } - await monitor.save(); + } catch (error) { + console.log(error); + } finally { + next(); } - - next; }); module.exports = mongoose.model("Check", CheckSchema); From ceecf3427cda070ad1b2249bd1a79c17f995c404 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 3 Jul 2024 12:34:57 -0700 Subject: [PATCH 18/19] Added JS doc --- Server/models/Check.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Server/models/Check.js b/Server/models/Check.js index 2bcb912f4..339c21482 100644 --- a/Server/models/Check.js +++ b/Server/models/Check.js @@ -34,6 +34,7 @@ const CheckSchema = mongoose.Schema( /** * When a check is created, we should update the stauts of the associated monitor * if it has changed. Monitor starts with default status: true + * @param {function} next - Callback to proceed to the next middleware. */ CheckSchema.pre("save", async function (next) { From 04ff74dcefd7923a09e87d434a754e95fc3f7011 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 3 Jul 2024 13:19:47 -0700 Subject: [PATCH 19/19] Update monitor status --- Server/models/Check.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Server/models/Check.js b/Server/models/Check.js index 339c21482..7732fd312 100644 --- a/Server/models/Check.js +++ b/Server/models/Check.js @@ -50,6 +50,7 @@ CheckSchema.pre("save", async function (next) { // TODO issue alert console.log("Monitor went up"); } + monitor.status = this.status; await monitor.save(); } } catch (error) {