diff --git a/package-lock.json b/package-lock.json index a2122c3..64d7ffa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ciscode/ui-authentication-kit", - "version": "1.0.12", + "version": "1.0.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ciscode/ui-authentication-kit", - "version": "1.0.12", + "version": "1.0.13", "license": "ISC", "devDependencies": { "@testing-library/jest-dom": "^6.9.1", diff --git a/package.json b/package.json index fea2c9e..9d984e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ciscode/ui-authentication-kit", - "version": "1.0.12", + "version": "1.0.13", "description": "", "main": "dist/index.umd.js", "module": "dist/index.mjs", diff --git a/src/pages/auth/ForgotPasswordPage.tsx b/src/pages/auth/ForgotPasswordPage.tsx index 32537c8..4e54b21 100644 --- a/src/pages/auth/ForgotPasswordPage.tsx +++ b/src/pages/auth/ForgotPasswordPage.tsx @@ -6,6 +6,7 @@ import { InlineError } from "../../components/InlineError"; import { useAuthConfig } from "../../context/AuthConfigContext"; import { useAuthState } from "../../context/AuthStateContext"; import { toTailwindColorClasses } from "../../utils/colorHelpers"; +import { extractHttpErrorMessage } from "../../utils/errorHelpers"; export const ForgotPasswordPage: React.FC = () => { const t = useT("authLib"); @@ -31,9 +32,10 @@ export const ForgotPasswordPage: React.FC = () => { // Always show generic success regardless of user existence setSent(true); } catch (err) { - // Do not enumerate users; still show success message - setSent(true); - console.error("Forgot password request failed", err); + // Show backend error details.message via InlineError + const msg = extractHttpErrorMessage(err); + setError(msg); + setSent(false); } finally { setPending(false); } diff --git a/src/pages/auth/ResetPasswordPage.tsx b/src/pages/auth/ResetPasswordPage.tsx index cf0df5e..32b4566 100644 --- a/src/pages/auth/ResetPasswordPage.tsx +++ b/src/pages/auth/ResetPasswordPage.tsx @@ -6,6 +6,7 @@ import { InlineError } from "../../components/InlineError"; import { useAuthConfig } from "../../context/AuthConfigContext"; import { useAuthState } from "../../context/AuthStateContext"; import { toTailwindColorClasses } from "../../utils/colorHelpers"; +import { extractHttpErrorMessage } from "../../utils/errorHelpers"; export const ResetPasswordPage: React.FC = () => { const t = useT("authLib"); @@ -53,16 +54,8 @@ export const ResetPasswordPage: React.FC = () => { // On success, show brief confirmation then navigate to login navigate("/login", { replace: true }); } catch (err: any) { - const status = err?.response?.status; - if (status === 400 || status === 401 || status === 410) { - setError( - t("ResetPasswordPage.invalidOrExpired", { - defaultValue: "Reset link is invalid or has expired. Request a new one.", - }) - ); - } else { - setError(t("errors.generic", { defaultValue: "Something went wrong. Please try again." })); - } + const msg = extractHttpErrorMessage(err); + setError(msg); } finally { setPending(false); } diff --git a/src/pages/auth/SignInPage.tsx b/src/pages/auth/SignInPage.tsx index 6fb6f8b..c0817d7 100644 --- a/src/pages/auth/SignInPage.tsx +++ b/src/pages/auth/SignInPage.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { InputField } from "../../components/actions/InputField"; import { SocialButton } from "../../components/actions/SocialButton"; import googleIcon from "../../assets/icons/google-icon-svgrepo-com.svg"; @@ -7,6 +7,7 @@ import { toTailwindColorClasses } from "../../utils/colorHelpers"; import { useAuthConfig } from "../../context/AuthConfigContext"; import { useAuthState } from "../../context/AuthStateContext"; import { InlineError } from "../../components/InlineError"; +import { extractHttpErrorMessage } from "../../utils/errorHelpers"; import { AuthConfigProps } from "../../models/AuthConfig"; import { useT } from "@ciscode/ui-translate-core"; import { useNavigate, useLocation } from "react-router-dom"; @@ -39,6 +40,14 @@ export const SignInPage: React.FC = () => { const [password, setPassword] = useState(""); const [pending, setPending] = useState(false); const [error, setError] = useState(null); + // Show any provider-level error surfaced by the interceptor (e.g., refresh failures) + useEffect(() => { + const msg = sessionStorage.getItem('authErrorMessage'); + if (msg) { + setError(msg); + sessionStorage.removeItem('authErrorMessage'); + } + }, []); const allProvidersData = { google: { icon: googleIcon, label: t("social.google") }, @@ -64,11 +73,8 @@ export const SignInPage: React.FC = () => { try { await login({ email, password }); } catch (err: any) { - if (err?.response?.status === 401) { - setError(t("errors.invalidCredentials")); - } else { - setError(t("errors.generic")); - } + const msg = extractHttpErrorMessage(err); + setError(msg); } finally { setPending(false); } diff --git a/src/pages/auth/SignUpPage.tsx b/src/pages/auth/SignUpPage.tsx index 429905b..d61e3d9 100644 --- a/src/pages/auth/SignUpPage.tsx +++ b/src/pages/auth/SignUpPage.tsx @@ -7,6 +7,7 @@ import { toTailwindColorClasses } from "../../utils/colorHelpers"; import { useAuthConfig } from "../../context/AuthConfigContext"; import { useAuthState } from "../../context/AuthStateContext"; import { InlineError } from "../../components/InlineError"; +import { extractHttpErrorMessage } from "../../utils/errorHelpers"; import { useT } from "@ciscode/ui-translate-core"; import { useNavigate, useLocation } from "react-router-dom"; @@ -83,27 +84,8 @@ export const SignUpPage: React.FC = () => { navigate(`/verify-email?email=${encodeURIComponent(email)}`, { replace: true }); return; } catch (err: any) { - const status = err?.response?.status; - if (status === 400) { - setError( - err?.response?.data?.message || - t("errors.invalidData", { - defaultValue: "Please check the fields and try again.", - }) - ); - } else if (status === 409) { - setError( - t("errors.emailInUse", { - defaultValue: "This email is already in use.", - }) - ); - } else { - setError( - t("errors.generic", { - defaultValue: "Something went wrong. Please try again.", - }) - ); - } + const msg = extractHttpErrorMessage(err); + setError(msg); } finally { setPending(false); } diff --git a/src/utils/attachAuthInterceptor.ts b/src/utils/attachAuthInterceptor.ts index 4415336..a7191c7 100644 --- a/src/utils/attachAuthInterceptor.ts +++ b/src/utils/attachAuthInterceptor.ts @@ -4,6 +4,7 @@ import axios, { AxiosRequestConfig, InternalAxiosRequestConfig, } from 'axios'; +import { extractHttpErrorMessage } from './errorHelpers'; interface Options { baseUrl: string; // e.g. https://api.myapp.com @@ -60,6 +61,14 @@ export function attachAuthInterceptor(api: AxiosInstance, opts: Options) { opts.logout(); // 🔔 open modal, keep token for now } + // Surface detailed error message for UI to display on login page + try { + const msg = extractHttpErrorMessage(refreshErr); + if (msg) { + sessionStorage.setItem('authErrorMessage', msg); + } + } catch { /* ignore storage errors */ } + queue.forEach(cb => cb(null)); queue = []; return Promise.reject(refreshErr); diff --git a/src/utils/errorHelpers.ts b/src/utils/errorHelpers.ts new file mode 100644 index 0000000..773f0a7 --- /dev/null +++ b/src/utils/errorHelpers.ts @@ -0,0 +1,47 @@ +import type { AxiosError } from 'axios'; + +type ErrorResponse = { + ok?: boolean; + code?: string; + message?: string; + requestId?: string; + path?: string; + details?: { + message?: string; + error?: string; + statusCode?: number; + }; +}; + +function isAxiosError(err: unknown): err is AxiosError { + return !!(err as any)?.isAxiosError; +} + +/** + * Extract a user-facing error message from common backend/HTTP shapes. + * Preference order: + * 1) response.data.details.message + * 2) response.data.message + * 3) response.data.details.error + * 4) err.message + * 5) generic fallback + */ +export function extractHttpErrorMessage(err: unknown): string { + try { + if (typeof err === 'string') return err; + + if (isAxiosError(err)) { + const data = err.response?.data as ErrorResponse | undefined; + const fromResponse = + data?.details?.message?.toString()?.trim() || + data?.message?.toString()?.trim() || + data?.details?.error?.toString()?.trim(); + if (fromResponse) return fromResponse; + if (err.message) return err.message; + } + + if (err instanceof Error && err.message) return err.message; + } catch {/* ignore parsing issues */} + + return 'An unexpected error occurred'; +}