-
-
-
-
+
+
+
+
+ Loading…
+
+
+
);
+ }
+
+ return (
+
+
+
+
+ Set Your Password
+ {errorMessage && {errorMessage}}
+
+ {(formik) => (
+
+ )}
+
+
+
+
+ );
};
-export default SetPassword;
\ No newline at end of file
+export default SetPassword;
diff --git a/llm-fe/src/llm-fe/pages/PasswordReset/PasswordReset.test.tsx b/llm-fe/src/llm-fe/pages/PasswordReset/PasswordReset.test.tsx
new file mode 100644
index 0000000..2691d90
--- /dev/null
+++ b/llm-fe/src/llm-fe/pages/PasswordReset/PasswordReset.test.tsx
@@ -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; reset: () => void }>
+ ) => {
+ ReactLib.useImperativeHandle(ref, () => ({
+ executeAsync: mockExecuteAsync,
+ reset: mockReset,
+ }));
+ return ;
+ }
+ );
+});
+
+jest.mock('../../../axiosApi', () => ({
+ cleanAxiosInstance: {
+ post: (...args: unknown[]) => mockPost(...args),
+ },
+}));
+
+const renderPasswordReset = () =>
+ render(
+
+
+ } />
+ Confirmation page}
+ />
+
+
+ );
+
+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();
+ });
+});
diff --git a/llm-fe/src/llm-fe/pages/PasswordReset/PasswordReset.tsx b/llm-fe/src/llm-fe/pages/PasswordReset/PasswordReset.tsx
index 9a6d7f6..2435fa2 100644
--- a/llm-fe/src/llm-fe/pages/PasswordReset/PasswordReset.tsx
+++ b/llm-fe/src/llm-fe/pages/PasswordReset/PasswordReset.tsx
@@ -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(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 (
-
-
-
-
- Reset Password
-
- {(formik) => (
-
- )}
-
- navigate('/sign_in')}>
- Back to Sign In
-
-
-
-
- );
+ email: string;
};
-export default PasswordReset;
\ No newline at end of file
+const validationSchema = Yup.object().shape({
+ email: Yup.string().email('Invalid email').required('Required'),
+});
+
+const PasswordReset = (): JSX.Element => {
+ const navigate = useNavigate();
+ const recaptchaRef = useRef(null);
+ const [errorMessage, setErrorMessage] = useState('');
+
+ const handlePasswordResetEmail = async ({
+ email,
+ }: EmailPasswordResetValues): Promise => {
+ 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 (
+
+
+
+
+ Reset Password
+
+ Enter your email and we will send a link to set a new password.
+
+ {errorMessage && {errorMessage}}
+
+ {(formik) => (
+
+ )}
+
+ navigate('/signin/')}>Back to Sign In
+
+
+
+ );
+};
+
+export default PasswordReset;
diff --git a/llm-fe/src/llm-fe/pages/PasswordResetConfirmation/PasswordReset.tsx b/llm-fe/src/llm-fe/pages/PasswordResetConfirmation/PasswordReset.tsx
index 7c5ebba..6a4b731 100644
--- a/llm-fe/src/llm-fe/pages/PasswordResetConfirmation/PasswordReset.tsx
+++ b/llm-fe/src/llm-fe/pages/PasswordResetConfirmation/PasswordReset.tsx
@@ -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 (
-
-
-
-
-
-
-
-
-
- Reset Password Confirmation
-
-
-
-
- Check your email for a link to set your password!
-
-
-
-
-
-
-
-
-
+
+
+
+
+ Check Your Email
+
+ If an account exists for that address, we sent a link to set a new password.
+
+ navigate('/signin/')}>Back to Sign In
+
+
+
);
};
diff --git a/llm-fe/src/llm-fe/pages/SignIn/SignIn.tsx b/llm-fe/src/llm-fe/pages/SignIn/SignIn.tsx
index 533e917..1d2c394 100644
--- a/llm-fe/src/llm-fe/pages/SignIn/SignIn.tsx
+++ b/llm-fe/src/llm-fe/pages/SignIn/SignIn.tsx
@@ -255,7 +255,7 @@ const SignIn = (): JSX.Element => {
)}
- navigate('/password_reset')}>
+ navigate('/password_reset/')}>
Forgot Password?
{registrationEnabled && (