feat(#416): add error display from URL query params on login page
Some checks failed
ci/woodpecker/push/web Pipeline failed

Maps error codes to PDA-friendly messages (no alarming language).
Dismissible error banner with URL param cleanup.

Refs #416

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-16 11:50:33 -06:00
parent 1d7d5a9d01
commit 077bb042b7
2 changed files with 133 additions and 3 deletions

View File

@@ -2,7 +2,7 @@
import { useEffect, useState, useCallback } from "react";
import type { ReactElement } from "react";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import { Loader2 } from "lucide-react";
import type { AuthConfigResponse, AuthProviderConfig } from "@mosaic/shared";
import { API_BASE_URL } from "@/lib/config";
@@ -17,13 +17,44 @@ const EMAIL_ONLY_CONFIG: AuthConfigResponse = {
providers: [{ id: "email", name: "Email", type: "credentials" }],
};
/** Maps URL error codes to PDA-friendly messages (no alarming language). */
const ERROR_CODE_MESSAGES: Record<string, string> = {
access_denied: "Authentication paused. Please try again when ready.",
invalid_credentials: "The email and password combination wasn't recognized.",
server_error: "The service is taking a break. Please try again in a moment.",
network_error: "Unable to connect. Check your network and try again.",
rate_limited: "You've tried a few times. Take a moment and try again shortly.",
session_expired: "Your session ended. Please sign in again when ready.",
};
const DEFAULT_ERROR_MESSAGE = "Authentication didn't complete. Please try again when ready.";
function mapErrorCodeToMessage(code: string): string {
return ERROR_CODE_MESSAGES[code] ?? DEFAULT_ERROR_MESSAGE;
}
export default function LoginPage(): ReactElement {
const router = useRouter();
const searchParams = useSearchParams();
const [config, setConfig] = useState<AuthConfigResponse | null>(null);
const [loadingConfig, setLoadingConfig] = useState(true);
const [oauthLoading, setOauthLoading] = useState<string | null>(null);
const [credentialsLoading, setCredentialsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [urlError, setUrlError] = useState<string | null>(null);
/* Read ?error= query param on mount and map to PDA-friendly message */
useEffect(() => {
const errorCode = searchParams.get("error");
if (errorCode) {
setUrlError(mapErrorCodeToMessage(errorCode));
// Clean up the URL by removing the error param without triggering navigation
const nextParams = new URLSearchParams(searchParams.toString());
nextParams.delete("error");
const query = nextParams.toString();
router.replace(query ? `/login?${query}` : "/login");
}
}, [searchParams, router]);
useEffect(() => {
let cancelled = false;
@@ -115,6 +146,15 @@ export default function LoginPage(): ReactElement {
</div>
) : (
<>
{urlError && (
<AuthErrorBanner
message={urlError}
onDismiss={(): void => {
setUrlError(null);
}}
/>
)}
{error && !hasCredentials && (
<AuthErrorBanner
message={error}