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(),
});