Merge pull request #358 from bluewave-labs/feat/form-validation
Refactored form validation and page structure, resolves #332
@@ -2,6 +2,8 @@
|
||||
margin: 0;
|
||||
width: fit-content;
|
||||
}
|
||||
.alert, .alert button{
|
||||
.alert,
|
||||
.alert button,
|
||||
.alert .MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, IconButton, Stack } from "@mui/material";
|
||||
import { Box, IconButton, Stack, Typography } from "@mui/material";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
||||
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
||||
@@ -50,9 +50,17 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
|
||||
}}
|
||||
>
|
||||
{hasIcon && <Box sx={{ color: color }}>{icon}</Box>}
|
||||
<Stack direction="column" gap="2px" sx={{ flex: 1, color: color }}>
|
||||
{title && <Box sx={{ fontWeight: "700" }}>{title}</Box>}
|
||||
{body && <Box sx={{ fontWeight: "400" }}>{body}</Box>}
|
||||
<Stack direction="column" gap="2px" sx={{ flex: 1 }}>
|
||||
{title && (
|
||||
<Typography sx={{ fontWeight: "700", color: `${color} !important` }}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{body && (
|
||||
<Typography sx={{ fontWeight: "400", color: `${color} !important` }}>
|
||||
{body}
|
||||
</Typography>
|
||||
)}
|
||||
{hasIcon && isToast && (
|
||||
<Button
|
||||
level="tertiary"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.background-pattern {
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
top: -5%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: -1;
|
||||
|
||||
@@ -63,6 +63,10 @@ const Button = ({ id, type, level, label, disabled, img, onClick, sx }) => {
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import "./check.css";
|
||||
import React from "react";
|
||||
import CheckGrey from "../../assets/Images/Check-icon-grey.png";
|
||||
import PropTypes from "prop-types";
|
||||
import CheckGrey from "../../assets/icons/check.svg?react";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
/**
|
||||
* `Check` is a functional React component that displays a check icon and a label.
|
||||
@@ -8,6 +10,7 @@ import CheckGrey from "../../assets/Images/Check-icon-grey.png";
|
||||
* @component
|
||||
* @param {Object} props - The properties that define the `Check` component.
|
||||
* @param {string} props.text - The text to be displayed as the label next to the check icon.
|
||||
* @param {'info' | 'error' | 'success'} [props.variant='info'] - The variant of the check component, affecting its styling.
|
||||
*
|
||||
* @example
|
||||
* // To use this component, import it and use it in your JSX like this:
|
||||
@@ -15,14 +18,30 @@ import CheckGrey from "../../assets/Images/Check-icon-grey.png";
|
||||
*
|
||||
* @returns {React.Element} The `Check` component with a check icon and a label, defined by the `text` prop.
|
||||
*/
|
||||
const Check = ({ text }) => {
|
||||
const Check = ({ text, variant = "info" }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div className="check">
|
||||
<img className="check-icon" src={CheckGrey} alt="un-checked" />
|
||||
<div className="check-h-spacing" />
|
||||
<label className="check-label">{text}</label>
|
||||
</div>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.gap.small}
|
||||
className={`check${
|
||||
variant === "error"
|
||||
? " check-error"
|
||||
: variant === "success"
|
||||
? " check-success"
|
||||
: " check-info"
|
||||
}`}
|
||||
alignItems="center"
|
||||
>
|
||||
<CheckGrey alt="form checks" />
|
||||
<Typography component="span">{text}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Check.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
variant: PropTypes.oneOf(["info", "error", "success"]),
|
||||
};
|
||||
|
||||
export default Check;
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
.check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
width: var(--env-var-img-width-1);
|
||||
height: var(--env-var-img-width-1);
|
||||
}
|
||||
|
||||
.check-h-spacing {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.check-label {
|
||||
font-size: var(--env-var-font-size-small);
|
||||
font-weight: 600;
|
||||
.check span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
color: var(--env-var-color-2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.check-error span.MuiTypography-root {
|
||||
color: var(--env-var-color-24);
|
||||
}
|
||||
.check-error svg > path {
|
||||
fill: var(--env-var-color-19);
|
||||
}
|
||||
.check-success span.MuiTypography-root {
|
||||
color: var(--env-var-color-17);
|
||||
}
|
||||
.check-success svg > path {
|
||||
fill: var(--env-var-color-23);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
.field .MuiInputBase-root:has(.MuiInputAdornment-root) {
|
||||
padding-right: var(--env-var-spacing-1-minus);
|
||||
}
|
||||
.field .MuiInputBase-root:has(.copy.MuiInputAdornment-root > .MuiButtonBase-root) {
|
||||
.field
|
||||
.MuiInputBase-root:has(.copy.MuiInputAdornment-root > .MuiButtonBase-root) {
|
||||
padding-right: 0;
|
||||
}
|
||||
.field .MuiInputBase-root .copy.MuiInputAdornment-root .MuiButtonBase-root {
|
||||
@@ -64,7 +65,7 @@
|
||||
border-color: var(--env-var-color-29);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
}
|
||||
.field:not(:has(.input-error))
|
||||
.field:not(:has(.Mui-disabled)):not(:has(.input-error))
|
||||
.MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus))
|
||||
fieldset {
|
||||
border-color: var(--env-var-color-29);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTheme } from "@emotion/react";
|
||||
import { Box, Divider, Stack, Typography } from "@mui/material";
|
||||
import ButtonSpinner from "../../ButtonSpinner";
|
||||
import Field from "../../Inputs/Field";
|
||||
import { editPasswordValidation } from "../../../Validation/validation";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import Alert from "../../Alert";
|
||||
import { update } from "../../../Features/Auth/authSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
@@ -44,9 +44,9 @@ const PasswordPanel = () => {
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const validation = editPasswordValidation.validate(
|
||||
const validation = credentials.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false, context: { newPassword: localData.newPassword } }
|
||||
{ abortEarly: false, context: { password: localData.newPassword } }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
@@ -64,9 +64,9 @@ const PasswordPanel = () => {
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { error } = editPasswordValidation.validate(localData, {
|
||||
const { error } = credentials.validate(localData, {
|
||||
abortEarly: false,
|
||||
context: { newPassword: localData.newPassword },
|
||||
context: { password: localData.newPassword },
|
||||
});
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -7,10 +7,7 @@ import Button from "../../Button";
|
||||
import Avatar from "../../Avatar";
|
||||
import Field from "../../Inputs/Field";
|
||||
import ImageField from "../../Inputs/Image";
|
||||
import {
|
||||
editProfileValidation,
|
||||
imageValidation,
|
||||
} from "../../../Validation/validation";
|
||||
import { credentials, imageValidation } from "../../../Validation/validation";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
clearAuthState,
|
||||
@@ -66,7 +63,7 @@ const ProfilePanel = () => {
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
validateField({ [name]: value }, editProfileValidation, name);
|
||||
validateField({ [name]: value }, credentials, name);
|
||||
};
|
||||
|
||||
// Handles image file
|
||||
@@ -165,8 +162,8 @@ const ProfilePanel = () => {
|
||||
localData.file === undefined
|
||||
) {
|
||||
createToast({
|
||||
variant: "warning",
|
||||
body: "Unable to update profile: No changes detected.",
|
||||
variant: "info",
|
||||
body: "Unable to update profile — no changes detected.",
|
||||
hasIcon: false,
|
||||
});
|
||||
setErrors({ unchanged: "unable to update profile" });
|
||||
@@ -204,6 +201,22 @@ const ProfilePanel = () => {
|
||||
if (action.payload.success) {
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearMonitorState());
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: action.payload.msg,
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: "Unknown error.",
|
||||
hasIcon: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -96,6 +96,43 @@ export const deleteUser = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const forgotPassword = createAsyncThunk(
|
||||
"auth/forgotPassword",
|
||||
async (form, thunkApi) => {
|
||||
try {
|
||||
const res = await axiosInstance.post("/auth/recovery/request", form);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const setNewPassword = createAsyncThunk(
|
||||
"auth/setNewPassword",
|
||||
async (data, thunkApi) => {
|
||||
const { token, form } = data;
|
||||
try {
|
||||
await axiosInstance.post("/auth/recovery/validate", {
|
||||
recoveryToken: token,
|
||||
});
|
||||
const res = await axiosInstance.post("/auth/recovery/reset", {
|
||||
...form,
|
||||
recoveryToken: token,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleAuthFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
@@ -133,6 +170,25 @@ const handleDeleteRejected = (state, action) => {
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to delete account.";
|
||||
};
|
||||
const handleForgotFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
};
|
||||
const handleForgotRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to send reset instructions.";
|
||||
};
|
||||
const handleNewPasswordRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to reset password.";
|
||||
};
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: "auth",
|
||||
@@ -178,6 +234,22 @@ const authSlice = createSlice({
|
||||
})
|
||||
.addCase(deleteUser.fulfilled, handleDeleteFulfilled)
|
||||
.addCase(deleteUser.rejected, handleDeleteRejected);
|
||||
|
||||
// Forgot password thunk
|
||||
builder
|
||||
.addCase(forgotPassword.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(forgotPassword.fulfilled, handleForgotFulfilled)
|
||||
.addCase(forgotPassword.rejected, handleForgotRejected);
|
||||
|
||||
// Set new password thunk
|
||||
builder
|
||||
.addCase(setNewPassword.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(setNewPassword.fulfilled, handleAuthFulfilled)
|
||||
.addCase(setNewPassword.rejected, handleNewPasswordRejected);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -67,17 +67,6 @@
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
.edit-profile-form__wrapper input,
|
||||
.edit-password-form__wrapper input,
|
||||
#edit-organization-name,
|
||||
#input-team-member {
|
||||
padding: 10px;
|
||||
}
|
||||
#edit-new-password + div > button > svg,
|
||||
#edit-confirm-password + div > button > svg,
|
||||
#edit-current-password + div > button > svg {
|
||||
color: var(--env-var-color-29);
|
||||
}
|
||||
|
||||
.edit-team-form__wrapper td {
|
||||
padding: 10px 0;
|
||||
|
||||
@@ -1,83 +1,34 @@
|
||||
.check-email-page {
|
||||
width: var(--env-var-width-1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: var(--env-var-height-1);
|
||||
height: var(--env-var-height-1);
|
||||
}
|
||||
.check-email-page button:not(.MuiIconButton-root) {
|
||||
height: 34px;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
line-height: 0;
|
||||
}
|
||||
.check-email-page h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
color: var(--env-var-color-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
.check-email-page p.MuiTypography-root,
|
||||
.check-email-page button,
|
||||
.check-email-page span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.check-email-page span.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
.check-email-page p.MuiTypography-root {
|
||||
color: var(--env-var-color-2);
|
||||
}
|
||||
.check-email-page button svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.check-email-form {
|
||||
width: var(--env-var-width-2);
|
||||
margin: 90px auto;
|
||||
}
|
||||
|
||||
.check-email-form-header {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.check-email-v-gap-large {
|
||||
height: var(--env-var-spacing-3);
|
||||
}
|
||||
|
||||
.check-email-v-gap-medium {
|
||||
height: var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
.check-email-v-gap-small {
|
||||
height: var(--env-var-spacing-1);
|
||||
}
|
||||
|
||||
.check-email-form-heading {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 700;
|
||||
color: var(--env-var-color-1);
|
||||
}
|
||||
|
||||
.check-email-form-subheading {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 400;
|
||||
color: var(--env-var-color-2);
|
||||
}
|
||||
|
||||
.check-email-form-subheading span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.check-email-body {
|
||||
margin-top: 65px;
|
||||
width: var(--env-var-width-2);
|
||||
}
|
||||
|
||||
.check-email-resend {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 400;
|
||||
color: var(--env-var-color-2);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.check-email-resend span {
|
||||
font-weight: 700;
|
||||
color: var(--env-var-color-3);
|
||||
}
|
||||
|
||||
.check-email-back-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.check-email-back-button-img {
|
||||
width: var(--env-var-img-width-1);
|
||||
height: var(--env-var-img-width-1);
|
||||
margin-right: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.check-email-back-button-text {
|
||||
height: var(--env-var-img-width-1);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 500;
|
||||
color: var(--env-var-color-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -1,56 +1,143 @@
|
||||
import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPattern";
|
||||
import "./index.css";
|
||||
import React from "react";
|
||||
import EmailIcon from "../../assets/Images/email.png";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import EmailIcon from "../../assets/icons/email.svg?react";
|
||||
import Button from "../../Components/Button";
|
||||
import LeftArrow from "../../assets/Images/arrow-left.png";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { forgotPassword } from "../../Features/Auth/authSlice";
|
||||
|
||||
const CheckEmail = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [email, setEmail] = useState();
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
useEffect(() => {
|
||||
setEmail(sessionStorage.getItem("email"));
|
||||
}, []);
|
||||
|
||||
// TODO - fix
|
||||
const openMail = () => {
|
||||
window.location.href = "mailto:";
|
||||
};
|
||||
|
||||
const toastFail = [
|
||||
{
|
||||
variant: "info",
|
||||
body: "Email not found.",
|
||||
hasIcon: false,
|
||||
},
|
||||
{
|
||||
variant: "info",
|
||||
body: "Redirecting in 3...",
|
||||
hasIcon: false,
|
||||
},
|
||||
{
|
||||
variant: "info",
|
||||
body: "Redirecting in 2...",
|
||||
hasIcon: false,
|
||||
},
|
||||
{
|
||||
variant: "info",
|
||||
body: "Redirecting in 1...",
|
||||
hasIcon: false,
|
||||
},
|
||||
];
|
||||
|
||||
const resendToken = async () => {
|
||||
setDisabled(true); // prevent resent button from being spammed
|
||||
if (!email) {
|
||||
let index = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (index < toastFail.length) {
|
||||
createToast(toastFail[index]);
|
||||
index++;
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
navigate("/forgot-password");
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
const form = { email: email };
|
||||
const action = await dispatch(forgotPassword(form));
|
||||
if (action.payload.success) {
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: `Instructions sent to ${form.email}.`,
|
||||
hasIcon: false,
|
||||
});
|
||||
setDisabled(false);
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: action.payload.msg,
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: "Unknown error.",
|
||||
hasIcon: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="check-email-page">
|
||||
<BackgroundPattern />
|
||||
<div className="check-email-form">
|
||||
<div className="check-email-form-header">
|
||||
<img
|
||||
className="check-email-form-header-logo"
|
||||
src={EmailIcon}
|
||||
alt="EmailIcon"
|
||||
/>
|
||||
<div className="check-email-v-gap-medium"></div>
|
||||
<div className="check-email-form-heading">Check your email</div>
|
||||
<div className="check-email-v-gap-small"></div>
|
||||
<div className="check-email-form-subheading">
|
||||
We sent a password reset link to <span>username@email.com</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="check-email-v-gap-large"></div>
|
||||
<div className="check-email-body">
|
||||
<form className="check-email-form">
|
||||
<Stack direction="column" alignItems="center" gap={theme.gap.small}>
|
||||
<EmailIcon alt="EmailIcon" style={{ fill: "white" }} />
|
||||
<Typography component="h1" sx={{ mt: theme.gap.ml }}>
|
||||
Check your email
|
||||
</Typography>
|
||||
<Typography sx={{ width: "max-content" }}>
|
||||
We sent a password reset link to{" "}
|
||||
<Typography component="span">
|
||||
{email || "username@email.com"}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.ml} sx={{ mt: `calc(${theme.gap.ml}*2)` }}>
|
||||
<Button level="primary" label="Open email app" onClick={openMail} />
|
||||
<Typography sx={{ alignSelf: "center", mb: theme.gap.medium }}>
|
||||
Didn't receive the email?{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={resendToken}
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
letterSpacing: "-0.1px",
|
||||
userSelect: "none",
|
||||
pointerEvents: disabled ? "none" : "auto",
|
||||
cursor: disabled ? "default" : "pointer",
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
Click to resend
|
||||
</Typography>
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
level="primary"
|
||||
label="Open email app"
|
||||
sx={{
|
||||
width: "100%",
|
||||
fontSize: "13px",
|
||||
fontWeight: "200",
|
||||
height: "44px",
|
||||
}}
|
||||
level="tertiary"
|
||||
label="Back to log in"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
sx={{ alignSelf: "center", width: "fit-content" }}
|
||||
onClick={() => navigate("/login")}
|
||||
/>
|
||||
</div>
|
||||
<div className="check-email-v-gap-large"></div>
|
||||
<div className="check-email-resend">
|
||||
Didn’t receive the email?
|
||||
<span> Click to resend</span>
|
||||
</div>
|
||||
<div className="check-email-v-gap-large"></div>
|
||||
<div className="check-email-back-button">
|
||||
<img
|
||||
className="check-email-back-button-img"
|
||||
src={LeftArrow}
|
||||
alt="LeftArrow"
|
||||
/>
|
||||
<div className="check-email-back-button-text">Back to log in</div>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,73 +1,31 @@
|
||||
.forgot-password-page {
|
||||
width: var(--env-var-width-1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: var(--env-var-height-1);
|
||||
height: var(--env-var-height-1);
|
||||
}
|
||||
|
||||
.forgot-password-form {
|
||||
width: var(--env-var-width-2);
|
||||
margin: 90px auto;
|
||||
.forgot-password-page button:not(.MuiIconButton-root) {
|
||||
height: 34px;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.forgot-password-form-header {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.forgot-password-form-header-logo {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.forgot-password-v-gap-large {
|
||||
height: var(--env-var-spacing-3);
|
||||
}
|
||||
|
||||
.forgot-password-v-gap-medium {
|
||||
height: var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
.forgot-password-v-gap-small {
|
||||
height: var(--env-var-spacing-1);
|
||||
}
|
||||
|
||||
.forgot-password-form-heading {
|
||||
.forgot-password-page h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 700;
|
||||
color: var(--env-var-color-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.forgot-password-form-subheading {
|
||||
.forgot-password-page p.MuiTypography-root,
|
||||
.forgot-password-page button {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 500;
|
||||
}
|
||||
.forgot-password-page p.MuiTypography-root {
|
||||
color: var(--env-var-color-2);
|
||||
}
|
||||
|
||||
.forgot-password-body {
|
||||
width: var(--env-var-width-2);
|
||||
}
|
||||
|
||||
#forgot-password-email-input {
|
||||
width: 308px;
|
||||
height: 11px;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
.forgot-password-back-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.forgot-password-back-button-img {
|
||||
width: var(--env-var-img-width-1);
|
||||
height: var(--env-var-img-width-1);
|
||||
.forgot-password-page button svg{
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.forgot-password-back-button-text {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 500;
|
||||
color: var(--env-var-color-2);
|
||||
|
||||
.forgot-password-form {
|
||||
margin-top: 65px;
|
||||
width: var(--env-var-width-2);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPattern";
|
||||
import "./index.css";
|
||||
import React from "react";
|
||||
import Logomark from "../../assets/Images/key-password.png";
|
||||
import Button from "../../Components/Button";
|
||||
import LeftArrow from "../../assets/Images/arrow-left.png";
|
||||
import { useState, useEffect } from "react";
|
||||
import { recoveryValidation } from "../../Validation/validation";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import Logomark from "../../assets/icons/key.svg?react";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import { useState } from "react";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { forgotPassword } from "../../Features/Auth/authSlice";
|
||||
import ButtonSpinner from "../../Components/ButtonSpinner";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Button from "../../Components/Button";
|
||||
|
||||
const ForgotPassword = () => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false); // Used to disable the button while loading so user doesn't submit multiple times
|
||||
const { isLoading } = useSelector((state) => state.auth);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [form, setForm] = useState({
|
||||
email: "",
|
||||
@@ -23,92 +30,115 @@ const ForgotPassword = () => {
|
||||
"forgot-password-email-input": "email",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { error } = recoveryValidation.validate(form);
|
||||
if (error === undefined) {
|
||||
setErrors({});
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { error } = credentials.validate(form, { abortEarly: false });
|
||||
|
||||
if (error) {
|
||||
// validation errors
|
||||
const err =
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.";
|
||||
setErrors(err);
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: err,
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
const validationErrors = error.details.reduce((acc, err) => {
|
||||
return { ...acc, [err.path[0]]: err.message };
|
||||
}, {});
|
||||
setErrors(validationErrors);
|
||||
}
|
||||
}, [form]);
|
||||
|
||||
const handleInput = (e) => {
|
||||
const fieldName = idMap[e.target.id];
|
||||
setForm({ ...form, [fieldName]: e.target.value });
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
//TODO show loading spinner
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const { error } = recoveryValidation.validate(form);
|
||||
if (error !== undefined) {
|
||||
throw error;
|
||||
const action = await dispatch(forgotPassword(form));
|
||||
if (action.payload.success) {
|
||||
sessionStorage.setItem("email", form.email);
|
||||
navigate("/check-email");
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: `Instructions sent to ${form.email}.`,
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: action.payload.msg,
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: "Unknown error.",
|
||||
hasIcon: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
await axiosInstance.post(`/auth/recovery/request`, form);
|
||||
navigate("/check-email");
|
||||
} catch (error) {
|
||||
//TODO display error (Toast?)
|
||||
alert(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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 }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
if (error) prevErrors[name] = error.details[0].message;
|
||||
else delete prevErrors[name];
|
||||
return prevErrors;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="forgot-password-page">
|
||||
<BackgroundPattern></BackgroundPattern>
|
||||
<div className="forgot-password-form">
|
||||
<div className="forgot-password-form-header">
|
||||
<img
|
||||
className="forgot-password-form-header-logo"
|
||||
src={Logomark}
|
||||
alt="Logomark"
|
||||
/>
|
||||
<div className="forgot-password-v-gap-medium"></div>
|
||||
<div className="forgot-password-form-heading">Forgot password?</div>
|
||||
<div className="forgot-password-v-gap-small"></div>
|
||||
<div className="forgot-password-form-subheading">
|
||||
No worries, we’ll send you reset instructions.
|
||||
</div>
|
||||
</div>
|
||||
<div className="forgot-password-v-gap-large"></div>
|
||||
<div className="forgot-password-body">
|
||||
<form className="forgot-password-form" onSubmit={handleSubmit}>
|
||||
<Stack direction="column" alignItems="center" gap={theme.gap.small}>
|
||||
<Logomark alt="Logomark" style={{ fill: "white" }} />
|
||||
<Typography component="h1" sx={{ mt: theme.gap.ml }}>
|
||||
Forgot password?
|
||||
</Typography>
|
||||
<Typography>
|
||||
No worries, we'll send you reset instructions.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.ml} sx={{ mt: `calc(${theme.gap.ml}*2)` }}>
|
||||
<Field
|
||||
type="email"
|
||||
id="forgot-password-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="Enter your email"
|
||||
onChange={handleInput}
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
/>
|
||||
<div className="forgot-password-v-gap-medium"></div>
|
||||
<Button
|
||||
disabled={errors.email !== undefined || isLoading === true}
|
||||
<ButtonSpinner
|
||||
disabled={errors.email !== undefined}
|
||||
onClick={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
level="primary"
|
||||
label="Reset password"
|
||||
sx={{
|
||||
width: "100%",
|
||||
fontSize: "13px",
|
||||
fontWeight: "200",
|
||||
height: "44px",
|
||||
}}
|
||||
sx={{ mb: theme.gap.medium }}
|
||||
/>
|
||||
</div>
|
||||
<div className="forgot-password-v-gap-large"></div>
|
||||
<div className="forgot-password-back-button">
|
||||
<img
|
||||
className="forgot-password-back-button-img"
|
||||
src={LeftArrow}
|
||||
alt="LeftArrow"
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back to log in"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
sx={{ alignSelf: "center", width: "fit-content" }}
|
||||
onClick={() => navigate("/login")}
|
||||
/>
|
||||
<div className="forgot-password-back-button-text">Back to log in</div>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,79 +2,57 @@
|
||||
width: var(--env-var-width-1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: var(--env-var-height-1);
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: var(--env-var-width-2);
|
||||
margin: 90px auto;
|
||||
.login-page svg rect {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.login-form-header {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
.login-page p.MuiTypography-root,
|
||||
.login-page span,
|
||||
.login-page button {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
#login-form-header-logo {
|
||||
margin-bottom: 60px;
|
||||
.login-page p.MuiTypography-root {
|
||||
color: var(--env-var-color-2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.login-form-v-spacing {
|
||||
height: var(--env-var-spacing-1);
|
||||
.login-page button:not(.MuiIconButton-root) {
|
||||
height: 34px;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.login-form-v2-spacing {
|
||||
height: var(--env-var-spacing-2);
|
||||
.login-page span:not(.MuiTypography-root):not(.MuiButtonBase-root) {
|
||||
color: var(--env-var-color-3);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-form-v3-spacing {
|
||||
height: var(--env-var-spacing-3);
|
||||
.login-page .MuiFormControlLabel-root {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-form-heading {
|
||||
font-size: var(--env-var-font-size-xlarge);
|
||||
font-weight: 700;
|
||||
color: var(--env-var-color-1);
|
||||
.login-page .MuiFormControlLabel-root .MuiButtonBase-root {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.login-page .MuiFormControlLabel-root span:not(.Mui-checked) svg{
|
||||
fill: var(--env-var-color-4);
|
||||
}
|
||||
.login-page .MuiFormControlLabel-root svg{
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
.login-page .MuiFormControlLabel-root .MuiTypography-root {
|
||||
color: var(--env-var-color-5);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* .login-form-inputs .field h3.MuiTypography-root{
|
||||
font-weight: 600;
|
||||
} */
|
||||
|
||||
.login-form-password-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.login-form-forgot-password {
|
||||
color: var(--env-var-color-3);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
.login-form{
|
||||
margin-top: 95px;
|
||||
}
|
||||
|
||||
.google-enter {
|
||||
width: var(--env-var-img-width-1);
|
||||
height: var(--env-var-img-width-1);
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.new-account-option {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--env-var-color-2);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.new-account-option-span {
|
||||
margin-left: 5px;
|
||||
color: var(--env-var-color-3);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
@@ -4,31 +4,38 @@ import { useNavigate } from "react-router-dom";
|
||||
import "./index.css";
|
||||
import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPattern";
|
||||
import Logomark from "../../assets/Images/bwl-logo-2.svg?react";
|
||||
import CheckBox from "../../Components/Checkbox/Checkbox";
|
||||
import Button from "../../Components/Button";
|
||||
import Google from "../../assets/Images/Google.png";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import { loginValidation } from "../../Validation/validation";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import { login } from "../../Features/Auth/authSlice";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import {
|
||||
Checkbox,
|
||||
Divider,
|
||||
FormControlLabel,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const Login = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const idMap = {
|
||||
"login-email-input": "email",
|
||||
"login-password-input": "password",
|
||||
};
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const [form, setForm] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
axiosInstance
|
||||
@@ -43,138 +50,159 @@ const Login = () => {
|
||||
});
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
const { error } = loginValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
// Creates an error object in the format { field: message }
|
||||
const validationErrors = error.details.reduce((acc, err) => {
|
||||
return { ...acc, [err.path[0]]: err.message };
|
||||
}, {});
|
||||
setErrors(validationErrors);
|
||||
} else {
|
||||
setErrors({});
|
||||
}
|
||||
}, [form]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await loginValidation.validateAsync(form, { abortEarly: false });
|
||||
|
||||
const { error } = credentials.validate(form, { abortEarly: false });
|
||||
|
||||
if (error) {
|
||||
// validation errors
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({
|
||||
variant: "info",
|
||||
body:
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.",
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
const action = await dispatch(login(form));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
if (action.payload.success) {
|
||||
navigate("/monitors");
|
||||
}
|
||||
if (action.meta.requestStatus === "rejected") {
|
||||
const error = new Error("Request rejected");
|
||||
error.response = action.payload;
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === "ValidationError") {
|
||||
// validation errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body:
|
||||
error && error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.",
|
||||
hasIcon: false,
|
||||
});
|
||||
} else if (error.response) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: error.response.msg,
|
||||
body: "Welcome back! You're successfully logged in.",
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: "Unknown error.",
|
||||
hasIcon: false,
|
||||
});
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: action.payload.msg,
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: "Unknown error.",
|
||||
hasIcon: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (e) => {
|
||||
const newForm = { ...form, [idMap[e.target.id]]: e.target.value };
|
||||
setForm(newForm);
|
||||
};
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idMap[id];
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const handleSignupClick = () => {
|
||||
navigate("/register");
|
||||
const { error } = credentials.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
if (error) prevErrors[name] = error.details[0].message;
|
||||
else delete prevErrors[name];
|
||||
return prevErrors;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<BackgroundPattern></BackgroundPattern>
|
||||
<form className="login-form" onSubmit={handleSubmit}>
|
||||
<div className="login-form-header">
|
||||
<Logomark id="login-form-header-logo" alt="Logomark" />
|
||||
<div className="login-form-v-spacing" />
|
||||
<div className="login-form-heading">Log in to your account</div>
|
||||
</div>
|
||||
<div className="login-form-v3-spacing" />
|
||||
|
||||
<div className="login-form-inputs">
|
||||
<Stack gap={theme.gap.large} direction="column">
|
||||
<Logomark alt="BlueWave Uptime Icon" />
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Sign in with Google"
|
||||
sx={{ fontWeight: "600", mt: theme.gap.xxl }}
|
||||
img={<img className="google-enter" src={Google} alt="Google" />}
|
||||
/>
|
||||
<Divider>
|
||||
<Typography>or</Typography>
|
||||
</Divider>
|
||||
<Field
|
||||
type="email"
|
||||
id="login-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="Enter your email"
|
||||
autoComplete="email"
|
||||
onChange={handleInput}
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
/>
|
||||
<div className="login-form-v2-spacing" />
|
||||
<Field
|
||||
type="password"
|
||||
id="login-password-input"
|
||||
label="Password"
|
||||
placeholder="Enter your password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
onChange={handleInput}
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
/>
|
||||
</div>
|
||||
<div className="login-form-v3-spacing" />
|
||||
<div className="login-form-password-options">
|
||||
<CheckBox />
|
||||
<div className="login-form-forgot-password">Forgot password</div>
|
||||
</div>
|
||||
<div className="login-form-v3-spacing" />
|
||||
<div className="login-form-actions">
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
sx={{
|
||||
padding: 0,
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Remember me"
|
||||
/>
|
||||
<span onClick={() => navigate("/forgot-password")}>
|
||||
Forgot password
|
||||
</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.ml} mt={theme.gap.large}>
|
||||
<Button
|
||||
type="submit"
|
||||
level="primary"
|
||||
label="Sign in"
|
||||
sx={{ width: "100%" }}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
/>
|
||||
<div className="login-form-v-spacing" />
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Sign in with Google"
|
||||
sx={{ width: "100%", color: "#344054", fontWeight: "700" }}
|
||||
img={<img className="google-enter" src={Google} alt="Google" />}
|
||||
/>
|
||||
</div>
|
||||
<div className="login-form-v3-spacing" />
|
||||
<div className="new-account-option">
|
||||
Don’t have an account?
|
||||
<span
|
||||
onClick={() => {
|
||||
navigate("/register");
|
||||
}}
|
||||
className="new-account-option-span"
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
gap="5px"
|
||||
mt={theme.gap.ml}
|
||||
>
|
||||
Sign up
|
||||
</span>
|
||||
</div>
|
||||
<Typography component="p" sx={{ alignSelf: "center" }}>
|
||||
Don't have an account?
|
||||
</Typography>
|
||||
<span
|
||||
onClick={() => {
|
||||
navigate("/register");
|
||||
}}
|
||||
>
|
||||
Sign up
|
||||
</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,67 +1,36 @@
|
||||
.password-confirmed-page {
|
||||
width: var(--env-var-width-1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: var(--env-var-height-1);
|
||||
height: var(--env-var-height-1);
|
||||
}
|
||||
.password-confirmed-page button:not(.MuiIconButton-root) {
|
||||
height: 34px;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
line-height: 0;
|
||||
}
|
||||
.password-confirmed-page h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
color: var(--env-var-color-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
.password-confirmed-page p.MuiTypography-root,
|
||||
.password-confirmed-page button {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.password-confirmed-page p.MuiTypography-root {
|
||||
color: var(--env-var-color-2);
|
||||
}
|
||||
.password-confirmed-page button:not(.MuiIconButton-root) svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.password-confirmed-page
|
||||
.MuiFormControl-root:has(#register-password-input)
|
||||
+ span.MuiTypography-root {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.password-confirmed-form {
|
||||
width: var(--env-var-width-2);
|
||||
margin: 90px auto;
|
||||
}
|
||||
|
||||
.password-confirmed-form-header {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.password-confirmed-v-gap-large {
|
||||
height: var(--env-var-spacing-3);
|
||||
}
|
||||
|
||||
.password-confirmed-v-gap-medium {
|
||||
height: var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
.password-confirmed-v-gap-small {
|
||||
height: var(--env-var-spacing-1);
|
||||
}
|
||||
|
||||
.password-confirmed-form-heading {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 700;
|
||||
color: var(--env-var-color-1);
|
||||
}
|
||||
|
||||
.password-confirmed-form-subheading {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 600;
|
||||
color: var(--env-var-color-2);
|
||||
}
|
||||
|
||||
.password-confirmed-body {
|
||||
margin-top: 65px;
|
||||
width: var(--env-var-width-2);
|
||||
}
|
||||
|
||||
.password-confirmed-back-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.password-confirmed-back-button-img {
|
||||
width: var(--env-var-img-width-1);
|
||||
height: var(--env-var-img-width-1);
|
||||
margin-right: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.password-confirmed-back-button-text {
|
||||
height: var(--env-var-img-width-1);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 600;
|
||||
color: var(--env-var-color-2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,54 +1,55 @@
|
||||
import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPattern";
|
||||
import "./index.css";
|
||||
import React from "react";
|
||||
import SuccessIcon from "../../assets/Images/success-icon.png";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import ConfirmIcon from "../../assets/icons/confirm-icon.svg?react";
|
||||
import Button from "../../Components/Button";
|
||||
import LeftArrow from "../../assets/Images/arrow-left.png";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { clearMonitorState } from "../../Features/Monitors/monitorsSlice";
|
||||
|
||||
const NewPasswordConfirmed = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleNavigate = () => {
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearMonitorState());
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="password-confirmed-page">
|
||||
<BackgroundPattern />
|
||||
<div className="password-confirmed-form">
|
||||
<div className="password-confirmed-form-header">
|
||||
<img
|
||||
className="password-confirmed-form-header-logo"
|
||||
src={SuccessIcon}
|
||||
alt="SuccessIcon"
|
||||
/>
|
||||
<div className="password-confirmed-v-gap-medium"></div>
|
||||
<div className="password-confirmed-form-heading">Password reset</div>
|
||||
<div className="password-confirmed-v-gap-small"></div>
|
||||
<div className="password-confirmed-form-subheading">
|
||||
<form className="password-confirmed-form">
|
||||
<Stack direction="column" alignItems="center" gap={theme.gap.small}>
|
||||
<ConfirmIcon alt="confirm icon" style={{ fill: "white" }} />
|
||||
<Typography component="h1" sx={{ mt: theme.gap.ml }}>
|
||||
Password reset
|
||||
</Typography>
|
||||
<Typography sx={{ textAlign: "center" }}>
|
||||
Your password has been successfully reset. Click below to log in
|
||||
magically.
|
||||
</div>
|
||||
</div>
|
||||
<div className="password-confirmed-v-gap-large"></div>
|
||||
<div className="password-confirmed-body">
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.large} sx={{ mt: `calc(${theme.gap.ml}*2)` }}>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Continue"
|
||||
sx={{
|
||||
width: "100%",
|
||||
fontSize: "13px",
|
||||
fontWeight: "200",
|
||||
height: "44px",
|
||||
}}
|
||||
onClick={() => navigate("/monitors")}
|
||||
/>
|
||||
</div>
|
||||
<div className="password-confirmed-v-gap-large"></div>
|
||||
<div className="password-confirmed-back-button">
|
||||
<img
|
||||
className="password-confirmed-back-button-img"
|
||||
src={LeftArrow}
|
||||
alt="LeftArrow"
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back to log in"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
sx={{ alignSelf: "center", width: "fit-content" }}
|
||||
onClick={handleNavigate}
|
||||
/>
|
||||
<div className="password-confirmed-back-button-text">
|
||||
Back to log in
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,44 +1,38 @@
|
||||
.register-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: var(--env-var-height-1);
|
||||
height: var(--env-var-height-1);
|
||||
}
|
||||
.register-page h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
color: var(--env-var-color-1);
|
||||
font-weight: 600;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.register-page p.MuiTypography-root,
|
||||
.register-page span,
|
||||
.register-page button {
|
||||
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) {
|
||||
height: 34px;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
line-height: 0;
|
||||
}
|
||||
.register-page svg rect {
|
||||
fill: none;
|
||||
}
|
||||
.register-page
|
||||
.MuiFormControl-root:has(#register-password-input)
|
||||
+ span.MuiTypography-root {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.register-form {
|
||||
width: var(--env-var-width-2);
|
||||
margin: 140px auto;
|
||||
}
|
||||
|
||||
.register-form-header {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.register-form-header-logo {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.register-form-v-spacing-large {
|
||||
height: var(--env-var-spacing-3);
|
||||
}
|
||||
|
||||
.register-form-v-spacing-small {
|
||||
height: var(--env-var-spacing-1);
|
||||
}
|
||||
|
||||
.register-form-heading {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 700;
|
||||
color: var(--env-var-color-1);
|
||||
}
|
||||
|
||||
.register-form-subheading {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.register-bottom-spacing {
|
||||
height: 50px;
|
||||
margin-top: 95px;
|
||||
}
|
||||
|
||||
@@ -3,20 +3,23 @@ import { useNavigate } from "react-router-dom";
|
||||
|
||||
import "./index.css";
|
||||
import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPattern";
|
||||
import Logomark from "../../assets/Images/Logomark.png";
|
||||
import Logomark from "../../assets/Images/bwl-logo-2.svg?react";
|
||||
import Check from "../../Components/Check/Check";
|
||||
import Button from "../../Components/Button";
|
||||
import Google from "../../assets/Images/Google.png";
|
||||
import { registerValidation } from "../../Validation/validation";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { register } from "../../Features/Auth/authSlice";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Divider, Stack, Typography } from "@mui/material";
|
||||
|
||||
const Register = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
// TODO If possible, change the IDs of these fields to match the backend
|
||||
const idMap = {
|
||||
@@ -24,17 +27,18 @@ const Register = () => {
|
||||
"register-lastname-input": "lastname",
|
||||
"register-email-input": "email",
|
||||
"register-password-input": "password",
|
||||
"register-confirm-input": "confirm",
|
||||
};
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const [form, setForm] = useState({
|
||||
firstname: "",
|
||||
lastname: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirm: "",
|
||||
role: "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
axiosInstance
|
||||
@@ -49,124 +53,129 @@ const Register = () => {
|
||||
});
|
||||
}, [form, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
const { error } = registerValidation.validate(form, {
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const adminForm = { ...form, role: "admin" };
|
||||
const { error } = credentials.validate(adminForm, {
|
||||
abortEarly: false,
|
||||
context: { password: form.password },
|
||||
});
|
||||
|
||||
if (error) {
|
||||
// Creates an error object in the format { field: message }
|
||||
const validationErrors = error.details.reduce((acc, err) => {
|
||||
return { ...acc, [err.path[0]]: err.message };
|
||||
}, {});
|
||||
setErrors(validationErrors);
|
||||
// validation errors
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({
|
||||
variant: "info",
|
||||
body:
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.",
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
setErrors({});
|
||||
}
|
||||
}, [form]);
|
||||
|
||||
const handleInput = (e) => {
|
||||
const newForm = { ...form, [idMap[e.target.id]]: e.target.value };
|
||||
setForm(newForm);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const adminForm = { ...form, role: "admin" };
|
||||
await registerValidation.validateAsync(adminForm, { abortEarly: false });
|
||||
delete adminForm.confirm;
|
||||
const action = await dispatch(register(adminForm));
|
||||
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
if (action.payload.success) {
|
||||
const token = action.payload.data;
|
||||
localStorage.setItem("token", token);
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
if (action.meta.requestStatus === "rejected") {
|
||||
const error = new Error("Request rejected");
|
||||
error.response = action.payload;
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === "ValidationError") {
|
||||
// validation errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body:
|
||||
error && error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.",
|
||||
hasIcon: false,
|
||||
});
|
||||
} else if (error.response) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: error.response.msg,
|
||||
body: "Welcome! Your account was created successfully.",
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: "Unknown error.",
|
||||
hasIcon: false,
|
||||
});
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: action.payload.msg,
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: "Unknown error.",
|
||||
hasIcon: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<BackgroundPattern></BackgroundPattern>
|
||||
<form className="register-form" onSubmit={handleSubmit}>
|
||||
<div className="register-form-header">
|
||||
<img
|
||||
className="register-form-header-logo"
|
||||
src={Logomark}
|
||||
alt="Logomark"
|
||||
<form className="register-form" onSubmit={handleSubmit} noValidate>
|
||||
<Stack gap={theme.gap.large} direction="column">
|
||||
<Logomark alt="BlueWave Uptime Icon" />
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Sign up with Google"
|
||||
sx={{ fontWeight: 600, mt: theme.gap.xxl }}
|
||||
img={<img className="google-enter" src={Google} alt="Google" />}
|
||||
/>
|
||||
<div className="register-form-v-spacing-large" />
|
||||
<div className="register-form-heading">
|
||||
Create Uptime Manager admin account
|
||||
</div>
|
||||
<div className="register-form-v-spacing-large"></div>
|
||||
</div>
|
||||
<div className="register-form-v-spacing-40px" />
|
||||
<div className="register-form-inputs">
|
||||
<Divider>
|
||||
<Typography>or</Typography>
|
||||
</Divider>
|
||||
<Field
|
||||
id="register-firstname-input"
|
||||
label="Name"
|
||||
isRequired={true}
|
||||
placeholder="Talha"
|
||||
placeholder="Daniel"
|
||||
autoComplete="given-name"
|
||||
onChange={handleInput}
|
||||
value={form.firstname}
|
||||
onChange={handleChange}
|
||||
error={errors.firstname}
|
||||
/>
|
||||
<div className="login-form-v2-spacing" />
|
||||
<Field
|
||||
id="register-lastname-input"
|
||||
label="Surname"
|
||||
isRequired={true}
|
||||
placeholder="Bolat"
|
||||
placeholder="Cojocea"
|
||||
autoComplete="family-name"
|
||||
onChange={handleInput}
|
||||
value={form.lastname}
|
||||
onChange={handleChange}
|
||||
error={errors.lastname}
|
||||
/>
|
||||
<div className="login-form-v2-spacing" />
|
||||
<Field
|
||||
type="email"
|
||||
id="register-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="name.surname@companyname.com"
|
||||
placeholder="daniel.cojocea@domain.com"
|
||||
autoComplete="email"
|
||||
onChange={handleInput}
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
/>
|
||||
<div className="login-form-v2-spacing" />
|
||||
<Field
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
@@ -174,11 +183,10 @@ const Register = () => {
|
||||
isRequired={true}
|
||||
placeholder="Create a password"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
onChange={handleInput}
|
||||
/>
|
||||
<div className="login-form-v2-spacing" />
|
||||
{/* TODO - hook up to form state and run checks */}
|
||||
<Field
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
@@ -186,35 +194,59 @@ const Register = () => {
|
||||
isRequired={true}
|
||||
placeholder="Confirm your password"
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors.confirm}
|
||||
onChange={handleInput}
|
||||
/>
|
||||
</div>
|
||||
<div className="login-form-v2-spacing" />
|
||||
<div className="register-form-checks">
|
||||
<Check text="Must be at least 8 characters" />
|
||||
<div className="register-form-v-spacing-small"></div>
|
||||
<Check text="Must contain one special character" />
|
||||
</div>
|
||||
<div className="login-form-v2-spacing" />
|
||||
<div className="register-form-actions">
|
||||
<Stack gap={theme.gap.small}>
|
||||
<Check
|
||||
text="Must be at least 8 characters long"
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: form.password.length < 8
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text="Must contain one special character and a number"
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[!@#$%^&*(),.?":{}|])(?=.*\d).+$/.test(
|
||||
form.password
|
||||
)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text="Must contain at least one upper and lower character"
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[A-Z])(?=.*[a-z]).+$/.test(form.password)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Button
|
||||
type="submit"
|
||||
level="primary"
|
||||
label="Get started"
|
||||
sx={{ width: "100%" }}
|
||||
sx={{ marginBottom: theme.gap.large }}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
/>
|
||||
<div className="login-form-v-spacing" />
|
||||
<Button
|
||||
disabled={true}
|
||||
level="secondary"
|
||||
label="Sign up with Google"
|
||||
sx={{ width: "100%", color: "#344054", fontWeight: "700" }}
|
||||
img={<img className="google-enter" src={Google} alt="Google" />}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</form>
|
||||
<div className="register-bottom-spacing"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,63 +1,36 @@
|
||||
.set-new-password-page {
|
||||
width: var(--env-var-width-1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: var(--env-var-height-1);
|
||||
height: var(--env-var-height-1);
|
||||
}
|
||||
.set-new-password-page button:not(.MuiIconButton-root) {
|
||||
height: 34px;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
line-height: 0;
|
||||
}
|
||||
.set-new-password-page h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
color: var(--env-var-color-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
.set-new-password-page p.MuiTypography-root,
|
||||
.set-new-password-page button {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.set-new-password-page p.MuiTypography-root {
|
||||
color: var(--env-var-color-2);
|
||||
}
|
||||
.set-new-password-page button:not(.MuiIconButton-root) svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.set-new-password-page
|
||||
.MuiFormControl-root:has(#register-password-input)
|
||||
+ span.MuiTypography-root {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.set-new-password-form {
|
||||
margin-top: 65px;
|
||||
width: var(--env-var-width-2);
|
||||
margin: 90px auto;
|
||||
}
|
||||
|
||||
.set-new-password-form-header {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.set-new-password-form-gap-large {
|
||||
height: var(--env-var-spacing-3);
|
||||
}
|
||||
|
||||
.set-new-password-form-gap-medium {
|
||||
height: var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
.set-new-password-form-gap-small {
|
||||
height: var(--env-var-spacing-1);
|
||||
}
|
||||
|
||||
.set-new-password-form-heading {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
color: var(--env-var-color-1);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.set-new-password-form-subheading {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.set-new-password-back-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.set-new-password-back-button-img {
|
||||
width: var(--env-var-img-width-1);
|
||||
height: var(--env-var-img-width-1);
|
||||
margin-right: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.set-new-password-back-button-text {
|
||||
height: var(--env-var-img-width-1);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-2);
|
||||
font-weight: 600;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -1,139 +1,207 @@
|
||||
import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPattern";
|
||||
import "./index.css";
|
||||
import LockIcon from "../../assets/Images/lock-icon.png";
|
||||
import LockIcon from "../../assets/icons/lock-button-icon.svg?react";
|
||||
import Check from "../../Components/Check/Check";
|
||||
import Button from "../../Components/Button";
|
||||
import LeftArrow from "../../assets/Images/arrow-left.png";
|
||||
import ButtonSpinner from "../../Components/ButtonSpinner";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import { newPasswordValidation } from "../../Validation/validation";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import { useState } from "react";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setNewPassword } from "../../Features/Auth/authSlice";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Button from "../../Components/Button";
|
||||
|
||||
const SetNewPassword = () => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
const [form, setForm] = useState({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
password: "",
|
||||
confirm: "",
|
||||
});
|
||||
|
||||
const idMap = {
|
||||
"register-password-input": "password",
|
||||
"confirm-password-input": "confirm",
|
||||
};
|
||||
|
||||
const handleInput = (e) => {
|
||||
const fieldName = idMap[e.target.id];
|
||||
setForm({ ...form, [fieldName]: e.target.value });
|
||||
console.log(errors);
|
||||
};
|
||||
const { isLoading } = useSelector((state) => state.auth);
|
||||
const { token } = useParams();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// TODO show loading spinner
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await axiosInstance.post("/auth/recovery/validate", {
|
||||
recoveryToken: token,
|
||||
});
|
||||
await axiosInstance.post("/auth/recovery/reset", {
|
||||
...form,
|
||||
recoveryToken: token,
|
||||
});
|
||||
navigate("/new-password-confirmed");
|
||||
} catch (error) {
|
||||
// TODO display error (Toast?)
|
||||
alert(error.response.data.msg);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
useEffect(() => {
|
||||
const { error } = newPasswordValidation.validate(form, {
|
||||
const passwordForm = { ...form };
|
||||
const { error } = credentials.validate(passwordForm, {
|
||||
abortEarly: false,
|
||||
context: { password: form.password },
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const validationErrors = error.details.reduce((acc, err) => {
|
||||
return { ...acc, [err.path[0]]: err.message };
|
||||
}, {});
|
||||
setErrors(validationErrors);
|
||||
// validation errors
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({
|
||||
variant: "info",
|
||||
body:
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.",
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
setErrors({});
|
||||
delete passwordForm.confirm;
|
||||
const action = await dispatch(
|
||||
setNewPassword({ token: token, form: passwordForm })
|
||||
);
|
||||
if (action.payload.success) {
|
||||
navigate("/new-password-confirmed");
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: "Your password was reset successfully.",
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: action.payload.msg,
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: "Unknown error.",
|
||||
hasIcon: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [form]);
|
||||
};
|
||||
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
const { token } = useParams();
|
||||
return (
|
||||
<div className="set-new-password-page">
|
||||
<BackgroundPattern />
|
||||
<div className="set-new-password-form">
|
||||
<div className="set-new-password-form-header">
|
||||
<img
|
||||
className="set-new-password-form-header-logo"
|
||||
src={LockIcon}
|
||||
alt="LockIcon"
|
||||
/>
|
||||
<div className="set-new-password-form-gap-medium" />
|
||||
<div className="set-new-password-form-heading">Set new password</div>
|
||||
<div className="set-new-password-form-gap-small" />
|
||||
<div className="set-new-password-form-subheading">
|
||||
<form className="set-new-password-form" onSubmit={handleSubmit}>
|
||||
<Stack direction="column" alignItems="center" gap={theme.gap.small}>
|
||||
<LockIcon alt="lock icon" style={{ fill: "white" }} />
|
||||
<Typography component="h1" sx={{ mt: theme.gap.ml }}>
|
||||
Set new password
|
||||
</Typography>
|
||||
<Typography sx={{ textAlign: "center" }}>
|
||||
Your new password must be different to previously used passwords.
|
||||
</div>
|
||||
</div>
|
||||
<div className="set-new-password-form-gap-large"></div>
|
||||
<div className="set-new-password-form-content">
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.large} sx={{ mt: `calc(${theme.gap.ml}*2)` }}>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
onChange={handleInput}
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
/>
|
||||
<div className="set-new-password-form-gap-medium"></div>
|
||||
<Field
|
||||
type="password"
|
||||
id="confirm-password-input"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
onChange={handleInput}
|
||||
value={form.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors.confirm}
|
||||
/>
|
||||
<div className="set-new-password-form-gap-medium"></div>
|
||||
<div className="set-new-password-form-checks">
|
||||
<Check text="Must be at least 8 characters" />
|
||||
<div className="set-new-password-form-gap-small"></div>
|
||||
<Check text="Must contain one special character" />
|
||||
</div>
|
||||
<div className="set-new-password-form-gap-medium"></div>
|
||||
<Button
|
||||
disabled={Object.keys(errors).length !== 0 || isLoading === true}
|
||||
<Stack gap={theme.gap.small}>
|
||||
<Check
|
||||
text="Must be at least 8 characters long"
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: form.password.length < 8
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text="Must contain one special character and a number"
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[!@#$%^&*(),.?":{}|])(?=.*\d).+$/.test(
|
||||
form.password
|
||||
)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text="Must contain at least one upper and lower character"
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[A-Z])(?=.*[a-z]).+$/.test(form.password)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<ButtonSpinner
|
||||
disabled={Object.keys(errors).length !== 0}
|
||||
isLoading={isLoading}
|
||||
onClick={handleSubmit}
|
||||
level="primary"
|
||||
label="Reset password"
|
||||
sx={{
|
||||
width: "100%",
|
||||
fontSize: "13px",
|
||||
fontWeight: "200",
|
||||
height: "35px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="set-new-password-form-gap-medium"></div>
|
||||
<div className="set-new-password-back-button">
|
||||
<img
|
||||
className="set-new-password-back-button-img"
|
||||
src={LeftArrow}
|
||||
alt="LeftArrow"
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back to log in"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
sx={{ alignSelf: "center", width: "fit-content" }}
|
||||
onClick={() => navigate("/login")}
|
||||
/>
|
||||
<div className="set-new-password-back-button-text">
|
||||
Back to log in
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -97,6 +97,7 @@ const theme = createTheme({
|
||||
ml: "16px",
|
||||
large: "24px",
|
||||
xl: "40px",
|
||||
xxl: "60px",
|
||||
},
|
||||
alert: {
|
||||
info: {
|
||||
|
||||
@@ -1,138 +1,76 @@
|
||||
import joi from "joi";
|
||||
|
||||
const registerValidation = joi.object({
|
||||
firstname: joi.string().required().messages({
|
||||
"string.empty": "First name is required",
|
||||
}),
|
||||
|
||||
lastname: joi.string().required().messages({
|
||||
"string.empty": "Last name is required",
|
||||
}),
|
||||
|
||||
email: joi
|
||||
.string()
|
||||
.email({ tlds: { allow: false } })
|
||||
.required()
|
||||
.messages({
|
||||
"string.email": "Email must be a valid email",
|
||||
"string.empty": "Email is required",
|
||||
}),
|
||||
|
||||
password: joi.string().min(8).required().messages({
|
||||
"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({
|
||||
email: joi
|
||||
.string()
|
||||
.email({ tlds: { allow: false } })
|
||||
.required()
|
||||
.messages({
|
||||
"string.email": "Email must be a valid email",
|
||||
"string.empty": "Email is required",
|
||||
}),
|
||||
|
||||
password: joi.string().min(8).required().messages({
|
||||
"string.min": "Password must be at least 8 characters",
|
||||
"string.empty": "Password is required",
|
||||
}),
|
||||
});
|
||||
|
||||
const recoveryValidation = joi.object({
|
||||
email: joi
|
||||
.string()
|
||||
.email({ tlds: { allow: false } })
|
||||
.required()
|
||||
.messages({
|
||||
"string.email": "Email must be a valid email",
|
||||
"string.empty": "Email is required",
|
||||
}),
|
||||
});
|
||||
|
||||
const newPasswordValidation = joi.object({
|
||||
password: joi.string().min(8).required().messages({
|
||||
"string.min": "Password must be at least 8 characters",
|
||||
"string.empty": "Password is required",
|
||||
}),
|
||||
confirm: joi.string().valid(joi.ref("password")).min(8).required().messages({
|
||||
"string.min": "Password must be at least 8 characters",
|
||||
"string.empty": "Password is required",
|
||||
"any.only": "Passwords do not match",
|
||||
}),
|
||||
});
|
||||
|
||||
const editProfileValidation = joi.object({
|
||||
firstname: joi.string().trim().pattern(new RegExp("^[A-Za-z]+$")).messages({
|
||||
"string.empty": "*First name is required.",
|
||||
"string.pattern.base": "*First name must contain only letters.",
|
||||
}),
|
||||
lastname: joi.string().trim().pattern(new RegExp("^[A-Za-z]+$")).messages({
|
||||
"string.empty": "*Last name is required.",
|
||||
"string.pattern.base": "*Last name must contain only letters.",
|
||||
}),
|
||||
email: joi
|
||||
.string()
|
||||
.trim()
|
||||
.email({ tlds: { allow: false } })
|
||||
.messages({
|
||||
"string.empty": "*Email is required.",
|
||||
"string.email": "*Invalid email address.",
|
||||
}),
|
||||
});
|
||||
const nameSchema = joi
|
||||
.string()
|
||||
.max(50)
|
||||
.trim()
|
||||
.pattern(new RegExp("^[A-Za-z]+$"))
|
||||
.messages({
|
||||
"string.empty": "Name is required",
|
||||
"string.max": "Name must be less than 50 characters long",
|
||||
"string.pattern.base": "Name must contain only letters",
|
||||
});
|
||||
|
||||
const passwordSchema = joi
|
||||
.string()
|
||||
.trim()
|
||||
.min(8)
|
||||
.messages({
|
||||
"string.empty": "*Password is required.",
|
||||
"string.min": "*Password must be at least 8 characters long.",
|
||||
"string.empty": "Password is required",
|
||||
"string.min": "Password must be at least 8 characters long",
|
||||
})
|
||||
.custom((value, helpers) => {
|
||||
if (!/[A-Z]/.test(value)) {
|
||||
return helpers.message(
|
||||
"*Password must contain at least one uppercase letter."
|
||||
"Password must contain at least one uppercase letter"
|
||||
);
|
||||
}
|
||||
if (!/[a-z]/.test(value)) {
|
||||
return helpers.message(
|
||||
"*Password must contain at least one lowercase letter."
|
||||
"Password must contain at least one lowercase letter"
|
||||
);
|
||||
}
|
||||
if (!/\d/.test(value)) {
|
||||
return helpers.message("*Password must contain at least one number.");
|
||||
return helpers.message("Password must contain at least one number");
|
||||
}
|
||||
if (!/[!@#$%^&*]/.test(value)) {
|
||||
return helpers.message(
|
||||
"*Password must contain at least one special character."
|
||||
"Password must contain at least one special character"
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
const editPasswordValidation = joi.object({
|
||||
// TBD - validation for current password ?
|
||||
const credentials = joi.object({
|
||||
firstname: nameSchema,
|
||||
lastname: nameSchema,
|
||||
email: joi
|
||||
.string()
|
||||
.trim()
|
||||
.email({ tlds: { allow: false } })
|
||||
.messages({
|
||||
"string.empty": "Email is required",
|
||||
"string.email": "Must be a valid email address",
|
||||
}),
|
||||
password: passwordSchema,
|
||||
newPassword: passwordSchema,
|
||||
confirm: joi
|
||||
.string()
|
||||
.trim()
|
||||
.messages({
|
||||
"string.empty": "*Password confirmation is required.",
|
||||
"string.empty": "Password confirmation is required",
|
||||
})
|
||||
.custom((value, helpers) => {
|
||||
const { newPassword } = helpers.prefs.context;
|
||||
if (value !== newPassword) {
|
||||
return helpers.message("*Passwords do not match.");
|
||||
const { password } = helpers.prefs.context;
|
||||
if (value !== password) {
|
||||
return helpers.message("Passwords do not match");
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
role: joi.string().messages({
|
||||
"string.empty": "Role is required",
|
||||
}),
|
||||
});
|
||||
|
||||
const createMonitorValidation = joi.object({
|
||||
@@ -168,13 +106,4 @@ const imageValidation = joi.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export {
|
||||
imageValidation,
|
||||
createMonitorValidation,
|
||||
registerValidation,
|
||||
loginValidation,
|
||||
recoveryValidation,
|
||||
newPasswordValidation,
|
||||
editPasswordValidation,
|
||||
editProfileValidation,
|
||||
};
|
||||
export { credentials, imageValidation, createMonitorValidation };
|
||||
|
||||
|
Before Width: | Height: | Size: 571 B |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 310 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 10C0 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20C4.47715 20 0 15.5228 0 10Z" fill="#D0D5DD"/>
|
||||
<path d="M6.25 10L8.75 12.5L13.75 7.5" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
@@ -0,0 +1,18 @@
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_137_19027)">
|
||||
<path d="M2.5 13C2.5 6.64873 7.64873 1.5 14 1.5H46C52.3513 1.5 57.5 6.64873 57.5 13V45C57.5 51.3513 52.3513 56.5 46 56.5H14C7.64873 56.5 2.5 51.3513 2.5 45V13Z" stroke="#EAECF0" shape-rendering="crispEdges"/>
|
||||
<path d="M24.7499 28.9999L28.2499 32.4999L35.2499 25.4999M41.6666 28.9999C41.6666 35.4432 36.4432 40.6666 29.9999 40.6666C23.5566 40.6666 18.3333 35.4432 18.3333 28.9999C18.3333 22.5566 23.5566 17.3333 29.9999 17.3333C36.4432 17.3333 41.6666 22.5566 41.6666 28.9999Z" stroke="#344054" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_137_19027" x="0" y="0" width="60" height="60" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_137_19027"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_137_19027" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_137_18989)">
|
||||
<path d="M2.5 13C2.5 6.64873 7.64873 1.5 14 1.5H46C52.3513 1.5 57.5 6.64873 57.5 13V45C57.5 51.3513 52.3513 56.5 46 56.5H14C7.64873 56.5 2.5 51.3513 2.5 45V13Z" stroke="#EAECF0" shape-rendering="crispEdges"/>
|
||||
<path d="M18.3333 23.1667L27.859 29.8347C28.6304 30.3746 29.016 30.6446 29.4356 30.7492C29.8061 30.8416 30.1937 30.8416 30.5643 30.7492C30.9838 30.6446 31.3695 30.3746 32.1408 29.8347L41.6666 23.1667M23.9333 38.3333H36.0666C38.0268 38.3333 39.0069 38.3333 39.7556 37.9519C40.4141 37.6163 40.9496 37.0809 41.2851 36.4223C41.6666 35.6736 41.6666 34.6935 41.6666 32.7333V25.2667C41.6666 23.3065 41.6666 22.3264 41.2851 21.5777C40.9496 20.9191 40.4141 20.3837 39.7556 20.0481C39.0069 19.6667 38.0268 19.6667 36.0666 19.6667H23.9333C21.9731 19.6667 20.993 19.6667 20.2443 20.0481C19.5857 20.3837 19.0503 20.9191 18.7147 21.5777C18.3333 22.3264 18.3333 23.3065 18.3333 25.2667V32.7333C18.3333 34.6935 18.3333 35.6736 18.7147 36.4223C19.0503 37.0809 19.5857 37.6163 20.2443 37.9519C20.993 38.3333 21.9731 38.3333 23.9333 38.3333Z" stroke="#344054" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_137_18989" x="0" y="0" width="60" height="60" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_137_18989"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_137_18989" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_137_18974)">
|
||||
<path d="M2.5 13C2.5 6.64873 7.64873 1.5 14 1.5H46C52.3513 1.5 57.5 6.64873 57.5 13V45C57.5 51.3513 52.3513 56.5 46 56.5H14C7.64873 56.5 2.5 51.3513 2.5 45V13Z" stroke="#EAECF0" shape-rendering="crispEdges"/>
|
||||
<path d="M35.8333 25.4999C35.8333 24.9028 35.6055 24.3057 35.1499 23.8501C34.6943 23.3945 34.0972 23.1667 33.5 23.1667M33.5 32.5C37.366 32.5 40.5 29.366 40.5 25.5C40.5 21.634 37.366 18.5 33.5 18.5C29.634 18.5 26.5 21.634 26.5 25.5C26.5 25.8193 26.5214 26.1336 26.5628 26.4415C26.6309 26.948 26.6649 27.2013 26.642 27.3615C26.6181 27.5284 26.5877 27.6184 26.5055 27.7655C26.4265 27.9068 26.2873 28.046 26.009 28.3243L20.0467 34.2866C19.845 34.4884 19.7441 34.5893 19.6719 34.707C19.608 34.8114 19.5608 34.9252 19.5322 35.0442C19.5 35.1785 19.5 35.3212 19.5 35.6065V37.6333C19.5 38.2867 19.5 38.6134 19.6272 38.863C19.739 39.0825 19.9175 39.261 20.137 39.3728C20.3866 39.5 20.7133 39.5 21.3667 39.5H24.1667V37.1667H26.5V34.8333H28.8333L30.6757 32.991C30.954 32.7127 31.0932 32.5735 31.2345 32.4945C31.3816 32.4123 31.4716 32.3819 31.6385 32.358C31.7987 32.3351 32.052 32.3691 32.5585 32.4372C32.8664 32.4786 33.1807 32.5 33.5 32.5Z" stroke="#344054" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_137_18974" x="0" y="0" width="60" height="60" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_137_18974"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_137_18974" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_137_19004)">
|
||||
<path d="M2.5 13C2.5 6.64873 7.64873 1.5 14 1.5H46C52.3513 1.5 57.5 6.64873 57.5 13V45C57.5 51.3513 52.3513 56.5 46 56.5H14C7.64873 56.5 2.5 51.3513 2.5 45V13Z" stroke="#EAECF0" shape-rendering="crispEdges"/>
|
||||
<path d="M35.8334 26.6667V24.3333C35.8334 21.1117 33.2217 18.5 30.0001 18.5C26.7784 18.5 24.1667 21.1117 24.1667 24.3333V26.6667M30.0001 31.9167V34.25M26.2667 39.5H33.7334C35.6936 39.5 36.6737 39.5 37.4224 39.1185C38.081 38.783 38.6164 38.2475 38.9519 37.589C39.3334 36.8403 39.3334 35.8602 39.3334 33.9V32.2667C39.3334 30.3065 39.3334 29.3264 38.9519 28.5777C38.6164 27.9191 38.081 27.3837 37.4224 27.0481C36.6737 26.6667 35.6936 26.6667 33.7334 26.6667H26.2667C24.3066 26.6667 23.3265 26.6667 22.5778 27.0481C21.9192 27.3837 21.3838 27.9191 21.0482 28.5777C20.6667 29.3264 20.6667 30.3065 20.6667 32.2667V33.9C20.6667 35.8602 20.6667 36.8403 21.0482 37.589C21.3838 38.2475 21.9192 38.783 22.5778 39.1185C23.3265 39.5 24.3066 39.5 26.2667 39.5Z" stroke="#344054" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_137_19004" x="0" y="0" width="60" height="60" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_137_19004"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_137_19004" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -47,7 +47,7 @@
|
||||
--env-var-color-27: #fecf60;
|
||||
--lighter-env-var-color-27: rgba(254, 207, 96, 0.1);
|
||||
--env-var-color-28: #f79009;
|
||||
--env-var-color-29: #D0D5DD;
|
||||
--env-var-color-29: #d0d5dd;
|
||||
|
||||
--env-var-radius-1: 4px;
|
||||
--env-var-radius-2: 8px;
|
||||
@@ -128,11 +128,14 @@ button:focus-visible {
|
||||
.Toastify__toast-container {
|
||||
width: auto;
|
||||
}
|
||||
.Toastify__toast-body .alert{
|
||||
min-width: 150px;
|
||||
}
|
||||
.Toastify [class^="Toastify__toast"] {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.Toastify__toast{
|
||||
.Toastify__toast {
|
||||
min-height: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ const resetPasswordController = async (req, res, next) => {
|
||||
try {
|
||||
await newPasswordValidation.validateAsync(req.body);
|
||||
const user = await req.db.resetPassword(req, res);
|
||||
const token = issueToken(user);
|
||||
const token = issueToken(user._doc);
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.AUTH_RESET_PASSWORD,
|
||||
|
||||