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() {
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 [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const [emailFocused, setEmailFocused] = useState(false);
const [identifierFocused, setIdentifierFocused] = useState(false);
const [passwordFocused, setPasswordFocused] = useState(false);
const [view, setView] = useState<'login' | 'forgot'>('login');
@@ -47,7 +49,7 @@ export default function LoginPage() {
setLoading(true);
setError('');
try {
await login(email, password);
await login(identifier.trim(), password);
router.push('/dashboard');
} catch (err: any) {
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' }}>
{/* Email field */}
{/* Identifier field — email, phone number, or username */}
<div>
<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>
<div className={`relative rounded-xl transition-all duration-200 ${
emailFocused
identifierFocused
? 'ring-2 ring-[rgb(20,113,76)] ring-offset-0'
: 'ring-1 ring-gray-200 dark:ring-gray-800'
}`}>
<input
type="email"
value={email}
onChange={(e) => { setEmail(e.target.value); setError(''); }}
onFocus={() => setEmailFocused(true)}
onBlur={() => setEmailFocused(false)}
type="text"
value={identifier}
onChange={(e) => { setIdentifier(e.target.value); setError(''); }}
onFocus={() => setIdentifierFocused(true)}
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"
placeholder="name@edr.com"
placeholder="name@edr.com, +251… or username"
required
autoComplete="email"
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
autoComplete="username"
/>
</div>
</div>
@@ -209,7 +214,7 @@ export default function LoginPage() {
<div className="flex justify-end mt-2">
<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"
>
Forgot password?
@@ -220,9 +225,9 @@ export default function LoginPage() {
{/* Submit */}
<button
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"
style={{ background: loading || !email || !password
style={{ background: loading || !identifier.trim() || !password
? 'rgb(20,113,76)'
: `linear-gradient(135deg, rgb(20,113,76) 0%, rgb(16,143,96) 100%)`
}}

View File

@@ -18,7 +18,8 @@ interface AuthState {
token: string | null;
refreshToken: string | null;
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;
setUser: (user: AdminUser, token: string) => void;
initialize: () => void;
@@ -52,9 +53,10 @@ export const useAuthStore = create<AuthState>((set, get) => ({
}
},
login: async (email: string, password: string) => {
// Step 1: IAM login — returns token + refreshToken only
const loginRes = await axios.post(`${API_URL}/v1/auth/login`, { email, password });
login: async (identifier: string, password: string) => {
// Step 1: IAM login — returns token + refreshToken only.
// 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 { token, refreshToken } = loginData;
if (!token) throw new Error('No token received from server');