feat: improve account lockout UX with severity-aware error styling

Login errors now distinguish between wrong-password (red), progressive
lockout warnings (amber, Lock icon), and temporary lockout (amber, Lock
icon) based on the backend detail string. Removes the dead 423 branch
from handleCredentialSubmit — account lockout is now returned as 401
with a descriptive detail message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-18 01:00:42 +08:00
parent 1b868ba503
commit 863e9e2c45

View File

@ -155,11 +155,10 @@ export default function LockScreen() {
// mfaSetupRequired / mfaRequired handled by hook state → activeMode switches automatically // mfaSetupRequired / mfaRequired handled by hook state → activeMode switches automatically
} catch (error: any) { } catch (error: any) {
const status = error?.response?.status; const status = error?.response?.status;
if (status === 423) { if (status === 403) {
setLoginError(error.response.data?.detail || 'Account locked. Try again later.');
} else if (status === 403) {
setLoginError(error.response.data?.detail || 'Account is disabled. Contact an administrator.'); setLoginError(error.response.data?.detail || 'Account is disabled. Contact an administrator.');
} else { } else {
// 401 covers both wrong password and account lockout (backend embeds detail string)
setLoginError(getErrorMessage(error, 'Invalid username or password')); setLoginError(getErrorMessage(error, 'Invalid username or password'));
} }
} }
@ -519,18 +518,28 @@ export default function LockScreen() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{loginError && ( {loginError && (() => {
const isLockWarning =
loginError.includes('remaining') || loginError.includes('temporarily locked');
return (
<div <div
role="alert" role="alert"
className={cn( className={cn(
'flex items-center gap-2 rounded-md border border-red-500/30', 'flex items-center gap-2 rounded-md border px-3 py-2 mb-4',
'bg-red-500/10 px-3 py-2 mb-4' isLockWarning
? 'bg-amber-500/10 border-amber-500/30'
: 'bg-red-500/10 border-red-500/30'
)} )}
> >
<AlertTriangle className="h-4 w-4 text-red-400 shrink-0" aria-hidden="true" /> {isLockWarning
<p className="text-xs text-red-400">{loginError}</p> ? <Lock className="h-4 w-4 text-amber-400 shrink-0" aria-hidden="true" />
: <AlertTriangle className="h-4 w-4 text-red-400 shrink-0" aria-hidden="true" />}
<p className={cn('text-xs', isLockWarning ? 'text-amber-400' : 'text-red-400')}>
{loginError}
</p>
</div> </div>
)} );
})()}
<form onSubmit={handleCredentialSubmit} className="space-y-4"> <form onSubmit={handleCredentialSubmit} className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="username" required>Username</Label> <Label htmlFor="username" required>Username</Label>