diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 5a92998..a32976a 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -333,19 +333,20 @@ async def login( await db.commit() raise HTTPException(status_code=401, detail="Invalid username or password") - if new_hash: - user.password_hash = new_hash - # Block disabled accounts — checked AFTER password verification to avoid # leaking account-state info, and BEFORE _record_successful_login so # last_login_at and lockout counters are not reset for inactive users. if not user.is_active: await log_audit_event( - db, action="auth.login_blocked_inactive", actor_id=user.id, ip=client_ip, + db, action="auth.login_blocked_inactive", actor_id=user.id, + detail={"reason": "account_disabled"}, ip=client_ip, ) await db.commit() raise HTTPException(status_code=403, detail="Account is disabled. Contact an administrator.") + if new_hash: + user.password_hash = new_hash + await _record_successful_login(db, user) # SEC-03: MFA enforcement — block login entirely until MFA setup completes diff --git a/frontend/src/components/auth/LockScreen.tsx b/frontend/src/components/auth/LockScreen.tsx index eb9cc1c..95c9e5e 100644 --- a/frontend/src/components/auth/LockScreen.tsx +++ b/frontend/src/components/auth/LockScreen.tsx @@ -641,6 +641,7 @@ export default function LockScreen() { setUsername(''); setPassword(''); setConfirmPassword(''); + setLoginError(null); }} className="text-xs text-muted-foreground hover:text-foreground transition-colors" > diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index 3d2882e..7c181f5 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -36,10 +36,10 @@ export function useAuth() { setMfaSetupRequired(false); // Optimistically mark authenticated to prevent form flash during refetch if ('authenticated' in data && data.authenticated && !('must_change_password' in data && data.must_change_password)) { - queryClient.setQueryData(['auth'], (old: AuthStatus | undefined) => ({ - ...old!, - authenticated: true, - })); + queryClient.setQueryData(['auth'], (old: AuthStatus | undefined) => { + if (!old) return old; // let invalidateQueries handle it + return { ...old, authenticated: true }; + }); } queryClient.invalidateQueries({ queryKey: ['auth'] }); } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index acf70c0..7ff73d2 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -15,7 +15,8 @@ api.interceptors.response.use( if (error.response?.status === 401) { const url = error.config?.url || ''; // Don't redirect on auth endpoints — they legitimately return 401 - if (!url.startsWith('/auth/')) { + const authEndpoints = ['/auth/login', '/auth/register', '/auth/setup', '/auth/verify-password', '/auth/change-password']; + if (!authEndpoints.some(ep => url.startsWith(ep))) { window.location.href = '/login'; } }