From 4cabe0be0c247ffe001d9bc0e584ce483a3ded15 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Sun, 19 Jul 2026 18:12:48 +0530 Subject: [PATCH] fix: replace nested Routes with useLocation, remove pushState+reload, add debug logs - ProfileRoutes: removed nested in favor of useLocation()/useNavigate() with strict allowlist for /profile/me and /profile/me/edit - ProfileComponentWrapper: replaced pushState()+reload() with navigate() - useApi: log 401 and non-401 errors - contexts.tsx: log fetchCurrentUser lifecycle and auth:unauthorized events - axios.ts: log auth server 401s --- react-auth/axios.ts | 2 + react-auth/contexts.tsx | 33 +++++++-- react-openapi/src/components/Admin.tsx | 99 ++++++++++++++------------ react-openapi/src/hooks/useApi.ts | 20 +++++- 4 files changed, 103 insertions(+), 51 deletions(-) diff --git a/react-auth/axios.ts b/react-auth/axios.ts index 11b7a66..6ca6d20 100644 --- a/react-auth/axios.ts +++ b/react-auth/axios.ts @@ -17,7 +17,9 @@ export function attachAuthInterceptors(client: AxiosInstance) { (res) => res, (error) => { if (error.response?.status === 401) { + console.log("[authAxios] 401 from %s %s", error.config?.method, error.config?.url); tokenStore.clear(); + window.dispatchEvent(new CustomEvent("auth:unauthorized")); } return Promise.reject(error); } diff --git a/react-auth/contexts.tsx b/react-auth/contexts.tsx index 5808e75..3957a33 100644 --- a/react-auth/contexts.tsx +++ b/react-auth/contexts.tsx @@ -27,9 +27,11 @@ const AuthContext = createContext(undefined); export function AuthProvider({ children, authConfig, + onUnauthorized, }: { children: React.ReactNode; authConfig: AuthServerConfig; + onUnauthorized?: () => void; }) { const [currentUser, setCurrentUser] = useState(null); const [token, setToken] = useState(tokenStore.get()); @@ -82,20 +84,43 @@ export function AuthProvider({ }; const fetchCurrentUser = async () => { - if (!token) return; + if (!token) { console.log("[AuthProvider] fetchCurrentUser SKIP no token"); return; } + console.log("[AuthProvider] fetchCurrentUser calling %s%s", authConfig.serverUrl, authConfig.mePath); try { const me = await auth.get(authConfig.mePath); + console.log("[AuthProvider] fetchCurrentUser SUCCESS", me.data); setCurrentUser({ ...me.data }); - } catch { + } catch (e: any) { + console.log("[AuthProvider] fetchCurrentUser ERROR", e.message, e.response?.status); tokenStore.clear(); setToken(null); setCurrentUser(null); + onUnauthorized?.(); } }; useEffect(() => { - fetchCurrentUser(); - }, [token]); + console.log("[AuthProvider] useEffect token=%s serverUrl=%s", token, authConfig.serverUrl); + if (authConfig.serverUrl) { + fetchCurrentUser(); + } + }, [token, authConfig.serverUrl]); + + useEffect(() => { + const handler = () => { + console.log("[AuthProvider] auth:unauthorized event received"); + tokenStore.clear(); + setToken(null); + setCurrentUser(null); + onUnauthorized?.(); + }; + console.log("[AuthProvider] adding auth:unauthorized listener"); + window.addEventListener("auth:unauthorized", handler); + return () => { + console.log("[AuthProvider] removing auth:unauthorized listener"); + window.removeEventListener("auth:unauthorized", handler); + }; + }, [onUnauthorized]); return ( o.method === "GET"); + const location = useLocation(); + const navigate = useNavigate(); + + console.log("[ProfileRoutes] render pathname=%s ops=%o", location.pathname, ops.map(o => o.path)); + + // Redirect /:username to /me (only "me" and "me/edit" are valid) + const match = location.pathname.match(/^\/profile\/(.+)$/); + const username = match ? match[1] : ""; + console.log("[ProfileRoutes] match=%o username=%s", match && match[1], username); + if (username !== "me" && username !== "me/edit") { + console.log("[ProfileRoutes] redirecting to /profile/me"); + navigate("/profile/me", { replace: true }); + return null; + } + + const isEdit = username === "me/edit"; + const mode = isEdit ? "edit" as const : "view" as const; + console.log("[ProfileRoutes] mode=%s", mode); + + const op = ops.find((o) => { + if (isEdit && (o.method === "PUT" || o.method === "PATCH")) return true; + if (!isEdit && o.method === "GET") return true; + return false; + }); + + if (!op) return null; + + let Component: React.ComponentType | undefined; + if (mode === "view" && profileComponents.view) { + Component = profileComponents.view; + } else if (mode === "edit" && profileComponents.edit) { + Component = profileComponents.edit; + } + + if (!Component) return null; return ( - - } /> - {ops.map((op) => { - const basePath = friendlyProfilePath(op.path); - let Component: React.ComponentType | undefined; - let mode: "view" | "create" | "edit" | undefined; - - if (op.method === "GET" && profileComponents.view) { - Component = profileComponents.view; - mode = "view"; - } else if (op.method === "POST" && profileComponents.create) { - Component = profileComponents.create; - mode = "create"; - } else if ((op.method === "PUT" || op.method === "PATCH") && profileComponents.edit) { - Component = profileComponents.edit; - mode = "edit"; - } - - if (!Component || !mode) return null; - - const routePath = mode === "view" ? basePath : `${basePath}/edit`; - - return ( - - } - /> - ); - })} - + ); } @@ -134,19 +137,24 @@ function ProfileComponentWrapper({ operation: { path: string; method: string }; getOperation?: { path: string }; }) { + const navigate = useNavigate(); const [data, setData] = useState(null); const [loading, setLoading] = useState(mode !== "create"); const [error, setError] = useState(null); + console.log("[ProfileComponentWrapper] render mode=%s fetchPath=%s", mode, getOperation?.path ?? operation.path); const fetchPath = getOperation?.path ?? operation.path; const fetchData = async () => { + console.log("[ProfileComponentWrapper] fetchData START path=%s", fetchPath); setLoading(true); setError(null); try { const res = await getApi().get(fetchPath); + console.log("[ProfileComponentWrapper] fetchData SUCCESS", res.data); setData(res.data); } catch (e: any) { + console.log("[ProfileComponentWrapper] fetchData ERROR", e.message, e.response?.status); setError(e.response?.data?.detail ?? e.message ?? "Failed to load profile"); } finally { setLoading(false); @@ -155,11 +163,13 @@ function ProfileComponentWrapper({ useEffect(() => { if (mode !== "create") { + console.log("[ProfileComponentWrapper] useEffect calling fetchData"); fetchData(); } }, [fetchPath, mode]); const handleSubmit = async (formData: Record) => { + console.log("[ProfileComponentWrapper] handleSubmit START"); setLoading(true); setError(null); try { @@ -168,11 +178,11 @@ function ProfileComponentWrapper({ } else { await getApi()({ method: operation.method.toLowerCase(), url: operation.path, data: formData }); } - // Navigate back to the base profile view path + console.log("[ProfileComponentWrapper] handleSubmit SUCCESS, navigating to /profile/me"); const base = friendlyProfilePath(operation.path); - window.history.pushState(null, "", `/${base}`); - window.location.reload(); + navigate(`/profile/${base}`, { replace: true }); } catch (e: any) { + console.log("[ProfileComponentWrapper] handleSubmit ERROR", e.message, e.response?.status); setError(e.response?.data?.detail ?? e.message ?? "Operation failed"); } finally { setLoading(false); @@ -203,10 +213,7 @@ function ProfileComponentWrapper({ props.username = data.username; props.email = data.email; } - props.onEdit = () => { - window.history.pushState(null, "", `/profile/me/edit`); - window.location.reload(); - }; + props.onEdit = () => navigate("/profile/me/edit"); props.loading = loading; } diff --git a/react-openapi/src/hooks/useApi.ts b/react-openapi/src/hooks/useApi.ts index 22eacf8..7d7b5b4 100644 --- a/react-openapi/src/hooks/useApi.ts +++ b/react-openapi/src/hooks/useApi.ts @@ -1,12 +1,16 @@ import axios, { AxiosInstance } from "axios"; let apiClient: AxiosInstance | null = null; +let _onUnauthorized: (() => void) | undefined; -export function initApi(baseUrl: string, getToken?: () => string | null): AxiosInstance { +export function initApi(baseUrl: string, getToken?: () => string | null, onUnauthorized?: () => void): AxiosInstance { if (apiClient && apiClient.defaults.baseURL === baseUrl) { + _onUnauthorized = onUnauthorized; return apiClient; } + _onUnauthorized = onUnauthorized; + apiClient = axios.create({ baseURL: baseUrl, headers: { "Content-Type": "application/json" }, @@ -20,6 +24,20 @@ export function initApi(baseUrl: string, getToken?: () => string | null): AxiosI return config; }); + apiClient.interceptors.response.use( + (res) => res, + (error) => { + if (error.response?.status === 401) { + console.log("[useApi] 401 from %s %s - dispatching auth:unauthorized", error.config?.method, error.config?.url); + window.dispatchEvent(new CustomEvent("auth:unauthorized")); + _onUnauthorized?.(); + } else { + console.log("[useApi] non-401 error %s from %s %s", error.response?.status, error.config?.method, error.config?.url); + } + return Promise.reject(error); + } + ); + return apiClient; }