Merge pull request #562 from Tria-plc/alpha

feat: ( forget password ) add  phone number
This commit is contained in:
Abubeker Yasin
2026-07-09 09:39:40 +03:00
committed by GitHub
4 changed files with 38 additions and 34 deletions

View File

@@ -29,12 +29,12 @@ export default function LoginPage() {
const [emailFocused, setEmailFocused] = useState(false); const [emailFocused, setEmailFocused] = useState(false);
const [passwordFocused, setPasswordFocused] = useState(false); const [passwordFocused, setPasswordFocused] = useState(false);
const [view, setView] = useState<'login' | 'forgot'>('login'); const [view, setView] = useState<'login' | 'forgot'>('login');
const [forgotEmail, setForgotEmail] = useState(''); const [forgotIdentifier, setForgotIdentifier] = useState('');
const [forgotLoading, setForgotLoading] = useState(false); const [forgotLoading, setForgotLoading] = useState(false);
const [forgotError, setForgotError] = useState(''); const [forgotError, setForgotError] = useState('');
const [forgotSent, setForgotSent] = useState(false); const [forgotSent, setForgotSent] = useState(false);
const [forgotFocused, setForgotFocused] = useState(false); const [forgotFocused, setForgotFocused] = useState(false);
const router = useRouter(); const router = useRouter();
const { login } = useAuthStore(); const { login } = useAuthStore();
@@ -66,13 +66,13 @@ export default function LoginPage() {
setForgotLoading(true); setForgotLoading(true);
setForgotError(''); setForgotError('');
try { try {
await iamAuthApi.forgotPassword(forgotEmail); await iamAuthApi.forgotPassword(forgotIdentifier.trim());
setForgotSent(true); setForgotSent(true);
} catch (err: any) { } catch (err: any) {
const msg = err.response?.data?.message || err.message || ''; const msg = err.response?.data?.message || err.message || '';
setForgotError( setForgotError(
msg === 'user_not_found' msg === 'user_not_found'
? 'No account found with that email address.' ? 'No account found with that email or phone number.'
: msg || 'Failed to send the reset link. Please try again.' : msg || 'Failed to send the reset link. Please try again.'
); );
} finally { } finally {
@@ -209,7 +209,7 @@ export default function LoginPage() {
<div className="flex justify-end mt-2"> <div className="flex justify-end mt-2">
<button <button
type="button" type="button"
onClick={() => { setView('forgot'); setForgotEmail(email); setError(''); }} onClick={() => { setView('forgot'); setForgotIdentifier(email); setError(''); }}
className="text-xs font-medium text-[rgb(20,113,76)] hover:underline" className="text-xs font-medium text-[rgb(20,113,76)] hover:underline"
> >
Forgot password? Forgot password?
@@ -260,7 +260,7 @@ export default function LoginPage() {
Reset your password Reset your password
</h2> </h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1"> <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Enter your email address and we&apos;ll send a reset link to the phone number on your account. Enter your email or phone number and we&apos;ll send a reset link to the phone number on your account.
</p> </p>
</div> </div>
@@ -295,10 +295,10 @@ export default function LoginPage() {
)} )}
<form onSubmit={handleForgotSubmit} className="space-y-4 animate-fade-up" style={{ animationDelay: '80ms' }}> <form onSubmit={handleForgotSubmit} className="space-y-4 animate-fade-up" style={{ animationDelay: '80ms' }}>
{/* Email field */} {/* Email or phone field */}
<div> <div>
<label className="block text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-2"> <label className="block text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-2">
Email address Email or phone number
</label> </label>
<div className={`relative rounded-xl transition-all duration-200 ${ <div className={`relative rounded-xl transition-all duration-200 ${
forgotFocused forgotFocused
@@ -306,15 +306,15 @@ export default function LoginPage() {
: 'ring-1 ring-gray-200 dark:ring-gray-800' : 'ring-1 ring-gray-200 dark:ring-gray-800'
}`}> }`}>
<input <input
type="email" type="text"
value={forgotEmail} value={forgotIdentifier}
onChange={(e) => { setForgotEmail(e.target.value); setForgotError(''); }} onChange={(e) => { setForgotIdentifier(e.target.value); setForgotError(''); }}
onFocus={() => setForgotFocused(true)} onFocus={() => setForgotFocused(true)}
onBlur={() => setForgotFocused(false)} onBlur={() => setForgotFocused(false)}
className="w-full px-4 py-3 rounded-xl bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-600 text-sm focus:outline-none" className="w-full px-4 py-3 rounded-xl bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-600 text-sm focus:outline-none"
placeholder="name@edr.com" placeholder="name@edr.com or +251..."
required required
autoComplete="email" autoComplete="username"
/> />
</div> </div>
</div> </div>
@@ -322,9 +322,9 @@ export default function LoginPage() {
{/* Submit */} {/* Submit */}
<button <button
type="submit" type="submit"
disabled={forgotLoading || !forgotEmail} disabled={forgotLoading || !forgotIdentifier.trim()}
className="group w-full mt-2 flex items-center justify-center gap-2 py-3 px-4 rounded-xl font-semibold text-sm text-white transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" className="group w-full mt-2 flex items-center justify-center gap-2 py-3 px-4 rounded-xl font-semibold text-sm text-white transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
style={{ background: forgotLoading || !forgotEmail style={{ background: forgotLoading || !forgotIdentifier.trim()
? 'rgb(20,113,76)' ? 'rgb(20,113,76)'
: `linear-gradient(135deg, rgb(20,113,76) 0%, rgb(16,143,96) 100%)` : `linear-gradient(135deg, rgb(20,113,76) 0%, rgb(16,143,96) 100%)`
}} }}

View File

@@ -4,8 +4,10 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
export const iamAuthApi = { export const iamAuthApi = {
forgotPassword: (email: string) => // `identifier` can be an email address or a phone number — the IAM accepts
axios.post(`${API_URL}/v1/auth/forgot-password`, { email }), // either in the `email` field of the forgot-password body.
forgotPassword: (identifier: string) =>
axios.post(`${API_URL}/v1/auth/forgot-password`, { email: identifier }),
resetPassword: (data: { resetPassword: (data: {
userId: string; userId: string;

View File

@@ -6,7 +6,7 @@ import { Train, MailCheck, ArrowLeft } from 'lucide-react';
import { iamAuthApi } from '@/lib/api/auth'; import { iamAuthApi } from '@/lib/api/auth';
export default function ForgotPasswordPage() { export default function ForgotPasswordPage() {
const [email, setEmail] = useState(''); const [identifier, setIdentifier] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [sent, setSent] = useState(false); const [sent, setSent] = useState(false);
@@ -16,13 +16,13 @@ export default function ForgotPasswordPage() {
setLoading(true); setLoading(true);
setError(''); setError('');
try { try {
await iamAuthApi.forgotPassword(email); await iamAuthApi.forgotPassword(identifier.trim());
setSent(true); setSent(true);
} catch (err: any) { } catch (err: any) {
const msg = err.response?.data?.message || err.message || ''; const msg = err.response?.data?.message || err.message || '';
setError( setError(
msg === 'user_not_found' msg === 'user_not_found'
? 'No account found with that email address.' ? 'No account found with that email or phone number.'
: msg || 'Failed to send the reset link. Please try again.' : msg || 'Failed to send the reset link. Please try again.'
); );
} finally { } finally {
@@ -41,7 +41,7 @@ export default function ForgotPasswordPage() {
</div> </div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Reset your password</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Reset your password</h1>
<p className="text-gray-600 dark:text-gray-400 mt-2"> <p className="text-gray-600 dark:text-gray-400 mt-2">
Enter your email and we&apos;ll send a reset link to the phone number on your account. Enter your email or phone number and we&apos;ll send a reset link to the phone number on your account.
</p> </p>
</div> </div>
@@ -69,19 +69,19 @@ export default function ForgotPasswordPage() {
)} )}
<div> <div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Email</label> <label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Email or phone number</label>
<input <input
type="email" type="text"
value={email} value={identifier}
onChange={(e) => { setEmail(e.target.value); setError(''); }} onChange={(e) => { setIdentifier(e.target.value); setError(''); }}
className="input-field" className="input-field"
placeholder="your@email.com" placeholder="your@email.com or +251..."
autoComplete="email" autoComplete="username"
required required
/> />
</div> </div>
<button type="submit" className="btn-primary w-full" disabled={loading || !email}> <button type="submit" className="btn-primary w-full" disabled={loading || !identifier.trim()}>
{loading ? 'Sending...' : 'Send reset link'} {loading ? 'Sending...' : 'Send reset link'}
</button> </button>
</form> </form>

View File

@@ -8,8 +8,10 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
// current password is wrong on change-password, and OTP failures must surface // current password is wrong on change-password, and OTP failures must surface
// as inline errors, not a logout. // as inline errors, not a logout.
export const iamAuthApi = { export const iamAuthApi = {
forgotPassword: (email: string) => // `identifier` can be an email address or a phone number — the IAM accepts
axios.post(`${API_URL}/v1/auth/forgot-password`, { email }), // either in the `email` field of the forgot-password body.
forgotPassword: (identifier: string) =>
axios.post(`${API_URL}/v1/auth/forgot-password`, { email: identifier }),
// Re-sends the registration verification code for a still-pending account. // Re-sends the registration verification code for a still-pending account.
resendRegistrationCode: (data: { email: string; phoneNumber: string }) => resendRegistrationCode: (data: { email: string; phoneNumber: string }) =>