18 files changed
+670
-266
No files matched your search
@@ -58,6 +58,7 @@ const Button = ({
|
||||
disabled,
|
||||
img,
|
||||
onClick,
|
||||
props,
|
||||
sx,
|
||||
}) => {
|
||||
const { variant, color } = levelConfig[level];
|
||||
@@ -84,6 +85,7 @@ const Button = ({
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{img && img}
|
||||
<span>{label}</span>
|
||||
|
||||
@@ -21,6 +21,8 @@ import UserSvg from "../../assets/icons/user.svg?react";
|
||||
import TeamSvg from "../../assets/icons/user-two.svg?react";
|
||||
import LogoutSvg from "../../assets/icons/logout.svg?react";
|
||||
import { Stack, useScrollTrigger } from "@mui/material";
|
||||
import axiosIntance from "../../Utils/axiosConfig";
|
||||
import axios from "axios";
|
||||
|
||||
const icons = {
|
||||
Profile: <UserSvg />,
|
||||
@@ -93,10 +95,21 @@ function NavBar() {
|
||||
* Handles logging out the user
|
||||
*
|
||||
*/
|
||||
const logout = () => {
|
||||
const logout = async () => {
|
||||
// Clear auth state
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
// Make request to BE to remove JWT from user
|
||||
await axiosIntance.post(
|
||||
"/auth/logout",
|
||||
{ email: authState.user.email },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authState.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
id="login-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="john.doe@domain.com"
|
||||
placeholder="jordan.ellis@domain.com"
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onChange={onChange}
|
||||
@@ -117,6 +117,7 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
mr: theme.gap.xs,
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
/>
|
||||
<Button
|
||||
level="primary"
|
||||
@@ -190,6 +191,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
mr: theme.gap.xs,
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
/>
|
||||
<Button
|
||||
level="primary"
|
||||
@@ -326,7 +328,7 @@ const Login = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="login-page" overflow="hidden">
|
||||
<Stack className="login-page auth" overflow="hidden">
|
||||
<img
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
@@ -339,7 +341,7 @@ const Login = () => {
|
||||
gap={theme.gap.small}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography>BlueWave Uptime</Typography>
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
|
||||
@@ -1,201 +1,287 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useDispatch } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import "../index.css";
|
||||
import background from "../../../assets/Images/background_pattern_decorative.png";
|
||||
import Logomark from "../../../assets/Images/bwl-logo-2.svg?react";
|
||||
import Check from "../../../Components/Check/Check";
|
||||
import Button from "../../../Components/Button";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import { register } from "../../../Features/Auth/authSlice";
|
||||
import { useParams } from "react-router-dom";
|
||||
import background from "../../../assets/Images/background_pattern_decorative.png";
|
||||
import Logo from "../../../assets/icons/bwu-icon.svg?react";
|
||||
import Mail from "../../../assets/icons/mail.svg?react";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import Check from "../../../Components/Check/Check";
|
||||
import Button from "../../../Components/Button";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import axiosInstance from "../../../Utils/axiosConfig";
|
||||
import "../index.css";
|
||||
|
||||
const Register = ({ isAdmin }) => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { token } = useParams();
|
||||
/**
|
||||
* Displays the initial landing page.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.isAdmin - Whether the user is creating and admin account
|
||||
* @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const LandingPage = ({ isAdmin, onSignup }) => {
|
||||
const theme = useTheme();
|
||||
// TODO If possible, change the IDs of these fields to match the backend
|
||||
const idMap = {
|
||||
"register-firstname-input": "firstName",
|
||||
"register-lastname-input": "lastName",
|
||||
"register-email-input": "email",
|
||||
"register-password-input": "password",
|
||||
"register-confirm-input": "confirm",
|
||||
};
|
||||
|
||||
const [form, setForm] = useState({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirm: "",
|
||||
role: [],
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchInvite = async () => {
|
||||
if (token !== undefined) {
|
||||
try {
|
||||
const res = await axiosInstance.post(`/auth/invite/verify`, {
|
||||
token,
|
||||
});
|
||||
const { role, email } = res.data.data;
|
||||
console.log(role);
|
||||
setForm({ ...form, email, role });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchInvite();
|
||||
}, [token]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const registerForm = { ...form, role: isAdmin ? ["admin"] : form.role };
|
||||
const { error } = credentials.validate(registerForm, {
|
||||
abortEarly: false,
|
||||
context: { password: form.password },
|
||||
});
|
||||
|
||||
if (error) {
|
||||
// validation errors
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({
|
||||
body:
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.",
|
||||
});
|
||||
} else {
|
||||
delete registerForm.confirm;
|
||||
const action = await dispatch(register(registerForm));
|
||||
if (action.payload.success) {
|
||||
const token = action.payload.data;
|
||||
localStorage.setItem("token", token);
|
||||
navigate("/");
|
||||
createToast({
|
||||
body: "Welcome! Your account was created successfully.",
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idMap[id];
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const { error } = credentials.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false, context: { password: form.password } }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
if (error) prevErrors[name] = error.details[0].message;
|
||||
else delete prevErrors[name];
|
||||
return prevErrors;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="register-page">
|
||||
<img
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
alt="background pattern"
|
||||
/>
|
||||
<form className="register-form" onSubmit={handleSubmit} noValidate>
|
||||
<Stack gap={theme.gap.small} alignItems="center">
|
||||
<Logomark alt="BlueWave Uptime Icon" />
|
||||
<Typography component="h1" sx={{ mt: theme.gap.xl }}>
|
||||
Create{isAdmin ? " admin " : " "}account
|
||||
<>
|
||||
<Stack gap={theme.gap.large} alignItems="center" textAlign="center">
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>
|
||||
Create your {isAdmin ? "admin " : ""}account to get started.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box width="100%">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Sign up with Email"
|
||||
img={<Mail />}
|
||||
onClick={onSignup}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& svg": {
|
||||
mr: theme.gap.small,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box maxWidth={400}>
|
||||
<Typography className="tos-p">
|
||||
By signing up, you agree to our{" "}
|
||||
<Typography component="span">Terms of Service</Typography> and{" "}
|
||||
<Typography component="span">Privacy Policy.</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the first step of the sign up process.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} props.form - Form state object.
|
||||
* @param {Object} props.errors - Object containing form validation errors.
|
||||
* @param {Function} props.onSubmit - Callback function to handle form submission.
|
||||
* @param {Function} props.onChange - Callback function to handle form input changes.
|
||||
* @param {Function} props.onBack - Callback function to handle "Back" button click.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
const theme = useTheme();
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={theme.gap.large} textAlign="center">
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Enter your personal details</Typography>
|
||||
</Box>
|
||||
<Box textAlign="left">
|
||||
<form noValidate spellCheck={false} onSubmit={onSubmit}>
|
||||
<Field
|
||||
id="register-firstname-input"
|
||||
label="Name"
|
||||
isRequired={true}
|
||||
placeholder="Jordan"
|
||||
autoComplete="given-name"
|
||||
value={form.firstName}
|
||||
onChange={onChange}
|
||||
error={errors.firstName}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</form>
|
||||
<form noValidate spellCheck={false} onSubmit={onSubmit}>
|
||||
<Box my={theme.gap.ml}>
|
||||
<Field
|
||||
id="register-lastname-input"
|
||||
label="Surname"
|
||||
isRequired={true}
|
||||
placeholder="Ellis"
|
||||
autoComplete="family-name"
|
||||
value={form.lastName}
|
||||
onChange={onChange}
|
||||
error={errors.lastName}
|
||||
/>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.xs,
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
/>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Continue"
|
||||
onClick={onSubmit}
|
||||
disabled={(errors.firstName || errors.lastName) && true}
|
||||
sx={{ width: "30%" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.large} sx={{ mt: `calc(${theme.gap.ml}*2)` }}>
|
||||
<Field
|
||||
id="register-firstname-input"
|
||||
label="Name"
|
||||
isRequired={true}
|
||||
placeholder="Daniel"
|
||||
autoComplete="given-name"
|
||||
value={form.firstName}
|
||||
onChange={handleChange}
|
||||
error={errors.firstName}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the second step of the sign up process.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} props.form - Form state object.
|
||||
* @param {Object} props.errors - Object containing form validation errors.
|
||||
* @param {Function} props.onSubmit - Callback function to handle form submission.
|
||||
* @param {Function} props.onChange - Callback function to handle form input changes.
|
||||
* @param {Function} props.onBack - Callback function to handle "Back" button click.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
const theme = useTheme();
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={theme.gap.large} textAlign="center">
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Enter your email address</Typography>
|
||||
</Box>
|
||||
<Box textAlign="left">
|
||||
<form noValidate spellCheck={false} onSubmit={onSubmit}>
|
||||
<Field
|
||||
type="email"
|
||||
id="register-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="jordan.ellis@domain.com"
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onChange={onChange}
|
||||
error={errors.email}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</form>
|
||||
</Box>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.xs,
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
/>
|
||||
<Field
|
||||
id="register-lastname-input"
|
||||
label="Surname"
|
||||
isRequired={true}
|
||||
placeholder="Cojocea"
|
||||
autoComplete="family-name"
|
||||
value={form.lastName}
|
||||
onChange={handleChange}
|
||||
error={errors.lastName}
|
||||
<Button
|
||||
level="primary"
|
||||
label="Continue"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.email && true}
|
||||
sx={{ width: "30%" }}
|
||||
/>
|
||||
<Field
|
||||
type="email"
|
||||
id="register-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="daniel.cojocea@domain.com"
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
/>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="Create a password"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
/>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="Confirm your password"
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors.confirm}
|
||||
/>
|
||||
<Stack gap={theme.gap.small}>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the third step of the sign up process.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} props.form - Form state object.
|
||||
* @param {Object} props.errors - Object containing form validation errors.
|
||||
* @param {Function} props.onSubmit - Callback function to handle form submission.
|
||||
* @param {Function} props.onChange - Callback function to handle form input changes.
|
||||
* @param {Function} props.onBack - Callback function to handle "Back" button click.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
const theme = useTheme();
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={theme.gap.large} textAlign="center">
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Create your password</Typography>
|
||||
</Box>
|
||||
<Box textAlign="left">
|
||||
<form noValidate spellCheck={false} onSubmit={onSubmit}>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="Create a password"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
error={errors.password}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</form>
|
||||
<form noValidate spellCheck={false} onSubmit={onSubmit}>
|
||||
<Box mt={theme.gap.ml}>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="Confirm your password"
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={onChange}
|
||||
error={errors.confirm}
|
||||
/>
|
||||
</Box>
|
||||
</form>
|
||||
<Stack gap={theme.gap.small} my={theme.gap.large}>
|
||||
<Check
|
||||
text="Must be at least 8 characters long"
|
||||
variant={
|
||||
@@ -235,16 +321,279 @@ const Register = ({ isAdmin }) => {
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.xs,
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
level="primary"
|
||||
label="Get started"
|
||||
sx={{ marginBottom: theme.gap.large }}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
label="Continue"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.email && true}
|
||||
sx={{ width: "30%" }}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
</div>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Register = ({ isAdmin }) => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { token } = useParams();
|
||||
const theme = useTheme();
|
||||
// TODO If possible, change the IDs of these fields to match the backend
|
||||
const idMap = {
|
||||
"register-firstname-input": "firstName",
|
||||
"register-lastname-input": "lastName",
|
||||
"register-email-input": "email",
|
||||
"register-password-input": "password",
|
||||
"register-confirm-input": "confirm",
|
||||
};
|
||||
|
||||
const [form, setForm] = useState({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirm: "",
|
||||
role: [],
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [step, setStep] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchInvite = async () => {
|
||||
if (token !== undefined) {
|
||||
try {
|
||||
const res = await axiosInstance.post(`/auth/invite/verify`, {
|
||||
token,
|
||||
});
|
||||
const { role, email } = res.data.data;
|
||||
console.log(role);
|
||||
setForm({ ...form, email, role });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchInvite();
|
||||
}, [token]);
|
||||
|
||||
/**
|
||||
* Validates the form data against the validation schema.
|
||||
*
|
||||
* @param {Object} data - The form data to validate.
|
||||
* @param {Object} [options] - Optional settings for validation.
|
||||
* @returns {Object | undefined} - Returns the validation error object if there are validation errors; otherwise, `undefined`.
|
||||
*/
|
||||
const validateForm = (data, options = {}) => {
|
||||
const { error } = credentials.validate(data, {
|
||||
abortEarly: false,
|
||||
...options,
|
||||
});
|
||||
return error;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles validation errors by setting the state with error messages and displaying a toast notification.
|
||||
*
|
||||
* @param {Object} error - The validation error object returned from the validation schema.
|
||||
*/
|
||||
const handleError = (error) => {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: error.details[0].message || "Error validating data." });
|
||||
};
|
||||
|
||||
const handleStepOne = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let error = validateForm({
|
||||
firstName: form.firstName,
|
||||
lastName: form.lastName,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
handleError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
setStep(2);
|
||||
};
|
||||
|
||||
const handleStepTwo = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let error;
|
||||
error = validateForm({ email: form.email });
|
||||
if (error) {
|
||||
handleError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
setStep(3);
|
||||
};
|
||||
|
||||
// Final step
|
||||
// Attempts account registration
|
||||
const handleStepThree = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let registerForm = { ...form, role: isAdmin ? ["admin"] : form.role };
|
||||
let error = validateForm(registerForm, {
|
||||
context: { password: form.password },
|
||||
});
|
||||
if (error) {
|
||||
handleError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
delete registerForm.confirm;
|
||||
const action = await dispatch(register(registerForm));
|
||||
if (action.payload.success) {
|
||||
const token = action.payload.data;
|
||||
localStorage.setItem("token", token);
|
||||
navigate("/");
|
||||
createToast({
|
||||
body: "Welcome! Your account was created successfully.",
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idMap[id];
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const { error } = credentials.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false, context: { password: form.password } }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
if (error) prevErrors[name] = error.details[0].message;
|
||||
else delete prevErrors[name];
|
||||
return prevErrors;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="register-page auth" overflow="hidden">
|
||||
<img
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
alt="background pattern"
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.gap.large}
|
||||
gap={theme.gap.small}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
p={theme.gap.xl}
|
||||
pb={theme.gap.triplexl}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: theme.palette.otherColors.graishWhite,
|
||||
backgroundColor: theme.palette.otherColors.white,
|
||||
padding: {
|
||||
xs: theme.gap.large,
|
||||
sm: theme.gap.xl,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{step === 0 ? (
|
||||
<LandingPage isAdmin={isAdmin} onSignup={() => setStep(1)} />
|
||||
) : step === 1 ? (
|
||||
<StepOne
|
||||
form={form}
|
||||
errors={errors}
|
||||
onSubmit={handleStepOne}
|
||||
onChange={handleChange}
|
||||
onBack={() => setStep(0)}
|
||||
/>
|
||||
) : step === 2 ? (
|
||||
<StepTwo
|
||||
form={form}
|
||||
errors={errors}
|
||||
onSubmit={handleStepTwo}
|
||||
onChange={handleChange}
|
||||
onBack={() => setStep(1)}
|
||||
/>
|
||||
) : step === 3 ? (
|
||||
<StepThree
|
||||
form={form}
|
||||
errors={errors}
|
||||
onSubmit={handleStepThree}
|
||||
onChange={handleChange}
|
||||
onBack={() => setStep(2)}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
<Box textAlign="center" p={theme.gap.large}>
|
||||
<Typography display="inline-block">
|
||||
Already have an account? —
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.gap.xs}
|
||||
onClick={() => {
|
||||
navigate("/login");
|
||||
}}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
Register.propTypes = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* ////// */
|
||||
/* SHARED */
|
||||
/* ////// */
|
||||
.register-page,
|
||||
.set-new-password-page,
|
||||
.password-confirmed-page,
|
||||
.forgot-password-page,
|
||||
@@ -10,7 +9,6 @@
|
||||
justify-content: center;
|
||||
height: var(--env-var-height-1);
|
||||
}
|
||||
.register-page h1.MuiTypography-root,
|
||||
.set-new-password-page h1.MuiTypography-root,
|
||||
.password-confirmed-page h1.MuiTypography-root,
|
||||
.forgot-password-page h1.MuiTypography-root,
|
||||
@@ -19,10 +17,6 @@
|
||||
color: var(--env-var-color-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
.register-page p.MuiTypography-root,
|
||||
.register-page span,
|
||||
.register-page button,
|
||||
.login-page button,
|
||||
.set-new-password-page p.MuiTypography-root,
|
||||
.set-new-password-page button,
|
||||
.password-confirmed-page p.MuiTypography-root,
|
||||
@@ -34,11 +28,6 @@
|
||||
.check-email-page span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.register-page p.MuiTypography-root {
|
||||
color: var(--env-var-color-2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.register-page button:not(.MuiIconButton-root),
|
||||
.set-new-password-page button:not(.MuiIconButton-root),
|
||||
.password-confirmed-page button:not(.MuiIconButton-root),
|
||||
.forgot-password-page button:not(.MuiIconButton-root),
|
||||
@@ -48,13 +37,6 @@
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.register-page svg rect {
|
||||
fill: none;
|
||||
}
|
||||
.register-form {
|
||||
margin-top: 95px;
|
||||
}
|
||||
|
||||
.set-new-password-form,
|
||||
.password-confirmed-form,
|
||||
.forgot-password-form,
|
||||
@@ -76,9 +58,6 @@
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.register-page
|
||||
.MuiFormControl-root:has(#register-password-input)
|
||||
+ span.MuiTypography-root,
|
||||
.set-new-password-page
|
||||
.MuiFormControl-root:has(#register-password-input)
|
||||
+ span.MuiTypography-root,
|
||||
@@ -89,17 +68,6 @@
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* ///// */
|
||||
/* LOGIN */
|
||||
/* ///// */
|
||||
.login-page
|
||||
.MuiStack-root:not(:has(> .MuiButtonBase-root))
|
||||
> span:not(.field-required):not(.input-error) {
|
||||
color: var(--env-var-color-3);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* /////////// */
|
||||
/* Check Email */
|
||||
/* /////////// */
|
||||
@@ -107,68 +75,71 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* */
|
||||
.login-page {
|
||||
/* AUTH */
|
||||
.auth {
|
||||
height: 100vh;
|
||||
}
|
||||
.login-page h1 {
|
||||
font-size: 37px;
|
||||
.auth h1 {
|
||||
font-size: var(--env-var-font-size-xlarge);
|
||||
font-weight: 600;
|
||||
color: var(--env-var-color-3);
|
||||
}
|
||||
.login-page button:not(.MuiIconButton-root),
|
||||
.login-page .field h3.MuiTypography-root,
|
||||
.login-page .field input,
|
||||
.login-page .field .input-error,
|
||||
.login-page p,
|
||||
.login-page span {
|
||||
.auth button:not(.MuiIconButton-root),
|
||||
.auth .field h3.MuiTypography-root,
|
||||
.auth .field input,
|
||||
.auth .field .input-error,
|
||||
.auth p,
|
||||
.auth span {
|
||||
font-size: var(--env-var-font-size-medium-plus);
|
||||
}
|
||||
.login-page p {
|
||||
.auth p {
|
||||
color: var(--env-var-color-2-light);
|
||||
}
|
||||
.login-page .field h3.MuiTypography-root,
|
||||
.login-page .field input {
|
||||
.auth .field h3.MuiTypography-root,
|
||||
.auth .field input {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
|
||||
.login-page p + span {
|
||||
.auth p + span {
|
||||
color: var(--env-var-color-3);
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
transition: opacity 300ms ease-in;
|
||||
}
|
||||
.login-page p > span {
|
||||
.auth p > span {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 200ms;
|
||||
}
|
||||
.login-page p > span:hover {
|
||||
.auth p > span:hover {
|
||||
color: var(--env-var-color-2);
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
.login-page p + span:hover {
|
||||
.auth p + span:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.login-page button:not(.MuiIconButton-root) {
|
||||
.auth button:not(.MuiIconButton-root) {
|
||||
user-select: none;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
line-height: 1;
|
||||
}
|
||||
.login-page button:not(.MuiIconButton-root),
|
||||
.login-page .field .MuiInputBase-root:has(input) {
|
||||
.auth button:not(.MuiIconButton-root),
|
||||
.auth .field .MuiInputBase-root:has(input) {
|
||||
height: 38px;
|
||||
}
|
||||
.login-page .field svg {
|
||||
.auth .field svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.auth .check span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
.login-page > .MuiStack-root:nth-of-type(1) {
|
||||
.auth > .MuiStack-root:nth-of-type(1) {
|
||||
height: var(--env-var-nav-bar-height);
|
||||
}
|
||||
.login-page .background-pattern-svg {
|
||||
.auth .background-pattern-svg {
|
||||
top: 14%;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
|
||||
@@ -28,8 +28,18 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
rowsPerPage: 12,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
page: 0,
|
||||
});
|
||||
}, [filter, selectedMonitor]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPage = async () => {
|
||||
if (!monitors || Object.keys(monitors).length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let url = `/checks/${selectedMonitor}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`;
|
||||
|
||||
@@ -52,6 +62,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
}, [
|
||||
authToken,
|
||||
user,
|
||||
monitors,
|
||||
selectedMonitor,
|
||||
filter,
|
||||
paginationController.page,
|
||||
|
||||
@@ -27,6 +27,13 @@ const PaginationTable = ({ monitorId, dateRange }) => {
|
||||
rowsPerPage: 5,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
page: 0,
|
||||
});
|
||||
}, [dateRange]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPage = async () => {
|
||||
try {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg width="353" height="53" viewBox="0 0 353 53" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="353" height="52.4582" fill="white"/>
|
||||
<path d="M1.67676 4.59633H11.2078C12.5021 4.59633 13.767 4.74621 15.0025 5.04598C16.238 5.34574 17.3411 5.84534 18.3119 6.54478C19.2826 7.21093 20.0622 8.09355 20.6505 9.19267C21.2683 10.2918 21.5771 11.6574 21.5771 13.2894C21.5771 15.3544 21.077 17.0697 20.0769 18.4353C19.1061 19.7676 17.7824 20.7168 16.1056 21.283V21.383C17.1058 21.4829 18.0324 21.7826 18.8855 22.2822C19.768 22.7485 20.5181 23.3647 21.1359 24.1308C21.783 24.8968 22.2831 25.7962 22.6361 26.8286C22.9891 27.8278 23.1656 28.9102 23.1656 30.076C23.1656 32.0412 22.7978 33.6564 22.0625 34.9222C21.3565 36.1879 20.4152 37.2036 19.2385 37.9697C18.0913 38.7024 16.7822 39.2187 15.3114 39.5185C13.87 39.8183 12.4285 39.9682 10.9871 39.9682H1.67676V4.59633ZM5.38326 19.5844H10.5018C11.3843 19.5844 12.2373 19.5178 13.061 19.3846C13.9141 19.2513 14.6789 18.9849 15.3555 18.5852C16.0321 18.1522 16.5763 17.5693 16.9881 16.8366C17.4 16.0705 17.6059 15.0713 17.6059 13.839C17.6059 12.7398 17.4147 11.8572 17.0323 11.1911C16.6498 10.4916 16.135 9.94207 15.4879 9.54239C14.8701 9.14271 14.1494 8.87625 13.3258 8.74303C12.5315 8.57649 11.7225 8.49323 10.8989 8.49323H5.38326V19.5844ZM5.38326 36.0713H11.649C12.5609 36.0713 13.4581 35.9713 14.3406 35.7715C15.2525 35.5384 16.0615 35.1887 16.7675 34.7223C17.5029 34.2227 18.0913 33.59 18.5325 32.8238C18.9738 32.0245 19.1944 31.0585 19.1944 29.9262C19.1944 28.6939 18.9738 27.6612 18.5325 26.8286C18.1207 25.996 17.5618 25.3298 16.8558 24.8302C16.1792 24.3306 15.3849 23.9809 14.473 23.781C13.5905 23.5812 12.6786 23.4813 11.7373 23.4813H5.38326V36.0713ZM29.1616 2.19824H32.6033V39.9682H29.1616V2.19824ZM57.4558 34.4226C57.4558 35.4885 57.4854 36.4877 57.5441 37.4202C57.603 38.3527 57.6323 39.202 57.6323 39.9682H54.3671C54.3671 39.3354 54.3525 38.7024 54.323 38.0697C54.2936 37.4369 54.2788 36.7874 54.2788 36.1212H54.1906C53.9554 36.6875 53.617 37.2371 53.1757 37.7699C52.764 38.3027 52.2639 38.7858 51.6755 39.2187C51.0873 39.6184 50.4254 39.9349 49.6898 40.168C48.9545 40.4345 48.1602 40.5677 47.3071 40.5677C45.954 40.5677 44.7774 40.3346 43.7771 39.8682C42.8063 39.4019 41.9975 38.7524 41.3502 37.9198C40.7031 37.0872 40.2177 36.088 39.8941 34.9222C39.6 33.7231 39.4528 32.4242 39.4528 31.0253V16.5868H42.8946V30.6256C42.8946 32.5908 43.2771 34.1395 44.0418 35.2719C44.8067 36.4042 45.9981 36.9705 47.616 36.9705C48.7339 36.9705 49.6898 36.754 50.4841 36.3211C51.3079 35.8882 51.9698 35.2719 52.4697 34.4725C52.9992 33.6732 53.3818 32.7239 53.617 31.6248C53.8817 30.5257 54.0141 29.3099 54.0141 27.9777V16.5868H57.4558V34.4226ZM82.4158 35.7215C81.1803 37.5201 79.8272 38.7858 78.3563 39.5185C76.8856 40.218 75.1793 40.5677 73.2378 40.5677C71.62 40.5677 70.1638 40.2512 68.8694 39.6184C67.5752 38.9522 66.4721 38.0697 65.56 36.9705C64.6775 35.8714 63.9863 34.5724 63.4862 33.0736C63.0156 31.5748 62.7802 29.9761 62.7802 28.2775C62.7802 26.4789 63.0449 24.8302 63.5744 23.3314C64.1039 21.7993 64.8395 20.5003 65.7807 19.4345C66.7221 18.3354 67.84 17.4861 69.1342 16.8865C70.4286 16.287 71.8406 15.9873 73.3702 15.9873C74.8117 15.9873 76.1355 16.2704 77.3414 16.8366C78.5476 17.3695 79.5772 18.1688 80.4302 19.2347C81.3127 20.2672 81.9893 21.5495 82.4599 23.0816C82.9307 24.5804 83.1659 26.2958 83.1659 28.2275V29.4765H66.4867C66.5456 30.4757 66.7514 31.4417 67.1044 32.3742C67.487 33.2735 67.9724 34.0728 68.5605 34.7723C69.149 35.4385 69.8402 35.9713 70.6344 36.371C71.4287 36.7707 72.2966 36.9705 73.2378 36.9705C74.738 36.9705 76.0031 36.6708 77.0326 36.0713C78.0622 35.4717 78.9888 34.5892 79.8124 33.4234L82.4158 35.7215ZM79.4594 26.1791C79.4007 24.1807 78.8271 22.582 77.7386 21.383C76.6502 20.1839 75.15 19.5844 73.2378 19.5844C71.3259 19.5844 69.7815 20.1839 68.6047 21.383C67.4281 22.582 66.7221 24.1807 66.4867 26.1791H79.4594ZM85.4898 4.59633H89.5934L96.6975 33.9729H96.7858L104.464 4.59633H108.611L116.245 33.9729H116.333L123.57 4.59633H127.453L118.275 39.9682H114.259L106.537 9.99204H106.449L98.6831 39.9682H94.6236L85.4898 4.59633ZM129.076 19.5844C130.135 18.352 131.385 17.4528 132.826 16.8865C134.297 16.287 135.783 15.9873 137.283 15.9873C140.342 15.9873 142.548 16.8033 143.902 18.4353C145.255 20.0673 145.931 22.5487 145.931 25.8794V35.9214C145.931 36.5876 145.961 37.287 146.02 38.0197C146.078 38.7191 146.152 39.3686 146.24 39.9682H142.931C142.813 39.4353 142.74 38.8523 142.71 38.2195C142.71 37.5533 142.71 36.9705 142.71 36.4709H142.622C141.946 37.67 141.033 38.6524 139.886 39.4186C138.768 40.1847 137.415 40.5677 135.827 40.5677C134.768 40.5677 133.768 40.4178 132.826 40.118C131.914 39.8183 131.105 39.3854 130.399 38.8191C129.723 38.2195 129.179 37.4869 128.767 36.6208C128.355 35.7548 128.149 34.7556 128.149 33.6232C128.149 31.6915 128.59 30.176 129.473 29.0768C130.355 27.9777 131.444 27.1616 132.738 26.6288C134.062 26.096 135.474 25.7627 136.974 25.6296C138.504 25.4964 139.901 25.4297 141.166 25.4297H142.49V24.7303C142.49 23.0316 142.034 21.7493 141.122 20.8834C140.239 20.0174 138.989 19.5844 137.371 19.5844C136.253 19.5844 135.15 19.8009 134.062 20.2339Line truncated
|
||||
<path d="M234.357 26.4788C234.357 28.4105 234.136 30.2425 233.695 31.9744C233.282 33.7062 232.605 35.2385 231.665 36.5707C230.752 37.8697 229.547 38.9188 228.047 39.7182C226.547 40.4843 224.722 40.8673 222.575 40.8673C220.427 40.8673 218.604 40.4843 217.103 39.7182C215.603 38.9188 214.382 37.8697 213.441 36.5707C212.529 35.2385 211.852 33.7062 211.411 31.9744C210.999 30.2425 210.793 28.4105 210.793 26.4788V4.59619H214.5V25.8792C214.5 27.3113 214.632 28.6937 214.897 30.0259C215.162 31.3581 215.603 32.5407 216.221 33.5731C216.839 34.6055 217.662 35.4384 218.692 36.0711C219.721 36.6706 221.015 36.9704 222.575 36.9704C224.133 36.9704 225.428 36.6706 226.458 36.0711C227.486 35.4384 228.312 34.6055 228.929 33.5731C229.547 32.5407 229.988 31.3581 230.253 30.0259C230.518 28.6937 230.65 27.3113 230.65 25.8792V4.59619H234.357V26.4788ZM241.2 16.5866H244.642V19.9839H244.73C245.67 18.7183 246.76 17.7357 247.996 17.0363C249.26 16.3368 250.643 15.9871 252.143 15.9871C253.761 15.9871 255.232 16.3035 256.556 16.9364C257.908 17.5359 259.056 18.3852 259.998 19.4843C260.938 20.5835 261.674 21.8824 262.204 23.3812C262.733 24.88 262.998 26.512 262.998 28.2773C262.998 30.0427 262.733 31.6746 262.204 33.1734C261.674 34.6722 260.938 35.9712 259.998 37.0703C259.084 38.1694 257.997 39.0355 256.732 39.6682C255.466 40.2678 254.114 40.5675 252.673 40.5675C250.82 40.5675 249.187 40.1179 247.775 39.2186C246.392 38.2861 245.377 37.1535 244.73 35.8213H244.642V51.3589H241.2V16.5866ZM244.642 28.2773C244.642 29.5096 244.803 30.6587 245.128 31.7246C245.481 32.757 245.979 33.673 246.628 34.4724C247.274 35.2385 248.04 35.8545 248.922 36.3209C249.833 36.7538 250.848 36.9704 251.967 36.9704C253.083 36.9704 254.085 36.7538 254.967 36.3209C255.879 35.8545 256.657 35.2385 257.306 34.4724C257.952 33.673 258.438 32.757 258.762 31.7246C259.115 30.6587 259.292 29.5096 259.292 28.2773C259.292 27.0451 259.115 25.9125 258.762 24.88C258.438 23.8142 257.952 22.8983 257.306 22.1322C256.657 21.3329 255.879 20.7167 254.967 20.2837C254.085 19.8174 253.083 19.5842 251.967 19.5842C250.848 19.5842 249.833 19.8174 248.922 20.2837C248.04 20.7167 247.274 21.3329 246.628 22.1322C245.979 22.8983 245.481 23.8142 245.128 24.88C244.803 25.9125 244.642 27.0451 244.642 28.2773ZM279.647 19.884H273.557V31.5747C273.557 32.3408 273.573 33.0568 273.601 33.723C273.632 34.3557 273.749 34.922 273.954 35.4216C274.162 35.888 274.471 36.271 274.881 36.5707C275.294 36.8373 275.883 36.9704 276.646 36.9704C277.147 36.9704 277.661 36.9037 278.19 36.7706C278.72 36.6374 279.221 36.4376 279.691 36.171L279.823 39.7182C279.236 40.018 278.574 40.2345 277.837 40.3677C277.131 40.5008 276.456 40.5675 275.808 40.5675C274.572 40.5675 273.588 40.3844 272.851 40.018C272.117 39.6515 271.543 39.1354 271.13 38.4692C270.72 37.803 270.44 36.9871 270.292 36.0212C270.175 35.0219 270.116 33.9228 270.116 32.7238V19.884H265.615V16.5866H270.116V9.94193H273.557V16.5866H279.647V19.884ZM284.216 16.5866H287.657V39.968H284.216V16.5866ZM283.421 7.44392C283.421 6.67786 283.658 6.01174 284.127 5.44551C284.628 4.87929 285.231 4.59619 285.937 4.59619C286.643 4.59619 287.232 4.87929 287.702 5.44551C288.202 6.01174 288.452 6.67786 288.452 7.44392C288.452 8.27658 288.216 8.95939 287.746 9.49229C287.276 10.0252 286.671 10.2917 285.937 10.2917C285.202 10.2917 284.597 10.0252 284.127 9.49229C283.658 8.95939 283.421 8.27658 283.421 7.44392ZM294.508 22.1322C294.508 21.0664 294.477 20.0672 294.42 19.1346C294.36 18.202 294.331 17.3527 294.331 16.5866H297.597C297.597 17.2195 297.61 17.8523 297.641 18.4851C297.669 19.1179 297.685 19.7674 297.685 20.4336H297.773C298.007 19.8673 298.331 19.3178 298.744 18.7849C299.185 18.252 299.699 17.7857 300.288 17.386C300.875 16.953 301.537 16.6199 302.274 16.3868C303.009 16.1203 303.803 15.9871 304.657 15.9871C305.773 15.9871 306.7 16.1537 307.436 16.4867C308.2 16.7865 308.833 17.1695 309.334 17.6358C309.832 18.0688 310.216 18.5517 310.481 19.0846C310.775 19.5842 311.011 20.0339 311.187 20.4336C311.95 19.0347 312.833 17.9522 313.835 17.1862C314.834 16.3868 316.129 15.9871 317.718 15.9871C319.187 15.9871 320.422 16.2203 321.424 16.6866C322.424 17.1195 323.233 17.769 323.851 18.635C324.469 19.4677 324.91 20.5002 325.175 21.7325C325.439 22.9316 325.572 24.2805 325.572 25.7793V39.968H322.13V25.9792C322.13 25.1798 322.055 24.3971 321.909 23.631C321.762 22.865 321.497 22.1822 321.115 21.5827C320.731 20.9831 320.217 20.5002 319.571 20.1338C318.922 19.7674 318.099 19.5842 317.1 19.5842C316.363 19.5842 315.672 19.7508 315.026 20.0839C314.377 20.4169 313.804 20.8832 313.305 21.4827C312.833 22.0823 312.451 22.815 312.158 23.681C311.893 24.5136 311.761 25.4463 311.761 26.4788V39.968H308.319V26.7286C308.319 24.1306 308.01 22.2988 307.392 21.2329C306.775 20.1338 305.76 19.5842 304.348 19.5842C303.229 19.5842 302.258 19.8008 301.435 20.2337C300.641 20.6667 299.979 21.2829 299.45 22.0823C298.949 22.8816 298.567 23.8309 298.303 24.93C298.067 26.0291 297.95 27.2449 297.95 28.5771V39.968H294.508V22.1322ZM350.527 35.72Line truncated
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,4 +1,4 @@
|
||||
FROM node:20
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -11,10 +11,7 @@
|
||||
|
||||
<p align="center"><strong>An open source server monitoring application</strong></p>
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
BlueWave Uptime is an open source server monitoring application used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
|
||||
|
||||
|
||||
@@ -135,6 +135,10 @@ const loginController = async (req, res, next) => {
|
||||
throw new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
|
||||
}
|
||||
|
||||
if (user.authToken) {
|
||||
throw new Error(errorMessages.AUTH_ALREADY_LOGGED_IN);
|
||||
}
|
||||
|
||||
// Remove password from user object. Should this be abstracted to DB layer?
|
||||
const userWithoutPassword = { ...user._doc };
|
||||
delete userWithoutPassword.password;
|
||||
@@ -142,6 +146,8 @@ const loginController = async (req, res, next) => {
|
||||
|
||||
// Happy path, return token
|
||||
const token = issueToken(userWithoutPassword);
|
||||
user.authToken = token;
|
||||
await user.save();
|
||||
// reset avatar image
|
||||
userWithoutPassword.avatarImage = user.avatarImage;
|
||||
|
||||
@@ -157,6 +163,23 @@ const loginController = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
const logoutController = async (req, res, next) => {
|
||||
try {
|
||||
// Get user
|
||||
const { email } = req.body;
|
||||
const userToLogout = await req.db.getUserByEmail({ body: { email } }, res);
|
||||
userToLogout.authToken = null;
|
||||
await userToLogout.save();
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
.json({ success: true, msg: successMessages.AUTH_LOGOUT_USER });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const userEditController = async (req, res, next) => {
|
||||
try {
|
||||
await editUserParamValidation.validateAsync(req.params);
|
||||
@@ -524,6 +547,7 @@ const getAllUsersController = async (req, res) => {
|
||||
module.exports = {
|
||||
registerController,
|
||||
loginController,
|
||||
logoutController,
|
||||
userEditController,
|
||||
inviteController,
|
||||
inviteVerifyController,
|
||||
|
||||
@@ -37,6 +37,7 @@ const {
|
||||
updateUser,
|
||||
deleteUser,
|
||||
getAllUsers,
|
||||
logoutUser,
|
||||
} = require("./modules/userModule");
|
||||
|
||||
//****************************************
|
||||
@@ -133,6 +134,7 @@ module.exports = {
|
||||
updateUser,
|
||||
deleteUser,
|
||||
getAllUsers,
|
||||
logoutUser,
|
||||
requestInviteToken,
|
||||
getInviteToken,
|
||||
requestRecoveryToken,
|
||||
|
||||
@@ -27,7 +27,6 @@ const insertUser = async (req, res) => {
|
||||
const avatar = await GenerateAvatarImage(req.file);
|
||||
userData.avatarImage = avatar;
|
||||
}
|
||||
console.log(userData);
|
||||
const newUser = new UserModel(userData);
|
||||
await newUser.save();
|
||||
return await UserModel.findOne({ _id: newUser._id })
|
||||
@@ -151,10 +150,20 @@ const getAllUsers = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
const logoutUser = async (userId) => {
|
||||
try {
|
||||
await UserModel.updateOne({ _id: userId }, { $unset: { authToken: null } });
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
insertUser,
|
||||
getUserByEmail,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
getAllUsers,
|
||||
logoutUser,
|
||||
};
|
||||
@@ -3,6 +3,8 @@ const logger = require("../utils/logger");
|
||||
const SERVICE_NAME = "verifyJWT";
|
||||
const TOKEN_PREFIX = "Bearer ";
|
||||
const { errorMessages } = require("../utils/messages");
|
||||
const { parse } = require("path");
|
||||
const User = require("../models/user");
|
||||
/**
|
||||
* Verifies the JWT token
|
||||
* @function
|
||||
@@ -32,14 +34,27 @@ const verifyJWT = (req, res, next) => {
|
||||
|
||||
const parsedToken = token.slice(TOKEN_PREFIX.length, token.length);
|
||||
// Verify the token's authenticity
|
||||
jwt.verify(parsedToken, process.env.JWT_SECRET, (err, decoded) => {
|
||||
jwt.verify(parsedToken, process.env.JWT_SECRET, async (err, decoded) => {
|
||||
if (err) {
|
||||
logger.error(errorMessages.INVALID_AUTH_TOKEN, {
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
return res
|
||||
.status(401)
|
||||
.json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN });
|
||||
try {
|
||||
const userId = jwt.decode(parsedToken)._id;
|
||||
await req.db.logoutUser(userId);
|
||||
logger.error(errorMessages.INVALID_AUTH_TOKEN, {
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
return res
|
||||
.status(401)
|
||||
.json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN });
|
||||
} catch (error) {
|
||||
logger.error(errorMessages.UNKNOWN_ERROR, {
|
||||
service: SERVICE_NAME,
|
||||
error: error,
|
||||
});
|
||||
error.status = 401;
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
//Add the user to the request object for use in the route
|
||||
req.user = decoded;
|
||||
|
||||
@@ -40,6 +40,9 @@ const UserSchema = mongoose.Schema(
|
||||
default: "user",
|
||||
enum: ["user", "admin"],
|
||||
},
|
||||
authToken: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -9,6 +9,7 @@ const User = require("../models/user");
|
||||
const {
|
||||
registerController,
|
||||
loginController,
|
||||
logoutController,
|
||||
userEditController,
|
||||
recoveryRequestController,
|
||||
validateRecoveryTokenController,
|
||||
@@ -23,6 +24,7 @@ const {
|
||||
//Auth routes
|
||||
router.post("/register", upload.single("profileImage"), registerController);
|
||||
router.post("/login", loginController);
|
||||
router.post("/logout", logoutController);
|
||||
router.put(
|
||||
"/user/:userId",
|
||||
upload.single("profileImage"),
|
||||
|
||||
@@ -7,6 +7,7 @@ const errorMessages = {
|
||||
UNAUTHORIZED: "Unauthorized access",
|
||||
AUTH_ADMIN_EXISTS: "Admin already exists",
|
||||
AUTH_INVITE_NOT_FOUND: "Invite not found",
|
||||
AUTH_ALREADY_LOGGED_IN: "User already logged in",
|
||||
|
||||
//Error handling middleware
|
||||
UNKNOWN_SERVICE: "Unknown service",
|
||||
@@ -55,6 +56,7 @@ const successMessages = {
|
||||
// Auth Controller
|
||||
AUTH_CREATE_USER: "User created successfully",
|
||||
AUTH_LOGIN_USER: "User logged in successfully",
|
||||
AUTH_LOGOUT_USER: "User logged out successfully",
|
||||
AUTH_UPDATE_USER: "User updated successfully",
|
||||
AUTH_CREATE_RECOVERY_TOKEN: "Recovery token created successfully",
|
||||
AUTH_VERIFY_RECOVERY_TOKEN: "Recovery token verified successfully",
|
||||
|
||||
Reference in new issue
Block a user