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 { Form, Formik, Field } from 'formik';
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import CustomPasswordField from '../../components/CustomPasswordField/CustomPasswordField';
|
import { cleanAxiosInstance } from '../../../axiosApi';
|
||||||
import { Stack, Typography } from '@mui/material';
|
|
||||||
import { axiosInstance } from '../../../axiosApi';
|
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
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 * 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 = {
|
export type PasswordResetValues = {
|
||||||
password1: string;
|
password1: string;
|
||||||
password2: string;
|
password2: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialValues = { password1: '', password2: '' }
|
|
||||||
const validationSchema = Yup.object().shape({
|
const validationSchema = Yup.object().shape({
|
||||||
password1: Yup.string().min(6, "Passwords have to be at least 6 digits").required(),
|
password1: Yup.string()
|
||||||
password2: Yup.string().min(6, "Passwords have to be at least 6 digits").required().oneOf([Yup.ref('password1')], "Passwords must match"),
|
.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')
|
||||||
const contains_number = (item: string): boolean => {
|
.required('Required'),
|
||||||
const numbers = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
|
password2: Yup.string()
|
||||||
const hasNumber = numbers.some((character) => item.includes(character));
|
.oneOf([Yup.ref('password1')], 'Passwords must match')
|
||||||
if (hasNumber) {
|
.required('Required'),
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SetPassword = (): JSX.Element => {
|
const SetPassword = (): JSX.Element => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// see if the user is allowed to come here first
|
const [queryParameters] = useSearchParams();
|
||||||
const [queryParameters] = useSearchParams()
|
const slug = queryParameters.get('slug');
|
||||||
console.log(queryParameters.get("slug"))
|
const [linkValid, setLinkValid] = useState(false);
|
||||||
const slug = queryParameters.get("slug");
|
const [checking, setChecking] = useState(true);
|
||||||
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
try {
|
(async () => {
|
||||||
// make sure it comes back as 200 for a good request. Else go to the homepage
|
if (!slug) {
|
||||||
axiosInstance.get(`user/set_password/${slug}`)
|
navigate('/signin/');
|
||||||
} catch {
|
return;
|
||||||
navigate('/')
|
}
|
||||||
}
|
try {
|
||||||
|
await cleanAxiosInstance.get(`user/set_password/${slug}/`);
|
||||||
const handleSetPassword = ({ password1, password2 }: PasswordResetValues): void => {
|
if (!cancelled) {
|
||||||
try {
|
setLinkValid(true);
|
||||||
// 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} />
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
} 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 (
|
return (
|
||||||
|
<PageContainer>
|
||||||
<PageWrapperLayout>
|
<ParticleBackground />
|
||||||
<MDBox sx={{
|
<ContentWrapper>
|
||||||
height: '100vh',
|
<GlassCard>
|
||||||
display: 'flex',
|
<CardTitle>Loading…</CardTitle>
|
||||||
justifyContent: 'center',
|
</GlassCard>
|
||||||
alignItems: 'center',
|
</ContentWrapper>
|
||||||
bgcolor: 'background.default',
|
</PageContainer>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 { Form, Formik, Field } from 'formik';
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { cleanAxiosInstance } from '../../../axiosApi';
|
import { cleanAxiosInstance } from '../../../axiosApi';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
import ReCAPTCHA from 'react-google-recaptcha';
|
||||||
import ParticleBackground from '../../components/ParticleBackground/ParticleBackground';
|
import ParticleBackground from '../../components/ParticleBackground/ParticleBackground';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
// Styled Components
|
|
||||||
const PageContainer = styled.div`
|
const PageContainer = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
@@ -43,11 +43,19 @@ const GlassCard = styled.div`
|
|||||||
|
|
||||||
const CardTitle = styled.h2`
|
const CardTitle = styled.h2`
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1rem;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-align: center;
|
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`
|
const StyledInput = styled.input`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
@@ -111,134 +119,92 @@ const BackLink = styled.button`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type PasswordResetValues = {
|
const ErrorMessage = styled.div`
|
||||||
password1: string;
|
color: #ff6b6b;
|
||||||
password2: string;
|
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 = {
|
export type EmailPasswordResetValues = {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
email: Yup.string().email('Invalid email').required('Required'),
|
||||||
|
});
|
||||||
|
|
||||||
const PasswordReset = (): JSX.Element => {
|
const PasswordReset = (): JSX.Element => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const recaptchaRef = useRef<ReCAPTCHA>(null);
|
const recaptchaRef = useRef<ReCAPTCHA>(null);
|
||||||
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
|
||||||
const handlePasswordResetEmail = ({ email }: EmailPasswordResetValues): void => {
|
const handlePasswordResetEmail = async ({
|
||||||
if (recaptchaRef.current) {
|
email,
|
||||||
const token = recaptchaRef.current.getValue();
|
}: EmailPasswordResetValues): Promise<void> => {
|
||||||
if (!token) { // This logic seems inverted in original code? "if (!token)" usually means no token.
|
setErrorMessage('');
|
||||||
// But original code had "if (!token)" then try... catch.
|
try {
|
||||||
// Wait, if !token, it means user didn't solve captcha?
|
let token = '';
|
||||||
// Or maybe invisible captcha returns token immediately?
|
if (recaptchaRef.current) {
|
||||||
// Let's assume the original logic was trying to say "if token is present" but maybe had a bug or I'm misreading.
|
token = (await recaptchaRef.current.executeAsync()) || '';
|
||||||
// Actually, looking at original code:
|
recaptchaRef.current.reset();
|
||||||
// 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 {
|
await cleanAxiosInstance.post('user/reset_password/', {
|
||||||
cleanAxiosInstance.post('user/reset_password',
|
email,
|
||||||
{
|
recaptchaToken: token,
|
||||||
'email': email,
|
});
|
||||||
'recaptchaToken': token || "dummy_token_if_logic_was_inverted" // preserving original weirdness slightly but making it safer?
|
navigate('/password_reset_confirmation/');
|
||||||
}
|
} catch {
|
||||||
);
|
setErrorMessage('Unable to reset password. Please try again.');
|
||||||
// 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 (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<ParticleBackground />
|
<ParticleBackground />
|
||||||
<ContentWrapper>
|
<ContentWrapper>
|
||||||
<GlassCard>
|
<GlassCard>
|
||||||
<CardTitle>Reset Password</CardTitle>
|
<CardTitle>Reset Password</CardTitle>
|
||||||
<Formik
|
<CardSubtitle>
|
||||||
initialValues={{
|
Enter your email and we will send a link to set a new password.
|
||||||
email: '',
|
</CardSubtitle>
|
||||||
}}
|
{errorMessage && <ErrorMessage>{errorMessage}</ErrorMessage>}
|
||||||
onSubmit={handlePasswordResetEmail}
|
<Formik
|
||||||
>
|
initialValues={{ email: '' }}
|
||||||
{(formik) => (
|
onSubmit={handlePasswordResetEmail}
|
||||||
<Form style={{ width: '100%' }}>
|
validationSchema={validationSchema}
|
||||||
<Field
|
>
|
||||||
as={StyledInput}
|
{(formik) => (
|
||||||
name="email"
|
<Form style={{ width: '100%' }}>
|
||||||
placeholder="Email Address"
|
<Field
|
||||||
type="email"
|
as={StyledInput}
|
||||||
/>
|
name="email"
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '1rem' }}>
|
placeholder="Email Address"
|
||||||
<ReCAPTCHA
|
type="email"
|
||||||
ref={recaptchaRef}
|
/>
|
||||||
sitekey="6LfENu4qAAAAAFtPejcrP3dwBDxcRPjqi7RhytJJ"
|
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
size="invisible"
|
<ReCAPTCHA
|
||||||
theme="dark"
|
ref={recaptchaRef}
|
||||||
/>
|
sitekey="6LfENu4qAAAAAFtPejcrP3dwBDxcRPjqi7RhytJJ"
|
||||||
</div>
|
size="invisible"
|
||||||
<StyledButton type="submit" disabled={formik.isSubmitting}>
|
theme="dark"
|
||||||
Reset Password
|
/>
|
||||||
</StyledButton>
|
</div>
|
||||||
</Form>
|
<StyledButton type="submit" disabled={formik.isSubmitting}>
|
||||||
)}
|
Reset Password
|
||||||
</Formik>
|
</StyledButton>
|
||||||
<BackLink onClick={() => navigate('/sign_in')}>
|
</Form>
|
||||||
Back to Sign In
|
)}
|
||||||
</BackLink>
|
</Formik>
|
||||||
</GlassCard>
|
<BackLink onClick={() => navigate('/signin/')}>Back to Sign In</BackLink>
|
||||||
</ContentWrapper>
|
</GlassCard>
|
||||||
</PageContainer>
|
</ContentWrapper>
|
||||||
);
|
</PageContainer>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasswordReset;
|
export default PasswordReset;
|
||||||
@@ -1,40 +1,87 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Card, CardContent, Divider } from '@mui/material';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import PageWrapperLayout from '../../components/PageWrapperLayout/PageWrapperLayout';
|
import ParticleBackground from '../../components/ParticleBackground/ParticleBackground';
|
||||||
import MDBox from '../../ui-kit/components/MDBox';
|
import styled from 'styled-components';
|
||||||
import background from '../../../bg.jpeg'
|
|
||||||
import { Col, Row } from 'react-bootstrap';
|
const PageContainer = styled.div`
|
||||||
import MDTypography from '../../ui-kit/components/MDTypography';
|
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 PasswordResetConfirmation = (): JSX.Element => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapperLayout>
|
<PageContainer>
|
||||||
<MDBox sx={{'height': '100%', minHeight: '100vh', display: 'flex', flexDirection: 'column', backgroundImage: `url(${background})`,backgroundSize: "cover",
|
<ParticleBackground />
|
||||||
backgroundRepeat: "no-repeat",}}>
|
<ContentWrapper>
|
||||||
|
<GlassCard>
|
||||||
<MDBox sx={{ margin: '0 auto', width: '80%', height: '80%', minHeight: '80%', maxHeight: '80%', align:'center'}}>
|
<CardTitle>Check Your Email</CardTitle>
|
||||||
<Row>
|
<CardBody>
|
||||||
<Col className='col -lg-4 col-md-8 col-12 mx-auto'>
|
If an account exists for that address, we sent a link to set a new password.
|
||||||
<Card sx={{mt:30}} >
|
</CardBody>
|
||||||
<CardContent>
|
<BackLink onClick={() => navigate('/signin/')}>Back to Sign In</BackLink>
|
||||||
<MDTypography variant="h3">
|
</GlassCard>
|
||||||
Reset Password Confirmation
|
</ContentWrapper>
|
||||||
</MDTypography>
|
</PageContainer>
|
||||||
</CardContent>
|
|
||||||
<Divider />
|
|
||||||
<MDTypography>
|
|
||||||
Check your email for a link to set your password!
|
|
||||||
</MDTypography>
|
|
||||||
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</MDBox>
|
|
||||||
</MDBox>
|
|
||||||
|
|
||||||
</PageWrapperLayout>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ const SignIn = (): JSX.Element => {
|
|||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
<ForgotPasswordLink onClick={() => navigate('/password_reset')}>
|
<ForgotPasswordLink onClick={() => navigate('/password_reset/')}>
|
||||||
Forgot Password?
|
Forgot Password?
|
||||||
</ForgotPasswordLink>
|
</ForgotPasswordLink>
|
||||||
{registrationEnabled && (
|
{registrationEnabled && (
|
||||||
|
|||||||
Reference in New Issue
Block a user