diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index e902eff..963c54b 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -60,10 +60,8 @@ apiClient.interceptors.response.use( isRefreshing = true const { refreshToken, setTokens, logout } = useAuthStore.getState() - if (!refreshToken) { logout() - window.location.href = '/login' return Promise.reject(error) } @@ -78,7 +76,6 @@ apiClient.interceptors.response.use( } catch (refreshError) { processQueue(refreshError, null) logout() - window.location.href = '/login' return Promise.reject(refreshError) } finally { isRefreshing = false diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 7c42d8c..03caf97 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -9,6 +9,53 @@ import { authApi } from '../api/client' import { useAuthStore } from '../store/authStore' import type { User } from '../types' +/** Extract a human-readable error message from an Axios error. */ +function getErrorMessage(err: unknown): string { + // Network error — no response at all (server unreachable, CORS, DNS failure) + if (err instanceof Error && err.message === 'Network Error') { + return 'Unable to connect to the server. Please check your network connection and try again.' + } + + // Axios-style error with a response body + const axiosErr = err as { response?: { status?: number; data?: { error?: { code?: string; message?: string } } } } + const status = axiosErr.response?.status + const code = axiosErr.response?.data?.error?.code + const msg = axiosErr.response?.data?.error?.message + + // Rate limited + if (status === 429) { + return 'Too many login attempts. Please wait a moment and try again.' + } + + // MFA required + if (code === 'mfa_required') { + return 'MFA_REQUIRED' // sentinel — caller checks this + } + + // Account disabled + if (code === 'account_disabled') { + return 'This account has been disabled. Contact your administrator.' + } + + // Server-provided message + if (msg) { + return msg + } + + // Generic status-based messages + if (status === 401) { + return 'Invalid username or password.' + } + if (status === 403) { + return 'Access denied.' + } + if (status && status >= 500) { + return 'A server error occurred. Please try again later.' + } + + return 'Login failed. Please try again.' +} + export default function LoginPage() { const navigate = useNavigate() const { setTokens, setUser } = useAuthStore() @@ -33,13 +80,12 @@ export default function LoginPage() { setUser(user as User) navigate('/dashboard', { replace: true }) } catch (err: unknown) { - const e = err as { response?: { data?: { error?: { code?: string; message?: string } } } } - const code = e.response?.data?.error?.code - if (code === 'mfa_required') { + const message = getErrorMessage(err) + if (message === 'MFA_REQUIRED') { setNeedsMfa(true) setError('Please enter your MFA code.') } else { - setError(e.response?.data?.error?.message || 'Login failed') + setError(message) } } finally { setLoading(false) @@ -50,16 +96,24 @@ export default function LoginPage() { - Linux Patch Manager + 🐉 Linux Patch Manager - {error && {error}} + {error && ( + setError(null)} + > + {error} + + )} setUsername(e.target.value)} - disabled={loading} required + disabled={loading} required autoFocus />