diff --git a/ui/app/login/page.tsx b/ui/app/login/page.tsx index 9144c262..27b3aa30 100644 --- a/ui/app/login/page.tsx +++ b/ui/app/login/page.tsx @@ -5,7 +5,13 @@ import type { ChangeEvent, FormEvent, ReactElement } from 'react'; import { useRouter } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; import { adminLogin } from '@/lib/api/services/auth'; +import { + getApiErrorMessage, + isNodeUnreachable, + isUnauthorized, +} from '@/lib/api/errors'; import { ConfigurationService } from '@/lib/api/services/configuration'; import { toast } from 'sonner'; import { AuthPageShell } from '@/components/auth-page-shell'; @@ -16,6 +22,7 @@ export default function AdminLoginPage(): ReactElement { const [password, setPassword] = useState(''); const [baseUrl, setBaseUrl] = useState(''); const [isLoading, setIsLoading] = useState(false); + const [loginError, setLoginError] = useState(''); useEffect(() => { if (ConfigurationService.isTokenValid()) { @@ -43,18 +50,22 @@ export default function AdminLoginPage(): ReactElement { event: FormEvent ): Promise => { event.preventDefault(); + setLoginError(''); + // The login request itself must target the entered URL, so it is written + // before the attempt and rolled back if the node was unreachable. + const previousBaseUrl = ConfigurationService.getManualBaseUrl(); if (allowCustomBaseUrl) { const normalizedBaseUrl = baseUrl.trim(); if (!normalizedBaseUrl) { - toast.error('Please enter the API URL'); + setLoginError('Please enter the API URL'); return; } ConfigurationService.setManualBaseUrl(normalizedBaseUrl); } if (!password) { - toast.error('Please enter your password'); + setLoginError('Please enter your password'); return; } @@ -65,7 +76,18 @@ export default function AdminLoginPage(): ReactElement { router.push('/'); } catch (error) { console.error('Login error:', error); - toast.error('Invalid password. Please try again.'); + if (isNodeUnreachable(error)) { + setLoginError( + `Can't reach your node at ${ConfigurationService.getLocalBaseUrl()}. Is it running?` + ); + if (allowCustomBaseUrl) { + ConfigurationService.setManualBaseUrl(previousBaseUrl); + } + } else if (isUnauthorized(error)) { + setLoginError('Incorrect password. Please try again.'); + } else { + setLoginError(getApiErrorMessage(error, 'Login failed')); + } } finally { setIsLoading(false); } @@ -79,9 +101,13 @@ export default function AdminLoginPage(): ReactElement {
{allowCustomBaseUrl && (
+ ) => setBaseUrl(event.target.value) @@ -92,9 +118,13 @@ export default function AdminLoginPage(): ReactElement {
)}
+ ) => setPassword(event.target.value) @@ -104,6 +134,11 @@ export default function AdminLoginPage(): ReactElement { required />
+ {loginError && ( +

+ {loginError} +

+ )} diff --git a/ui/lib/api/errors.ts b/ui/lib/api/errors.ts index de8e4629..b7d2f5c4 100644 --- a/ui/lib/api/errors.ts +++ b/ui/lib/api/errors.ts @@ -24,3 +24,11 @@ export function getApiErrorMessage( } return fallback; } + +export function isNodeUnreachable(error: unknown): boolean { + return isAxiosError(error) && !error.response; +} + +export function isUnauthorized(error: unknown): boolean { + return isAxiosError(error) && error.response?.status === 401; +}