Merge branch 'bluewave-labs:master' into backend-completion

This commit is contained in:
Mohammad Khalilzadeh authored and GitHub committed 2024-07-04 11:18:30 +03:30
commit a2e378f1dd
13 files changed
+240 -53

No files matched your search

+1 -2
View File
@@ -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() {
+2 -4
View File
@@ -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;
+133 -38
View File
@@ -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(<StatusLabel status="Down" customStyles={{ backgroundColor: '#fff9f9', borderColor: '#ffcac6', color: '#344054' }} />, '2024-03-14 21:41:09', 'HTTP 350 - NOK'),
createData(<StatusLabel status="Down" customStyles={{ backgroundColor: '#fff9f9', borderColor: '#ffcac6', color: '#344054' }} />, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'),
createData(<StatusLabel status="Cannot resolve" customStyles={{ backgroundColor: '#f2f4f7', borderColor: '#d2d6de', color: '#344054' }} />, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'),
createData(<StatusLabel status="Cannot resolve" customStyles={{ backgroundColor: '#f2f4f7', borderColor: '#d2d6de', color: '#344054' }} />, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'),
createData(<StatusLabel status="Down" customStyles={{ backgroundColor: '#fff9f9', borderColor: '#ffcac6', color: '#344054' }} />, '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(<StatusLabel status="Down" customStyles={{ backgroundColor: '#fff9f9', borderColor: '#ffcac6', color: '#344054' }} />, '2024-03-14 21:41:09', 'HTTP 350 - NOK'),
createData(<StatusLabel status="Down" customStyles={{ backgroundColor: '#fff9f9', borderColor: '#ffcac6', color: '#344054' }} />, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'),
createData(<StatusLabel status="Cannot resolve" customStyles={{ backgroundColor: '#f2f4f7', borderColor: '#d2d6de', color: '#344054' }} />, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'),
createData(<StatusLabel status="Cannot resolve" customStyles={{ backgroundColor: '#f2f4f7', borderColor: '#d2d6de', color: '#344054' }} />, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'),
createData(<StatusLabel status="Down" customStyles={{ backgroundColor: '#fff9f9', borderColor: '#ffcac6', color: '#344054' }} />, '2024-03-14 21:41:09', 'HTTP 350 - NOK'),
];
return (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 700 }} aria-label="customized table">
<TableHead>
<TableRow>
<StyledTableCell>Status</StyledTableCell>
<StyledTableCell align="right">Date & Time</StyledTableCell>
<StyledTableCell align="right">Message</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, index) => (
<StyledTableRow key={index}>
<StyledTableCell component="th" scope="row">
{row.name}
</StyledTableCell>
<StyledTableCell align="right">{row.calories}</StyledTableCell>
<StyledTableCell align="right">{row.fat}</StyledTableCell>
</StyledTableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Box
sx={{
marginTop: theme.spacing(4),
marginX: 'auto',
width: '80%',
paddingX: theme.spacing(6),
[theme.breakpoints.down('md')]: {
width: '100%',
},
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: theme.spacing(2) }}>
<Typography variant="h6" component="div" sx={{ fontWeight: 600, fontSize: 16 }}>
Incident History
</Typography>
<Filter />
</Box>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 700 }} aria-label="customized table">
<TableHead>
<TableRow>
<StyledTableCell>Status</StyledTableCell>
<StyledTableCell align="right">Date & Time</StyledTableCell>
<StyledTableCell align="right">Message</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, index) => (
<StyledTableRow key={index}>
<StyledTableCell component="th" scope="row">
{row.name}
</StyledTableCell>
<StyledTableCell align="right">{row.date}</StyledTableCell>
<StyledTableCell align="right">{row.message}</StyledTableCell>
</StyledTableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
);
}
/**
* Filter component with autocomplete for status filtering.
* @returns {JSX.Element} The JSX element representing the filter component.
*/
function Filter() {
return (
<Autocomplete
id="filter-demo"
options={titles}
getOptionLabel={(option) => option.title}
filterOptions={filterOptions}
sx={{
width: 170,
'& .MuiAutocomplete-inputRoot': {
height: '50px',
},
'& .MuiAutocomplete-listbox': {
maxHeight: '200px',
},
}}
disableClearable
renderInput={(params) => (
<TextField
{...params}
size="small"
label={
<React.Fragment>
<TuneIcon sx={{ marginRight: '0.5rem', position: 'relative', top: '6px' }} />
Filter by status
</React.Fragment>
}
InputProps={{ ...params.InputProps, endAdornment: null }}
/>
)}
/>
);
}
+15
View File
@@ -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,
+18 -2
View File
@@ -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;
+3
View File
@@ -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({
+13 -1
View File
@@ -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({
+7 -3
View File
@@ -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;
+8 -1
View File
@@ -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,
+29
View File
@@ -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);
+4
View File
@@ -14,6 +14,10 @@ const MonitorSchema = mongoose.Schema(
description: {
type: String,
},
status: {
type: Boolean,
default: true,
},
type: {
type: String,
required: true,
+6 -1
View File
@@ -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);
+1 -1
View File
@@ -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(),
});