Wire up working password reset UI for forgot-password flow
Unit Tests / test (pull_request) Successful in 12s
Unit Tests / test (pull_request) Successful in 12s
Fix reCAPTCHA + API path bugs, await set-password calls, and align reset confirmation / set-password pages with Sign In glass styling.
This commit is contained in:
@@ -1,187 +1,263 @@
|
||||
import { Form, Formik } from 'formik';
|
||||
import React from 'react';
|
||||
import CustomPasswordField from '../../components/CustomPasswordField/CustomPasswordField';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { axiosInstance } from '../../../axiosApi';
|
||||
import { Form, Formik, Field } from 'formik';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { cleanAxiosInstance } from '../../../axiosApi';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import CustomToastMessage from '../../components/CustomToastMessage/CustomeToastMessage';
|
||||
|
||||
import ParticleBackground from '../ParticleBackground/ParticleBackground';
|
||||
import styled from 'styled-components';
|
||||
import * as Yup from 'yup';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import PageWrapperLayout from '../PageWrapperLayout/PageWrapperLayout';
|
||||
import MDBox from '../../ui-kit/components/MDBox';
|
||||
import { Card, CardContent } from '@mui/material';
|
||||
import MDTypography from '../../ui-kit/components/MDTypography';
|
||||
import MDButton from '../../ui-kit/components/MDButton';
|
||||
import ParticleBackground from '../../components/ParticleBackground/ParticleBackground';
|
||||
|
||||
const PageContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
font-family: 'Inter', sans-serif;
|
||||
`;
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 5;
|
||||
padding: 2rem;
|
||||
`;
|
||||
|
||||
const GlassCard = styled.div`
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 1rem;
|
||||
padding: 3rem;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const CardTitle = styled.h2`
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
&:focus {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(100, 149, 237, 0.5);
|
||||
box-shadow: 0 0 10px rgba(100, 149, 237, 0.2);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled.button`
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(118, 75, 162, 0.4);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const HintList = styled.ul`
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 0.5rem;
|
||||
width: 100%;
|
||||
font-size: 0.85rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
`;
|
||||
|
||||
const HintItem = styled.li<{ $ok: boolean }>`
|
||||
margin-bottom: 0.35rem;
|
||||
color: ${(props) => (props.$ok ? '#7dcea0' : 'rgba(255, 255, 255, 0.55)')};
|
||||
`;
|
||||
|
||||
const ErrorMessage = styled.div`
|
||||
color: #ff6b6b;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export type PasswordResetValues = {
|
||||
password1: string;
|
||||
password2: string;
|
||||
password1: string;
|
||||
password2: string;
|
||||
};
|
||||
|
||||
const initialValues = { password1: '', password2: '' }
|
||||
const validationSchema = Yup.object().shape({
|
||||
password1: Yup.string().min(6, "Passwords have to be at least 6 digits").required(),
|
||||
password2: Yup.string().min(6, "Passwords have to be at least 6 digits").required().oneOf([Yup.ref('password1')], "Passwords must match"),
|
||||
})
|
||||
|
||||
const contains_number = (item: string): boolean => {
|
||||
const numbers = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
|
||||
const hasNumber = numbers.some((character) => item.includes(character));
|
||||
if (hasNumber) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const contains_special_character = (item: string): boolean => {
|
||||
const specialCharacters = ['!', '@', '#', '$', ',%', '^', '&', '*', '(', ')', '-', '_', '=', '+', '/', '*', '\\', '|', '`', '~', '<', '>', '.', '?'];
|
||||
const hasSpecialChacater = specialCharacters.some((character) => item.includes(character));
|
||||
if (hasSpecialChacater) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
password1: Yup.string()
|
||||
.min(8, 'Passwords must be at least 8 characters')
|
||||
.matches(/[0-9]/, 'Must include a number')
|
||||
.matches(/[^A-Za-z0-9]/, 'Must include a special character')
|
||||
.required('Required'),
|
||||
password2: Yup.string()
|
||||
.oneOf([Yup.ref('password1')], 'Passwords must match')
|
||||
.required('Required'),
|
||||
});
|
||||
|
||||
const SetPassword = (): JSX.Element => {
|
||||
const navigate = useNavigate();
|
||||
// see if the user is allowed to come here first
|
||||
const [queryParameters] = useSearchParams()
|
||||
console.log(queryParameters.get("slug"))
|
||||
const slug = queryParameters.get("slug");
|
||||
const navigate = useNavigate();
|
||||
const [queryParameters] = useSearchParams();
|
||||
const slug = queryParameters.get('slug');
|
||||
const [linkValid, setLinkValid] = useState(false);
|
||||
const [checking, setChecking] = useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
try {
|
||||
// make sure it comes back as 200 for a good request. Else go to the homepage
|
||||
axiosInstance.get(`user/set_password/${slug}`)
|
||||
} catch {
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
const handleSetPassword = ({ password1, password2 }: PasswordResetValues): void => {
|
||||
try {
|
||||
// verify
|
||||
if (password1 === password2) {
|
||||
axiosInstance.post(`user/set_password/${slug}/`, {
|
||||
'password': password1,
|
||||
});
|
||||
|
||||
navigate('/')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('catching the error');
|
||||
<CustomToastMessage message={error as string} />
|
||||
|
||||
(async () => {
|
||||
if (!slug) {
|
||||
navigate('/signin/');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await cleanAxiosInstance.get(`user/set_password/${slug}/`);
|
||||
if (!cancelled) {
|
||||
setLinkValid(true);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
navigate('/signin/');
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setChecking(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [slug, navigate]);
|
||||
|
||||
const handleSetPassword = async ({
|
||||
password1,
|
||||
}: PasswordResetValues): Promise<void> => {
|
||||
setErrorMessage('');
|
||||
try {
|
||||
await cleanAxiosInstance.post(`user/set_password/${slug}/`, {
|
||||
password: password1,
|
||||
});
|
||||
navigate('/signin/');
|
||||
} catch {
|
||||
setErrorMessage('Unable to set password. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
if (checking || !linkValid) {
|
||||
return (
|
||||
|
||||
<PageWrapperLayout>
|
||||
<MDBox sx={{
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
bgcolor: 'background.default',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<ParticleBackground />
|
||||
<MDBox sx={{ width: '100%', maxWidth: '400px', zIndex: 1, position: 'relative' }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<MDTypography variant="h4" textAlign="center">
|
||||
Set your password
|
||||
</MDTypography>
|
||||
</CardContent>
|
||||
<div className='card-body text-center'>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSetPassword}
|
||||
validateOnMount
|
||||
validationSchema={validationSchema}>
|
||||
{(formik) => (
|
||||
<Form>
|
||||
<div className='row'>
|
||||
<div className='col'>
|
||||
<CustomPasswordField
|
||||
label='Password'
|
||||
name="password1"
|
||||
changeHandler={(e) => formik.setFieldValue('password1', e.target.value)} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<div className='col'>
|
||||
<CustomPasswordField
|
||||
label='Confirm Password'
|
||||
name="password2"
|
||||
changeHandler={(e) => formik.setFieldValue('password2', e.target.value)} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Stack alignItems="center" direction="row" gap={2}>
|
||||
{formik.values.password1 === formik.values.password2 ? <CheckCircleIcon fontSize='small' color='success' /> : <ErrorIcon fontSize='small' color='warning' />}
|
||||
|
||||
<Typography variant="body1">Passwords Match</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
<div>
|
||||
<Stack alignItems="center" direction="row" gap={2}>
|
||||
{formik.values.password1.length > 5 ? <CheckCircleIcon fontSize='small' color='success' /> : <ErrorIcon fontSize='small' color='warning' />}
|
||||
<Typography variant="body1">At least 6 characters</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
<div>
|
||||
<Stack alignItems="center" direction="row" gap={2}>
|
||||
{contains_special_character(formik.values.password1) ? <CheckCircleIcon fontSize='small' color='success' /> : <ErrorIcon fontSize='small' color='warning' />}
|
||||
<Typography variant="body1">At least one special character</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
<div>
|
||||
<Stack alignItems="center" direction="row" gap={2}>
|
||||
|
||||
{contains_number(formik.values.password1) ? <CheckCircleIcon fontSize='small' color='success' /> : <ErrorIcon fontSize='small' color='warning' />}
|
||||
<Typography variant="body1">At least one number</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<div className='col'>
|
||||
<MDButton
|
||||
type={'submit'}
|
||||
fullWidth
|
||||
disabled={!formik.isValid || formik.isSubmitting ||
|
||||
!contains_special_character(formik.values.password1)
|
||||
|| !contains_number(formik.values.password1)
|
||||
}
|
||||
|
||||
// type={'submit'}
|
||||
// loading={formik.isSubmitting}
|
||||
// disabled={
|
||||
// !formik.isValid || !formik.dirty || formik.isSubmitting
|
||||
// }
|
||||
>
|
||||
Set password
|
||||
</MDButton>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Form>
|
||||
)}
|
||||
|
||||
</Formik>
|
||||
</div>
|
||||
</Card>
|
||||
</MDBox>
|
||||
</MDBox>
|
||||
</PageWrapperLayout>
|
||||
<PageContainer>
|
||||
<ParticleBackground />
|
||||
<ContentWrapper>
|
||||
<GlassCard>
|
||||
<CardTitle>Loading…</CardTitle>
|
||||
</GlassCard>
|
||||
</ContentWrapper>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ParticleBackground />
|
||||
<ContentWrapper>
|
||||
<GlassCard>
|
||||
<CardTitle>Set Your Password</CardTitle>
|
||||
{errorMessage && <ErrorMessage>{errorMessage}</ErrorMessage>}
|
||||
<Formik
|
||||
initialValues={{ password1: '', password2: '' }}
|
||||
onSubmit={handleSetPassword}
|
||||
validationSchema={validationSchema}
|
||||
validateOnMount
|
||||
>
|
||||
{(formik) => (
|
||||
<Form style={{ width: '100%' }}>
|
||||
<Field
|
||||
as={StyledInput}
|
||||
name="password1"
|
||||
placeholder="New Password"
|
||||
type="password"
|
||||
/>
|
||||
<Field
|
||||
as={StyledInput}
|
||||
name="password2"
|
||||
placeholder="Confirm Password"
|
||||
type="password"
|
||||
/>
|
||||
<HintList>
|
||||
<HintItem $ok={formik.values.password1.length >= 8}>
|
||||
At least 8 characters
|
||||
</HintItem>
|
||||
<HintItem $ok={/[0-9]/.test(formik.values.password1)}>
|
||||
At least one number
|
||||
</HintItem>
|
||||
<HintItem $ok={/[^A-Za-z0-9]/.test(formik.values.password1)}>
|
||||
At least one special character
|
||||
</HintItem>
|
||||
<HintItem
|
||||
$ok={
|
||||
formik.values.password1.length > 0 &&
|
||||
formik.values.password1 === formik.values.password2
|
||||
}
|
||||
>
|
||||
Passwords match
|
||||
</HintItem>
|
||||
</HintList>
|
||||
<StyledButton
|
||||
type="submit"
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Set Password
|
||||
</StyledButton>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</GlassCard>
|
||||
</ContentWrapper>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetPassword;
|
||||
export default SetPassword;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import PasswordReset from './PasswordReset';
|
||||
|
||||
const mockPost = jest.fn();
|
||||
const mockExecuteAsync = jest.fn();
|
||||
const mockReset = jest.fn();
|
||||
|
||||
jest.mock('../../components/ParticleBackground/ParticleBackground', () => () => null);
|
||||
|
||||
jest.mock('react-google-recaptcha', () => {
|
||||
const ReactLib = require('react');
|
||||
return ReactLib.forwardRef(
|
||||
(
|
||||
_props: unknown,
|
||||
ref: React.Ref<{ executeAsync: () => Promise<string>; reset: () => void }>
|
||||
) => {
|
||||
ReactLib.useImperativeHandle(ref, () => ({
|
||||
executeAsync: mockExecuteAsync,
|
||||
reset: mockReset,
|
||||
}));
|
||||
return <div data-testid="recaptcha" />;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('../../../axiosApi', () => ({
|
||||
cleanAxiosInstance: {
|
||||
post: (...args: unknown[]) => mockPost(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
const renderPasswordReset = () =>
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/password_reset/']}>
|
||||
<Routes>
|
||||
<Route path="/password_reset/" element={<PasswordReset />} />
|
||||
<Route
|
||||
path="/password_reset_confirmation/"
|
||||
element={<div>Confirmation page</div>}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
describe('PasswordReset', () => {
|
||||
beforeEach(() => {
|
||||
mockPost.mockReset();
|
||||
mockExecuteAsync.mockReset();
|
||||
mockReset.mockReset();
|
||||
mockExecuteAsync.mockResolvedValue('captcha-token');
|
||||
mockPost.mockResolvedValue({ status: 200 });
|
||||
});
|
||||
|
||||
it('submits email with captcha token and navigates to confirmation', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPasswordReset();
|
||||
|
||||
await user.type(screen.getByPlaceholderText('Email Address'), 'user@example.com');
|
||||
await user.click(screen.getByRole('button', { name: 'Reset Password' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPost).toHaveBeenCalledWith('user/reset_password/', {
|
||||
email: 'user@example.com',
|
||||
recaptchaToken: 'captcha-token',
|
||||
});
|
||||
});
|
||||
expect(await screen.findByText('Confirmation page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error when request fails', async () => {
|
||||
mockPost.mockRejectedValue(new Error('network'));
|
||||
const user = userEvent.setup();
|
||||
renderPasswordReset();
|
||||
|
||||
await user.type(screen.getByPlaceholderText('Email Address'), 'user@example.com');
|
||||
await user.click(screen.getByRole('button', { name: 'Reset Password' }));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Unable to reset password. Please try again.')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText('Confirmation page')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Form, Formik, Field } from 'formik';
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { cleanAxiosInstance } from '../../../axiosApi';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ReCAPTCHA from 'react-google-recaptcha';
|
||||
import ParticleBackground from '../../components/ParticleBackground/ParticleBackground';
|
||||
import styled from 'styled-components';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
// Styled Components
|
||||
const PageContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
@@ -43,11 +43,19 @@ const GlassCard = styled.div`
|
||||
|
||||
const CardTitle = styled.h2`
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const CardSubtitle = styled.p`
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
@@ -111,134 +119,92 @@ const BackLink = styled.button`
|
||||
}
|
||||
`;
|
||||
|
||||
export type PasswordResetValues = {
|
||||
password1: string;
|
||||
password2: string;
|
||||
};
|
||||
const ErrorMessage = styled.div`
|
||||
color: #ff6b6b;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export type EmailPasswordResetValues = {
|
||||
email: string;
|
||||
}
|
||||
|
||||
|
||||
const PasswordReset = (): JSX.Element => {
|
||||
const navigate = useNavigate();
|
||||
const recaptchaRef = useRef<ReCAPTCHA>(null);
|
||||
|
||||
const handlePasswordResetEmail = ({ email }: EmailPasswordResetValues): void => {
|
||||
if (recaptchaRef.current) {
|
||||
const token = recaptchaRef.current.getValue();
|
||||
if (!token) { // This logic seems inverted in original code? "if (!token)" usually means no token.
|
||||
// But original code had "if (!token)" then try... catch.
|
||||
// Wait, if !token, it means user didn't solve captcha?
|
||||
// Or maybe invisible captcha returns token immediately?
|
||||
// Let's assume the original logic was trying to say "if token is present" but maybe had a bug or I'm misreading.
|
||||
// Actually, looking at original code:
|
||||
// if (recaptchaRef.current) {
|
||||
// const token = recaptchaRef.current.getValue();
|
||||
// if (!token) { ... try { post ... } }
|
||||
// }
|
||||
// This implies it posts ONLY if token is falsy? That's weird for a captcha.
|
||||
// Invisible captcha might need execution.
|
||||
// Let's stick to the logic but maybe fix it if it looks obviously wrong.
|
||||
// Standard reCAPTCHA flow: execute -> get token -> send token.
|
||||
// If size="invisible", we might need to execute it manually or it executes on submit.
|
||||
// I'll keep the structure but assume we want to send the token if we have it.
|
||||
// Actually, let's look at the original code again.
|
||||
// if (!token) { ... }
|
||||
// This is very strange. It sends the request if there is NO token?
|
||||
// Maybe it was a bypass for dev?
|
||||
// I will assume the user wants the captcha to work.
|
||||
// I'll try to get the token, if it exists, send it.
|
||||
|
||||
try {
|
||||
cleanAxiosInstance.post('user/reset_password',
|
||||
{
|
||||
'email': email,
|
||||
'recaptchaToken': token || "dummy_token_if_logic_was_inverted" // preserving original weirdness slightly but making it safer?
|
||||
}
|
||||
);
|
||||
// navigate to another page now
|
||||
navigate('/password_reset_confirmation')
|
||||
} catch (error) {
|
||||
console.log('error')
|
||||
}
|
||||
|
||||
} else {
|
||||
// If token exists, we should probably send it too?
|
||||
// The original code ONLY sent if !token.
|
||||
// I will correct this to send if token exists OR if the original intent was to just send it.
|
||||
// Let's just send the request.
|
||||
try {
|
||||
cleanAxiosInstance.post('user/reset_password',
|
||||
{
|
||||
'email': email,
|
||||
'recaptchaToken': token
|
||||
}
|
||||
);
|
||||
navigate('/password_reset_confirmation')
|
||||
} catch (error) {
|
||||
console.log('error')
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Fallback if ref is null
|
||||
try {
|
||||
cleanAxiosInstance.post('user/reset_password',
|
||||
{
|
||||
'email': email,
|
||||
'recaptchaToken': ''
|
||||
}
|
||||
);
|
||||
navigate('/password_reset_confirmation')
|
||||
} catch (error) {
|
||||
console.log('error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ParticleBackground />
|
||||
<ContentWrapper>
|
||||
<GlassCard>
|
||||
<CardTitle>Reset Password</CardTitle>
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: '',
|
||||
}}
|
||||
onSubmit={handlePasswordResetEmail}
|
||||
>
|
||||
{(formik) => (
|
||||
<Form style={{ width: '100%' }}>
|
||||
<Field
|
||||
as={StyledInput}
|
||||
name="email"
|
||||
placeholder="Email Address"
|
||||
type="email"
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '1rem' }}>
|
||||
<ReCAPTCHA
|
||||
ref={recaptchaRef}
|
||||
sitekey="6LfENu4qAAAAAFtPejcrP3dwBDxcRPjqi7RhytJJ"
|
||||
size="invisible"
|
||||
theme="dark"
|
||||
/>
|
||||
</div>
|
||||
<StyledButton type="submit" disabled={formik.isSubmitting}>
|
||||
Reset Password
|
||||
</StyledButton>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<BackLink onClick={() => navigate('/sign_in')}>
|
||||
Back to Sign In
|
||||
</BackLink>
|
||||
</GlassCard>
|
||||
</ContentWrapper>
|
||||
</PageContainer>
|
||||
);
|
||||
email: string;
|
||||
};
|
||||
|
||||
export default PasswordReset;
|
||||
const validationSchema = Yup.object().shape({
|
||||
email: Yup.string().email('Invalid email').required('Required'),
|
||||
});
|
||||
|
||||
const PasswordReset = (): JSX.Element => {
|
||||
const navigate = useNavigate();
|
||||
const recaptchaRef = useRef<ReCAPTCHA>(null);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const handlePasswordResetEmail = async ({
|
||||
email,
|
||||
}: EmailPasswordResetValues): Promise<void> => {
|
||||
setErrorMessage('');
|
||||
try {
|
||||
let token = '';
|
||||
if (recaptchaRef.current) {
|
||||
token = (await recaptchaRef.current.executeAsync()) || '';
|
||||
recaptchaRef.current.reset();
|
||||
}
|
||||
|
||||
await cleanAxiosInstance.post('user/reset_password/', {
|
||||
email,
|
||||
recaptchaToken: token,
|
||||
});
|
||||
navigate('/password_reset_confirmation/');
|
||||
} catch {
|
||||
setErrorMessage('Unable to reset password. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ParticleBackground />
|
||||
<ContentWrapper>
|
||||
<GlassCard>
|
||||
<CardTitle>Reset Password</CardTitle>
|
||||
<CardSubtitle>
|
||||
Enter your email and we will send a link to set a new password.
|
||||
</CardSubtitle>
|
||||
{errorMessage && <ErrorMessage>{errorMessage}</ErrorMessage>}
|
||||
<Formik
|
||||
initialValues={{ email: '' }}
|
||||
onSubmit={handlePasswordResetEmail}
|
||||
validationSchema={validationSchema}
|
||||
>
|
||||
{(formik) => (
|
||||
<Form style={{ width: '100%' }}>
|
||||
<Field
|
||||
as={StyledInput}
|
||||
name="email"
|
||||
placeholder="Email Address"
|
||||
type="email"
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<ReCAPTCHA
|
||||
ref={recaptchaRef}
|
||||
sitekey="6LfENu4qAAAAAFtPejcrP3dwBDxcRPjqi7RhytJJ"
|
||||
size="invisible"
|
||||
theme="dark"
|
||||
/>
|
||||
</div>
|
||||
<StyledButton type="submit" disabled={formik.isSubmitting}>
|
||||
Reset Password
|
||||
</StyledButton>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<BackLink onClick={() => navigate('/signin/')}>Back to Sign In</BackLink>
|
||||
</GlassCard>
|
||||
</ContentWrapper>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordReset;
|
||||
|
||||
@@ -1,40 +1,87 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, Divider } from '@mui/material';
|
||||
import PageWrapperLayout from '../../components/PageWrapperLayout/PageWrapperLayout';
|
||||
import MDBox from '../../ui-kit/components/MDBox';
|
||||
import background from '../../../bg.jpeg'
|
||||
import { Col, Row } from 'react-bootstrap';
|
||||
import MDTypography from '../../ui-kit/components/MDTypography';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ParticleBackground from '../../components/ParticleBackground/ParticleBackground';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const PageContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
font-family: 'Inter', sans-serif;
|
||||
`;
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 5;
|
||||
padding: 2rem;
|
||||
`;
|
||||
|
||||
const GlassCard = styled.div`
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 1rem;
|
||||
padding: 3rem;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const CardTitle = styled.h2`
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #fff;
|
||||
`;
|
||||
|
||||
const CardBody = styled.p`
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1.5rem;
|
||||
`;
|
||||
|
||||
const BackLink = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-top: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const PasswordResetConfirmation = (): JSX.Element => {
|
||||
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<PageWrapperLayout>
|
||||
<MDBox sx={{'height': '100%', minHeight: '100vh', display: 'flex', flexDirection: 'column', backgroundImage: `url(${background})`,backgroundSize: "cover",
|
||||
backgroundRepeat: "no-repeat",}}>
|
||||
|
||||
<MDBox sx={{ margin: '0 auto', width: '80%', height: '80%', minHeight: '80%', maxHeight: '80%', align:'center'}}>
|
||||
<Row>
|
||||
<Col className='col -lg-4 col-md-8 col-12 mx-auto'>
|
||||
<Card sx={{mt:30}} >
|
||||
<CardContent>
|
||||
<MDTypography variant="h3">
|
||||
Reset Password Confirmation
|
||||
</MDTypography>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<MDTypography>
|
||||
Check your email for a link to set your password!
|
||||
</MDTypography>
|
||||
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</MDBox>
|
||||
</MDBox>
|
||||
|
||||
</PageWrapperLayout>
|
||||
<PageContainer>
|
||||
<ParticleBackground />
|
||||
<ContentWrapper>
|
||||
<GlassCard>
|
||||
<CardTitle>Check Your Email</CardTitle>
|
||||
<CardBody>
|
||||
If an account exists for that address, we sent a link to set a new password.
|
||||
</CardBody>
|
||||
<BackLink onClick={() => navigate('/signin/')}>Back to Sign In</BackLink>
|
||||
</GlassCard>
|
||||
</ContentWrapper>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -255,7 +255,7 @@ const SignIn = (): JSX.Element => {
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<ForgotPasswordLink onClick={() => navigate('/password_reset')}>
|
||||
<ForgotPasswordLink onClick={() => navigate('/password_reset/')}>
|
||||
Forgot Password?
|
||||
</ForgotPasswordLink>
|
||||
{registrationEnabled && (
|
||||
|
||||
Reference in New Issue
Block a user