Merge branch 'bluewave-labs:master' into backend-completion
This commit is contained in:
13 files changed
+240
-53
No files matched your search
+1
-2
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
@@ -14,6 +14,10 @@ const MonitorSchema = mongoose.Schema(
|
||||
description: {
|
||||
type: String,
|
||||
},
|
||||
status: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user