diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 4a05c1de3..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,7 +17,6 @@ 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"; function App() { 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/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index a2cb59517..b9778cbe9 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -1,4 +1,7 @@ -import { styled } from '@mui/material/styles'; +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'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell, { tableCellClasses } from '@mui/material/TableCell'; @@ -6,12 +9,19 @@ 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 Typography from '@mui/material/Typography'; import { StatusLabel } from "../../Components/Label/"; +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, + backgroundColor: theme.palette.action.hover, color: theme.palette.common.black, }, [`&.${tableCellClasses.body}`]: { @@ -19,54 +29,139 @@ 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(1)': { - backgroundColor: theme.palette.action.hover, + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover, }, - '&:nth-of-type(n+1)': { - backgroundColor: theme.palette.common.white, + '&:nth-of-type(even)': { + 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 }; -} +const filterOptions = createFilterOptions({ + matchFrom: 'start', + stringify: (option) => option.title, +}); -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'), + +const titles = [ + { title: 'Down' }, + { 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 }; +} +/** + * 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 ( - - - - - Status - Date & Time - Message - - - - {rows.map((row, index) => ( - - - {row.name} - - {row.calories} - {row.fat} - - ))} - -
-
+ + + + Incident History + + + + + + + + Status + Date & Time + Message + + + + {rows.map((row, index) => ( + + + {row.name} + + {row.date} + {row.message} + + ))} + +
+
+
+ ); +} + +/** + * Filter component with autocomplete for status filtering. + * @returns {JSX.Element} The JSX element representing the filter component. + */ +function Filter() { + return ( + option.title} + filterOptions={filterOptions} + sx={{ + width: 170, + '& .MuiAutocomplete-inputRoot': { + height: '50px', + }, + '& .MuiAutocomplete-listbox': { + maxHeight: '200px', + }, + }} + disableClearable + renderInput={(params) => ( + + + Filter by status + + } + InputProps={{ ...params.InputProps, endAdornment: null }} + /> + )} + /> ); } 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..1760a5be2 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; @@ -36,8 +37,22 @@ const Register = () => { lastname: "", email: "", password: "", + role: "", }); + useEffect(() => { + axios + .get(BASE_URL + "/auth/users/admin") + .then((response) => { + if (response.data.data === true) { + navigate("/login"); + } + }) + .catch((error) => { + console.log(error); + }); + }, [form, navigate]); + useEffect(() => { const { error } = registerValidation.validate(form, { abortEarly: false, @@ -62,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({ 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 1b13bfe54..9ce55959d 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -56,7 +56,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"); @@ -70,7 +74,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; @@ -109,7 +113,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 7cc7c34e3..6f090662d 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/models/Check.js b/Server/models/Check.js index d57030202..7732fd312 100644 --- a/Server/models/Check.js +++ b/Server/models/Check.js @@ -31,4 +31,33 @@ 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) { + 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"); + } + monitor.status = this.status; + await monitor.save(); + } + } catch (error) { + console.log(error); + } finally { + 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, diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index 5e6377757..7f33381a1 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -17,7 +17,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); router.delete("/user/:userId", verifyJWT, deleteUserController); diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 81c92a3fe..8864fb8bd 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(), });