mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
forgot password implementation
This commit is contained in:
@@ -15,7 +15,8 @@
|
|||||||
"Bash(awk '/profile: \\\\{/,/^ \\\\},/' src/app/i18n/locales/am.ts)",
|
"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(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 \"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: $?\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
188
apps/portal/src/app/features/auth/pages/ForgotPasswordPage.tsx
Normal file
188
apps/portal/src/app/features/auth/pages/ForgotPasswordPage.tsx
Normal 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'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'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'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -120,9 +120,10 @@ export function LoginPage() {
|
|||||||
onChange={(e) => setRememberMe(e.currentTarget.checked)}
|
onChange={(e) => setRememberMe(e.currentTarget.checked)}
|
||||||
/>
|
/>
|
||||||
<Anchor
|
<Anchor
|
||||||
|
component={Link}
|
||||||
|
to="/forgot-password"
|
||||||
size="sm"
|
size="sm"
|
||||||
fw={600}
|
fw={600}
|
||||||
onClick={() => notify.info('Password reset is coming soon.')}
|
|
||||||
>
|
>
|
||||||
Forgot password?
|
Forgot password?
|
||||||
</Anchor>
|
</Anchor>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { PortalLayout } from './layouts/PortalLayout';
|
|||||||
import { LoginPage } from './features/auth/pages/LoginPage';
|
import { LoginPage } from './features/auth/pages/LoginPage';
|
||||||
import { SignupPage } from './features/auth/pages/SignupPage';
|
import { SignupPage } from './features/auth/pages/SignupPage';
|
||||||
import { OTPVerificationPage } from './features/auth/pages/OTPVerificationPage';
|
import { OTPVerificationPage } from './features/auth/pages/OTPVerificationPage';
|
||||||
|
import { ForgotPasswordPage } from './features/auth/pages/ForgotPasswordPage';
|
||||||
|
|
||||||
// Portal feature pages
|
// Portal feature pages
|
||||||
import { DashboardPage } from './features/dashboard/pages/DashboardPage';
|
import { DashboardPage } from './features/dashboard/pages/DashboardPage';
|
||||||
@@ -43,6 +44,7 @@ export const router = createBrowserRouter([
|
|||||||
{ path: '/login', element: <LoginPage /> },
|
{ path: '/login', element: <LoginPage /> },
|
||||||
{ path: '/signup', element: <SignupPage /> },
|
{ path: '/signup', element: <SignupPage /> },
|
||||||
{ path: '/otp-verify', element: <OTPVerificationPage /> },
|
{ path: '/otp-verify', element: <OTPVerificationPage /> },
|
||||||
|
{ path: '/forgot-password', element: <ForgotPasswordPage /> },
|
||||||
|
|
||||||
// Portal (open access for this UI template).
|
// Portal (open access for this UI template).
|
||||||
// Wrapped in the portal's own i18n instance so it is isolated from the
|
// Wrapped in the portal's own i18n instance so it is isolated from the
|
||||||
|
|||||||
Reference in New Issue
Block a user