From c3f1c5b6a822ed07c0a9b1a30b5abe26cc531a02 Mon Sep 17 00:00:00 2001 From: Mengisteab Date: Fri, 12 Jun 2026 06:01:23 +0000 Subject: [PATCH] forgot password implementation --- .claude/settings.json | 3 +- .../auth/pages/ForgotPasswordPage.tsx | 188 ++++++++++++++++++ .../src/app/features/auth/pages/LoginPage.tsx | 3 +- apps/portal/src/app/router.tsx | 2 + 4 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 apps/portal/src/app/features/auth/pages/ForgotPasswordPage.tsx diff --git a/.claude/settings.json b/.claude/settings.json index 499ac81dc..9b056b847 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -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: $?\")" ] } } diff --git a/apps/portal/src/app/features/auth/pages/ForgotPasswordPage.tsx b/apps/portal/src/app/features/auth/pages/ForgotPasswordPage.tsx new file mode 100644 index 000000000..08aa7394f --- /dev/null +++ b/apps/portal/src/app/features/auth/pages/ForgotPasswordPage.tsx @@ -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; + +export function ForgotPasswordPage() { + const [forgotTrigger, { isLoading }] = useApiMutation(); + const [sentTo, setSentTo] = useState(null); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + 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 = ( +
+ + + Back to sign in + +
+ ); + + if (sentTo) { + return ( + + + + + + +
+ + Check your email + + + We've sent a password reset link to{' '} + + {sentTo} + + . Follow the link in that email to choose a new password. + +
+ + + + + + Didn't get the email? + + + Resend link + + + + {backToSignIn} +
+
+ ); + } + + return ( + + +
+ + Forgot your password? + + + Enter the email linked to your account and we'll send you a link + to reset your password. + +
+ +
+ + } + error={errors.email?.message} + {...register('email')} + /> + + + +
+ + {backToSignIn} +
+
+ ); +} diff --git a/apps/portal/src/app/features/auth/pages/LoginPage.tsx b/apps/portal/src/app/features/auth/pages/LoginPage.tsx index 0db2de1d3..eb5d0e907 100644 --- a/apps/portal/src/app/features/auth/pages/LoginPage.tsx +++ b/apps/portal/src/app/features/auth/pages/LoginPage.tsx @@ -120,9 +120,10 @@ export function LoginPage() { onChange={(e) => setRememberMe(e.currentTarget.checked)} /> notify.info('Password reset is coming soon.')} > Forgot password? diff --git a/apps/portal/src/app/router.tsx b/apps/portal/src/app/router.tsx index fea4203de..d89c41f57 100644 --- a/apps/portal/src/app/router.tsx +++ b/apps/portal/src/app/router.tsx @@ -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: }, { path: '/signup', element: }, { path: '/otp-verify', element: }, + { path: '/forgot-password', element: }, // Portal (open access for this UI template). // Wrapped in the portal's own i18n instance so it is isolated from the