fix: ( backoffice ) accept phone or username in the login identifier field

This commit is contained in:
Abubeker Yasin
2026-08-18 11:28:10 +03:00
parent a9eac93fa3
commit 33100a31ae
2 changed files with 27 additions and 20 deletions

View File

@@ -20,13 +20,15 @@ const features = [
]; ];
export default function LoginPage() { export default function LoginPage() {
const [email, setEmail] = useState(''); // Accepts an email address, phone number, or username — sent to the IAM in
// the `email` field either way (the backend contract does not change).
const [identifier, setIdentifier] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [isMounted, setIsMounted] = useState(false); const [isMounted, setIsMounted] = useState(false);
const [emailFocused, setEmailFocused] = useState(false); const [identifierFocused, setIdentifierFocused] = 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');
@@ -47,7 +49,7 @@ export default function LoginPage() {
setLoading(true); setLoading(true);
setError(''); setError('');
try { try {
await login(email, password); await login(identifier.trim(), password);
router.push('/dashboard'); router.push('/dashboard');
} catch (err: any) { } catch (err: any) {
const msg = err.message || err.response?.data?.message || ''; const msg = err.message || err.response?.data?.message || '';
@@ -152,26 +154,29 @@ export default function LoginPage() {
<form onSubmit={handleSubmit} className="space-y-4 animate-fade-up" style={{ animationDelay: '80ms' }}> <form onSubmit={handleSubmit} className="space-y-4 animate-fade-up" style={{ animationDelay: '80ms' }}>
{/* Email field */} {/* Identifier field — email, phone number, or username */}
<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, phone or username
</label> </label>
<div className={`relative rounded-xl transition-all duration-200 ${ <div className={`relative rounded-xl transition-all duration-200 ${
emailFocused identifierFocused
? 'ring-2 ring-[rgb(20,113,76)] ring-offset-0' ? 'ring-2 ring-[rgb(20,113,76)] ring-offset-0'
: '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={email} value={identifier}
onChange={(e) => { setEmail(e.target.value); setError(''); }} onChange={(e) => { setIdentifier(e.target.value); setError(''); }}
onFocus={() => setEmailFocused(true)} onFocus={() => setIdentifierFocused(true)}
onBlur={() => setEmailFocused(false)} onBlur={() => setIdentifierFocused(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, +251… or username"
required required
autoComplete="email" autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
autoComplete="username"
/> />
</div> </div>
</div> </div>
@@ -209,7 +214,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'); setForgotIdentifier(email); setError(''); }} onClick={() => { setView('forgot'); setForgotIdentifier(identifier); 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?
@@ -220,9 +225,9 @@ export default function LoginPage() {
{/* Submit */} {/* Submit */}
<button <button
type="submit" type="submit"
disabled={loading || !email || !password} disabled={loading || !identifier.trim() || !password}
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: loading || !email || !password style={{ background: loading || !identifier.trim() || !password
? '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

@@ -18,7 +18,8 @@ interface AuthState {
token: string | null; token: string | null;
refreshToken: string | null; refreshToken: string | null;
isAuthenticated: boolean; isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>; /** `identifier` may be an email, phone number or username — always sent as `email`. */
login: (identifier: string, password: string) => Promise<void>;
logout: () => void; logout: () => void;
setUser: (user: AdminUser, token: string) => void; setUser: (user: AdminUser, token: string) => void;
initialize: () => void; initialize: () => void;
@@ -52,9 +53,10 @@ export const useAuthStore = create<AuthState>((set, get) => ({
} }
}, },
login: async (email: string, password: string) => { login: async (identifier: string, password: string) => {
// Step 1: IAM login — returns token + refreshToken only // Step 1: IAM login — returns token + refreshToken only.
const loginRes = await axios.post(`${API_URL}/v1/auth/login`, { email, password }); // The IAM accepts an email, phone number or username in the `email` field.
const loginRes = await axios.post(`${API_URL}/v1/auth/login`, { email: identifier, password });
const loginData = loginRes.data?.data ?? loginRes.data; const loginData = loginRes.data?.data ?? loginRes.data;
const { token, refreshToken } = loginData; const { token, refreshToken } = loginData;
if (!token) throw new Error('No token received from server'); if (!token) throw new Error('No token received from server');