forgot password implementation

This commit is contained in:
Mengisteab
2026-06-12 06:01:23 +00:00
parent 7de485ce28
commit c3f1c5b6a8
4 changed files with 194 additions and 2 deletions

View File

@@ -15,7 +15,8 @@
"Bash(awk '/profile: \\\\{/,/^ \\\\},/' src/app/i18n/locales/am.ts)",
"Bash(echo \"=== total errors: $\\(node_modules/.bin/tsc --noEmit -p apps/portal/tsconfig.json 2>&1)",
"Bash(node -e \"const p=require\\('./package.json'\\); console.log\\(JSON.stringify\\(p.scripts,null,2\\)\\)\")",
"Bash(node -e \"let s='';process.stdin.on\\('data',d=>s+=d\\).on\\('end',\\(\\)=>{try{console.log\\(Object.keys\\(JSON.parse\\(s\\).targets||{}\\)\\)}catch\\(e\\){console.log\\('no project.json'\\)}}\\)\")"
"Bash(node -e \"let s='';process.stdin.on\\('data',d=>s+=d\\).on\\('end',\\(\\)=>{try{console.log\\(Object.keys\\(JSON.parse\\(s\\).targets||{}\\)\\)}catch\\(e\\){console.log\\('no project.json'\\)}}\\)\")",
"Bash(echo \"EXIT: $?\")"
]
}
}

View File

@@ -0,0 +1,188 @@
import { useState } from 'react';
import {
Anchor,
Button,
Center,
Group,
Stack,
Text,
TextInput,
ThemeIcon,
Title,
} from '@mantine/core';
import {
IconArrowLeft,
IconMail,
IconMailForward,
IconSend,
} from '@tabler/icons-react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Link } from 'react-router-dom';
import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
import { AuthShell } from '../components/AuthShell';
const schema = z.object({
email: z.string().email({ message: 'Enter a valid email' }),
});
type FormValues = z.infer<typeof schema>;
export function ForgotPasswordPage() {
const [forgotTrigger, { isLoading }] = useApiMutation();
const [sentTo, setSentTo] = useState<string | null>(null);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
});
const sendResetLink = async (email: string) => {
await forgotTrigger({
url: '/auth/forgot-password',
method: 'POST',
body: { email },
}).unwrap();
setSentTo(email);
};
const onSubmit = async (values: FormValues) => {
try {
await sendResetLink(values.email);
} catch (err) {
const msg = err instanceof Error ? err.message : 'Something went wrong';
notify.error(msg);
}
};
const handleResend = async () => {
if (!sentTo || isLoading) return;
try {
await sendResetLink(sentTo);
notify.success('Reset link sent again');
} catch (err) {
const msg = err instanceof Error ? err.message : 'Something went wrong';
notify.error(msg);
}
};
const backToSignIn = (
<Center>
<Anchor
component={Link}
to="/login"
size="sm"
fw={600}
style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}
>
<IconArrowLeft size={16} />
Back to sign in
</Anchor>
</Center>
);
if (sentTo) {
return (
<AuthShell
brandTitle="Reset your password securely."
brandSubtitle="We'll email you a secure link to set a new password and get you back into your licensing portal."
>
<Stack gap="lg">
<ThemeIcon size={56} radius="md" variant="light" color="emaPrimary">
<IconMailForward size={30} />
</ThemeIcon>
<div>
<Title order={2} fz={30}>
Check your email
</Title>
<Text c="dimmed" mt={6}>
We&apos;ve sent a password reset link to{' '}
<Text span fw={600} c="dark">
{sentTo}
</Text>
. Follow the link in that email to choose a new password.
</Text>
</div>
<Button
component="a"
href="https://mail.google.com"
target="_blank"
rel="noopener noreferrer"
fullWidth
size="md"
leftSection={<IconMail size={18} />}
>
Open email app
</Button>
<Group justify="center" gap={6}>
<Text size="sm" c="dimmed">
Didn&apos;t get the email?
</Text>
<Anchor
size="sm"
fw={600}
onClick={handleResend}
style={isLoading ? { pointerEvents: 'none', opacity: 0.6 } : undefined}
>
Resend link
</Anchor>
</Group>
{backToSignIn}
</Stack>
</AuthShell>
);
}
return (
<AuthShell
brandTitle="Reset your password securely."
brandSubtitle="We'll email you a secure link to set a new password and get you back into your licensing portal."
>
<Stack gap="lg">
<div>
<Title order={2} fz={30}>
Forgot your password?
</Title>
<Text c="dimmed" mt={6}>
Enter the email linked to your account and we&apos;ll send you a link
to reset your password.
</Text>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
<TextInput
label="Email address"
placeholder="you@example.com"
size="md"
leftSection={<IconMail size={18} />}
error={errors.email?.message}
{...register('email')}
/>
<Button
type="submit"
loading={isLoading}
fullWidth
size="md"
rightSection={<IconSend size={18} />}
>
Send reset link
</Button>
</Stack>
</form>
{backToSignIn}
</Stack>
</AuthShell>
);
}

View File

@@ -120,9 +120,10 @@ export function LoginPage() {
onChange={(e) => setRememberMe(e.currentTarget.checked)}
/>
<Anchor
component={Link}
to="/forgot-password"
size="sm"
fw={600}
onClick={() => notify.info('Password reset is coming soon.')}
>
Forgot password?
</Anchor>

View File

@@ -8,6 +8,7 @@ import { PortalLayout } from './layouts/PortalLayout';
import { LoginPage } from './features/auth/pages/LoginPage';
import { SignupPage } from './features/auth/pages/SignupPage';
import { OTPVerificationPage } from './features/auth/pages/OTPVerificationPage';
import { ForgotPasswordPage } from './features/auth/pages/ForgotPasswordPage';
// Portal feature pages
import { DashboardPage } from './features/dashboard/pages/DashboardPage';
@@ -43,6 +44,7 @@ export const router = createBrowserRouter([
{ path: '/login', element: <LoginPage /> },
{ path: '/signup', element: <SignupPage /> },
{ path: '/otp-verify', element: <OTPVerificationPage /> },
{ path: '/forgot-password', element: <ForgotPasswordPage /> },
// Portal (open access for this UI template).
// Wrapped in the portal's own i18n instance so it is isolated from the