ensured password reset and account verification works as well as support dashboard
This commit is contained in:
2436
ditch-the-agent/package-lock.json
generated
2436
ditch-the-agent/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,8 @@
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"predeploy": "vite build && cp ./dist/index.html ./dist/404.html",
|
||||
"deploy": "gh-pages -d dist"
|
||||
"deploy": "gh-pages -d dist",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
@@ -43,6 +44,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "^4.1.1",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^14.2.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
@@ -52,8 +56,10 @@
|
||||
"eslint-plugin-prettier": "^5.5.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"jsdom": "^24.0.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.0",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.3.1"
|
||||
}
|
||||
}
|
||||
@@ -72,9 +72,9 @@ const SupportAgentDashboard: React.FC = () => {
|
||||
text: item.description,
|
||||
created_at: item.created_at,
|
||||
updated_at: item.updated_at,
|
||||
user: -1, // Placeholder
|
||||
user_first_name: 'System',
|
||||
user_last_name: 'Description'
|
||||
user: item.user_email,
|
||||
user_first_name: item.user_first_name,
|
||||
user_last_name: item.user_last_name,
|
||||
}]
|
||||
}));
|
||||
|
||||
@@ -129,7 +129,7 @@ const SupportAgentDashboard: React.FC = () => {
|
||||
result = result.filter(item =>
|
||||
item.title.toLowerCase().includes(lowerSearch) ||
|
||||
item.description.toLowerCase().includes(lowerSearch)
|
||||
// || item.user?.email?.toLowerCase().includes(lowerSearch) // If user object existed
|
||||
|| item.user_email?.toLowerCase().includes(lowerSearch) // If user object existed
|
||||
);
|
||||
}
|
||||
|
||||
@@ -215,6 +215,26 @@ const SupportAgentDashboard: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseCase = async () => {
|
||||
if (!selectedSupportCaseId) return;
|
||||
|
||||
try {
|
||||
const { data }: AxiosResponse<SupportCaseApi> = await axiosInstance.patch(
|
||||
`/support/cases/${selectedSupportCaseId}/`,
|
||||
{ status: 'closed' }
|
||||
);
|
||||
|
||||
setSupportCase(prev => prev ? { ...prev, status: data.status, updated_at: data.updated_at } : null);
|
||||
setSupportCases(prevCases =>
|
||||
prevCases.map(c =>
|
||||
c.id === selectedSupportCaseId ? { ...c, status: data.status, updated_at: data.updated_at } : c
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to close case", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <PageLoader />;
|
||||
}
|
||||
@@ -233,7 +253,8 @@ const SupportAgentDashboard: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{ py: 4, height: '90vh', width: 'fill', display: 'flex', flexDirection: 'column' }}
|
||||
maxWidth={false}
|
||||
sx={{ py: 4, height: '90vh', width: '100%', display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
@@ -242,7 +263,9 @@ const SupportAgentDashboard: React.FC = () => {
|
||||
<Grid container sx={{ height: '100%', width: '100%' }}>
|
||||
{/* Left Panel: List & Filters */}
|
||||
<Grid
|
||||
size={{ xs: 12, md: 4 }}
|
||||
item
|
||||
xs={12}
|
||||
md={4}
|
||||
sx={{
|
||||
borderRight: { md: '1px solid', borderColor: 'grey.200' },
|
||||
display: 'flex',
|
||||
@@ -337,6 +360,7 @@ const SupportAgentDashboard: React.FC = () => {
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))
|
||||
)}
|
||||
@@ -354,12 +378,21 @@ const SupportAgentDashboard: React.FC = () => {
|
||||
borderColor: 'grey.200',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="h3" sx={{ fontWeight: 'bold' }}>
|
||||
{supportCase.title} | {supportCase.category}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={handleCloseCase}
|
||||
disabled={supportCase.status === 'closed'}
|
||||
>
|
||||
{supportCase.status === 'closed' ? 'Closed' : 'Close Case'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 3, bgcolor: 'grey.50' }}>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { vi } from 'vitest';
|
||||
import ForgotPassword from './ForgotPassword';
|
||||
import { axiosInstance } from '../../axiosApi.js';
|
||||
|
||||
// Mock axiosInstance
|
||||
vi.mock('../../axiosApi.js', () => ({
|
||||
axiosInstance: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ForgotPassword', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
render(<ForgotPassword />);
|
||||
expect(screen.getByText('Forgot Password')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Send Password Reset Link/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates email input', () => {
|
||||
render(<ForgotPassword />);
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
|
||||
expect(emailInput).toHaveValue('test@example.com');
|
||||
});
|
||||
|
||||
it('calls API and shows success message on valid submission', async () => {
|
||||
(axiosInstance.post as any).mockResolvedValue({ status: 200 });
|
||||
render(<ForgotPassword />);
|
||||
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: /Send Password Reset Link/i });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(submitButton).toBeDisabled();
|
||||
expect(screen.getByText('Sending...')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(axiosInstance.post).toHaveBeenCalledWith('password-reset/', { email: 'test@example.com' });
|
||||
expect(screen.getByText('Password reset link has been sent to your email.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error message on API failure', async () => {
|
||||
const errorMessage = 'User not found';
|
||||
(axiosInstance.post as any).mockRejectedValue({
|
||||
response: {
|
||||
data: {
|
||||
detail: errorMessage,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ForgotPassword />);
|
||||
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
fireEvent.change(emailInput, { target: { value: 'wrong@example.com' } });
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: /Send Password Reset Link/i });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
FormControl,
|
||||
InputAdornment,
|
||||
@@ -10,12 +11,36 @@ import {
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import Image from 'components/base/Image';
|
||||
import { Suspense } from 'react';
|
||||
import { Suspense, useState } from 'react';
|
||||
import forgotPassword from 'assets/authentication-banners/green.png';
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import logo from 'assets/logo/favicon-logo.png';
|
||||
import { axiosInstance } from '../../axiosApi.js';
|
||||
|
||||
const ForgotPassword = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const handleResetPassword = async () => {
|
||||
setLoading(true);
|
||||
setSuccessMessage(null);
|
||||
setErrorMessage(null);
|
||||
try {
|
||||
await axiosInstance.post('password-reset/', { email });
|
||||
setSuccessMessage('Password reset link has been sent to your email.');
|
||||
} catch (error: any) {
|
||||
setErrorMessage(
|
||||
error.response?.data?.email?.[0] ||
|
||||
error.response?.data?.detail ||
|
||||
'An error occurred. Please try again.',
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -30,14 +55,18 @@ const ForgotPassword = () => {
|
||||
</Link>
|
||||
<Stack alignItems="center" gap={6.5} width={330} mx="auto">
|
||||
<Typography variant="h3">Forgot Password</Typography>
|
||||
{successMessage && <Alert severity="success">{successMessage}</Alert>}
|
||||
{errorMessage && <Alert severity="error">{errorMessage}</Alert>}
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel shrink htmlFor="new-password">
|
||||
<InputLabel shrink htmlFor="email">
|
||||
Email
|
||||
</InputLabel>
|
||||
<TextField
|
||||
variant="filled"
|
||||
placeholder="Enter your email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
@@ -47,8 +76,13 @@ const ForgotPassword = () => {
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button variant="contained" fullWidth>
|
||||
Send Password Reset Link
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleResetPassword}
|
||||
disabled={loading || !email}
|
||||
>
|
||||
{loading ? 'Sending...' : 'Send Password Reset Link'}
|
||||
</Button>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Back to{' '}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { axiosInstance } from '../../axiosApi.js';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Form, Formik } from 'formik';
|
||||
import { AuthContext } from 'contexts/AuthContext.js';
|
||||
import paths from 'routes/paths';
|
||||
|
||||
type loginValues = {
|
||||
email: string;
|
||||
@@ -50,7 +51,15 @@ const Login = (): ReactElement => {
|
||||
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
const hasErrors = Object.keys(error.response.data).length > 0;
|
||||
if (
|
||||
error.response &&
|
||||
error.response.status === 401 &&
|
||||
error.response.data.detail === 'No active account found with the given credentials'
|
||||
) {
|
||||
navigate(paths.verifyAccount, { state: { email } });
|
||||
return;
|
||||
}
|
||||
const hasErrors = error.response && Object.keys(error.response.data).length > 0;
|
||||
if (hasErrors) {
|
||||
setErrorMessage(error.response.data);
|
||||
} else {
|
||||
|
||||
157
ditch-the-agent/src/pages/authentication/ResetPassword.test.tsx
Normal file
157
ditch-the-agent/src/pages/authentication/ResetPassword.test.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { vi } from 'vitest';
|
||||
import ResetPassword from './ResetPassword';
|
||||
import { axiosInstance } from '../../axiosApi.js';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
// Mock axiosInstance
|
||||
vi.mock('../../axiosApi.js', () => ({
|
||||
axiosInstance: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock useSearchParams
|
||||
const mockGet = vi.fn();
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useSearchParams: () => [{ get: mockGet }],
|
||||
};
|
||||
});
|
||||
|
||||
describe('ResetPassword', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockGet.mockImplementation((key) => {
|
||||
if (key === 'email') return 'test@example.com';
|
||||
if (key === 'code') return '123456';
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
const renderComponent = () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ResetPassword />
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
it('renders correctly', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByRole('heading', { name: /Reset Password/i })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Reset Code')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Password')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Confirm Password')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Reset Password/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates inputs', () => {
|
||||
renderComponent();
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
const codeInput = screen.getByLabelText('Reset Code');
|
||||
const passwordInput = screen.getByLabelText('Password');
|
||||
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: 'new@example.com' } });
|
||||
fireEvent.change(codeInput, { target: { value: '654321' } });
|
||||
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||
fireEvent.change(confirmInput, { target: { value: 'password123' } });
|
||||
|
||||
expect(emailInput).toHaveValue('new@example.com');
|
||||
expect(codeInput).toHaveValue('654321');
|
||||
expect(passwordInput).toHaveValue('password123');
|
||||
expect(confirmInput).toHaveValue('password123');
|
||||
});
|
||||
|
||||
it('shows error if passwords do not match', async () => {
|
||||
renderComponent();
|
||||
const passwordInput = screen.getByLabelText('Password');
|
||||
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||
const submitButton = screen.getByRole('button', { name: /Reset Password/i });
|
||||
|
||||
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||
fireEvent.change(confirmInput, { target: { value: 'password456' } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(screen.getByText("Passwords don't match")).toBeInTheDocument();
|
||||
expect(axiosInstance.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls API and shows success message on valid submission', async () => {
|
||||
(axiosInstance.post as any).mockResolvedValue({ status: 200 });
|
||||
renderComponent();
|
||||
|
||||
// Email and Code are pre-filled from mock
|
||||
const passwordInput = screen.getByLabelText('Password');
|
||||
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||
const submitButton = screen.getByRole('button', { name: /Reset Password/i });
|
||||
|
||||
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||
fireEvent.change(confirmInput, { target: { value: 'password123' } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(submitButton).toBeDisabled();
|
||||
expect(screen.getByText('Resetting...')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(axiosInstance.post).toHaveBeenCalledWith('password-reset/confirm/', {
|
||||
email: 'test@example.com',
|
||||
code: '123456',
|
||||
new_password: 'password123',
|
||||
new_password2: 'password123',
|
||||
});
|
||||
expect(screen.getByText('Reset Successfully')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error message on API failure', async () => {
|
||||
const errorMessage = 'Invalid token';
|
||||
(axiosInstance.post as any).mockRejectedValue({
|
||||
response: {
|
||||
data: {
|
||||
detail: errorMessage,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderComponent();
|
||||
|
||||
const passwordInput = screen.getByLabelText('Password');
|
||||
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||
const submitButton = screen.getByRole('button', { name: /Reset Password/i });
|
||||
|
||||
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||
fireEvent.change(confirmInput, { target: { value: 'password123' } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error if email or code is missing', async () => {
|
||||
mockGet.mockReturnValue(null); // No URL params
|
||||
renderComponent();
|
||||
|
||||
// Inputs should be empty
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
const codeInput = screen.getByLabelText('Reset Code');
|
||||
expect(emailInput).toHaveValue('');
|
||||
expect(codeInput).toHaveValue('');
|
||||
|
||||
const passwordInput = screen.getByLabelText('Password');
|
||||
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||
const submitButton = screen.getByRole('button', { name: /Reset Password/i });
|
||||
|
||||
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||
fireEvent.change(confirmInput, { target: { value: 'password123' } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(screen.getByText('Please provide both email and reset code.')).toBeInTheDocument();
|
||||
expect(axiosInstance.post).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactElement, Suspense, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
@@ -17,30 +18,55 @@ import passwordUpdated from 'assets/authentication-banners/password-updated.png'
|
||||
import successTick from 'assets/authentication-banners/successTick.png';
|
||||
import Image from 'components/base/Image';
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import PasswordStrengthChecker from '../../components/PasswordStrengthChecker';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { axiosInstance } from '../../axiosApi.js';
|
||||
|
||||
const ResetPassword = (): ReactElement => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [showNewPassword, setShowNewPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [email, setEmail] = useState(searchParams.get('email') || '');
|
||||
const [code, setCode] = useState(searchParams.get('code') || '');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [resetSuccessful, setResetSuccessful] = useState(false);
|
||||
|
||||
const handleClickShowNewPassword = () => setShowNewPassword(!showNewPassword);
|
||||
const handleClickShowConfirmPassword = () => setShowConfirmPassword(!showConfirmPassword);
|
||||
const [resetSuccessful, setResetSuccessful] = useState(false);
|
||||
|
||||
const handleResetPassword = () => {
|
||||
const passwordField: HTMLInputElement = document.getElementById(
|
||||
'new-password',
|
||||
) as HTMLInputElement;
|
||||
const confirmPasswordField: HTMLInputElement = document.getElementById(
|
||||
'confirm-password',
|
||||
) as HTMLInputElement;
|
||||
|
||||
if (passwordField.value !== confirmPasswordField.value) {
|
||||
alert("Passwords don't match");
|
||||
const handleResetPassword = async () => {
|
||||
if (password !== confirmPassword) {
|
||||
setErrorMessage("Passwords don't match");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email || !code) {
|
||||
setErrorMessage('Please provide both email and reset code.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
await axiosInstance.post('password-reset/confirm/', {
|
||||
email,
|
||||
code,
|
||||
new_password: password,
|
||||
new_password2: confirmPassword,
|
||||
});
|
||||
setResetSuccessful(true);
|
||||
} catch (error: any) {
|
||||
setErrorMessage(
|
||||
error.response?.data?.detail ||
|
||||
error.response?.data?.non_field_errors?.[0] ||
|
||||
'An error occurred. Please try again.',
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -58,6 +84,45 @@ const ResetPassword = (): ReactElement => {
|
||||
{!resetSuccessful ? (
|
||||
<Stack alignItems="center" gap={3.75} width={330} mx="auto">
|
||||
<Typography variant="h3">Reset Password</Typography>
|
||||
{errorMessage && <Alert severity="error">{errorMessage}</Alert>}
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel shrink htmlFor="email">
|
||||
Email
|
||||
</InputLabel>
|
||||
<TextField
|
||||
variant="filled"
|
||||
placeholder="Enter your email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconifyIcon icon="ic:baseline-email" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel shrink htmlFor="code">
|
||||
Reset Code
|
||||
</InputLabel>
|
||||
<TextField
|
||||
variant="filled"
|
||||
placeholder="Enter reset code"
|
||||
id="code"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconifyIcon icon="ic:baseline-lock" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel shrink htmlFor="new-password">
|
||||
Password
|
||||
@@ -67,6 +132,7 @@ const ResetPassword = (): ReactElement => {
|
||||
placeholder="Enter new password"
|
||||
type={showNewPassword ? 'text' : 'password'}
|
||||
id="new-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
@@ -89,17 +155,18 @@ const ResetPassword = (): ReactElement => {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{/*<PasswordStrengthChecker password={password} />*/}
|
||||
</FormControl>
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel shrink htmlFor="confirm-password">
|
||||
Password
|
||||
Confirm Password
|
||||
</InputLabel>
|
||||
<TextField
|
||||
variant="filled"
|
||||
placeholder="Confirm password"
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
id="confirm-password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
@@ -122,8 +189,13 @@ const ResetPassword = (): ReactElement => {
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button variant="contained" fullWidth onClick={handleResetPassword}>
|
||||
Reset Password
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleResetPassword}
|
||||
disabled={loading || !password || !confirmPassword}
|
||||
>
|
||||
{loading ? 'Resetting...' : 'Reset Password'}
|
||||
</Button>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Back to{' '}
|
||||
@@ -140,7 +212,7 @@ const ResetPassword = (): ReactElement => {
|
||||
<Stack alignItems="center" gap={3.75} width={330} mx="auto">
|
||||
<Image src={successTick} />
|
||||
<Typography variant="h3">Reset Successfully</Typography>
|
||||
<Typography variant="body1" textAlign="center" color="text.secndary">
|
||||
<Typography variant="body1" textAlign="center" color="text.secondary">
|
||||
Your Ditch the Agent log in password has been updated successfully
|
||||
</Typography>
|
||||
<Button variant="contained" fullWidth LinkComponent={Link} href="/authentication/login">
|
||||
|
||||
203
ditch-the-agent/src/pages/authentication/VerifyAccount.tsx
Normal file
203
ditch-the-agent/src/pages/authentication/VerifyAccount.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { ReactElement, Suspense, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
FormControl,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
Link,
|
||||
Skeleton,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import loginBanner from 'assets/authentication-banners/green.png';
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import logo from 'assets/logo/favicon-logo.png';
|
||||
import Image from 'components/base/Image';
|
||||
import { axiosInstance } from '../../axiosApi.js';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Form, Formik } from 'formik';
|
||||
import paths from 'routes/paths';
|
||||
|
||||
type verifyValues = {
|
||||
email: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
const VerifyAccount = (): ReactElement => {
|
||||
const [errorMessage, setErrorMessage] = useState<any | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const initialEmail = location.state?.email || '';
|
||||
|
||||
const handleVerify = async ({ email, code }: verifyValues): Promise<void> => {
|
||||
try {
|
||||
await axiosInstance.post('/check-passcode/', {
|
||||
email: email,
|
||||
code: code,
|
||||
});
|
||||
navigate('/');
|
||||
} catch (error) {
|
||||
const hasErrors = error.response && Object.keys(error.response.data).length > 0;
|
||||
if (hasErrors) {
|
||||
setErrorMessage(error.response.data);
|
||||
} else {
|
||||
setErrorMessage('An unexpected error occurred.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
bgcolor="background.paper"
|
||||
boxShadow={(theme) => theme.shadows[3]}
|
||||
height={560}
|
||||
width={{ md: 960 }}
|
||||
>
|
||||
<Stack width={{ md: 0.5 }} m={2.5} gap={10}>
|
||||
<Link href="/" height="fit-content">
|
||||
<Image src={logo} width={82.6} />
|
||||
</Link>
|
||||
<Stack alignItems="center" gap={2.5} width={330} mx="auto">
|
||||
<Typography variant="h3">Verify Account</Typography>
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: initialEmail,
|
||||
code: '',
|
||||
}}
|
||||
onSubmit={handleVerify}
|
||||
enableReinitialize
|
||||
>
|
||||
{({ setFieldValue, values }) => (
|
||||
<Form>
|
||||
<FormControl variant="standard" fullWidth>
|
||||
{successMessage ? (
|
||||
<Alert severity="success" sx={{ mb: 2 }}>
|
||||
<Typography>{successMessage}</Typography>
|
||||
</Alert>
|
||||
) : null}
|
||||
{errorMessage ? (
|
||||
<Alert severity="error">
|
||||
{errorMessage.detail ? (
|
||||
<Typography>{errorMessage.detail}</Typography>
|
||||
) : (
|
||||
<ul>
|
||||
{Object.entries(errorMessage).map(([fieldName, errorMessages]) => (
|
||||
<li key={fieldName}>
|
||||
<strong>{fieldName}</strong>
|
||||
{Array.isArray(errorMessages) ? (
|
||||
<ul>
|
||||
{errorMessages.map((message, index) => (
|
||||
<li key={`${fieldName}-${index}`}>{message}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<span>: {String(errorMessages)}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Alert>
|
||||
) : null}
|
||||
<InputLabel shrink htmlFor="email">
|
||||
Email
|
||||
</InputLabel>
|
||||
<TextField
|
||||
variant="filled"
|
||||
placeholder="Enter your email"
|
||||
id="email"
|
||||
value={values.email}
|
||||
onChange={(event) => setFieldValue('email', event.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconifyIcon icon="ic:baseline-email" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl variant="standard" fullWidth sx={{ mt: 2 }}>
|
||||
<InputLabel shrink htmlFor="code">
|
||||
Verification Code
|
||||
</InputLabel>
|
||||
<TextField
|
||||
variant="filled"
|
||||
placeholder="Enter verification code"
|
||||
id="code"
|
||||
value={values.code}
|
||||
onChange={(event) => setFieldValue('code', event.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconifyIcon icon="ic:baseline-lock" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<Button variant="contained" type={'submit'} fullWidth sx={{ mt: 3 }}>
|
||||
Verify
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
fullWidth
|
||||
sx={{ mt: 1 }}
|
||||
onClick={async () => {
|
||||
setErrorMessage(null);
|
||||
setSuccessMessage(null);
|
||||
if (!values.email) {
|
||||
setErrorMessage({ detail: 'Email is required to resend code.' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await axiosInstance.post('/resend-registration-email/', {
|
||||
email: values.email,
|
||||
});
|
||||
setSuccessMessage('Registration email sent.');
|
||||
} catch (error) {
|
||||
setErrorMessage({ detail: 'There was an error sending the verification link.' });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Resend Verification Code
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Back to{' '}
|
||||
<Link
|
||||
href={paths.login}
|
||||
underline="hover"
|
||||
fontSize={(theme) => theme.typography.body1.fontSize}
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Suspense
|
||||
fallback={
|
||||
<Skeleton variant="rectangular" height={1} width={1} sx={{ bgcolor: 'primary.main' }} />
|
||||
}
|
||||
>
|
||||
<Image
|
||||
alt="Login banner"
|
||||
src={loginBanner}
|
||||
sx={{
|
||||
width: 0.5,
|
||||
display: { xs: 'none', md: 'block' },
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyAccount;
|
||||
@@ -29,6 +29,7 @@ export default {
|
||||
home: `/${rootPaths.homeRoot}`,
|
||||
dashboard: `/${rootPaths.dashboardRoot}`,
|
||||
login: `/${rootPaths.authRoot}/login`,
|
||||
verifyAccount: `/${rootPaths.authRoot}/verify-account`,
|
||||
signup: `/${rootPaths.authRoot}/sign-up`,
|
||||
resetPassword: `/${rootPaths.authRoot}/reset-password`,
|
||||
forgotPassword: `/${rootPaths.authRoot}/forgot-password`,
|
||||
|
||||
@@ -76,6 +76,7 @@ const Error404 = lazy(async () => {
|
||||
});
|
||||
|
||||
const Login = lazy(async () => import('pages/authentication/Login'));
|
||||
const VerifyAccount = lazy(async () => import('pages/authentication/VerifyAccount'));
|
||||
const SignUp = lazy(async () => import('pages/authentication/SignUp'));
|
||||
|
||||
const ResetPassword = lazy(async () => import('pages/authentication/ResetPassword'));
|
||||
@@ -179,6 +180,10 @@ const routes: RouteObject[] = [
|
||||
path: paths.login,
|
||||
element: <Login />,
|
||||
},
|
||||
{
|
||||
path: paths.verifyAccount,
|
||||
element: <VerifyAccount />,
|
||||
},
|
||||
{
|
||||
path: paths.signup,
|
||||
element: <SignUp />,
|
||||
|
||||
1
ditch-the-agent/src/setupTests.ts
Normal file
1
ditch-the-agent/src/setupTests.ts
Normal file
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -786,5 +786,8 @@ export interface SupportCaseApi {
|
||||
messages: SupportMessageApi[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
user_email: string;
|
||||
user_first_name: string;
|
||||
user_last_name: string;
|
||||
}
|
||||
// Walk Score API Type Definitions
|
||||
|
||||
@@ -16,4 +16,9 @@ export default defineConfig({
|
||||
port: 3000,
|
||||
},
|
||||
base: '/',
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: './src/setupTests.ts',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user