diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7c8e0be..69e432f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,6 +13,7 @@ import PublicLayout from "./pages/public/PublicLayout"; import Dashboard from "./features/dashboard/pages/Dashboard"; import TrainingPlanner from "./features/training/pages/TrainingPlanner"; import TrainingAttendance from "./features/training/pages/TrainingAttendance"; +import NotFound from "./pages/NotFound"; export default function App(){ @@ -43,7 +44,7 @@ export default function App(){ {/* catch-all for 404 */} - Page Not Found} /> + } /> ) diff --git a/frontend/src/api/axios.ts b/frontend/src/api/axios.ts index 78c99e5..3f516fd 100644 --- a/frontend/src/api/axios.ts +++ b/frontend/src/api/axios.ts @@ -35,21 +35,25 @@ api.interceptors.request.use( // ------ Response: handle 401 with single-flight refresh & retry ------ let refreshingPromise: Promise | null = null; -async function refreshAccess(): Promise | null{ - // De-dupe concurrent refresh attempts +const AUTH_ENDPOINTS = ["/auth/login", "/auth/register", "/auth/refresh"]; + +function isAuthEndpoint(url?: string): boolean { + if (!url) return false; + return AUTH_ENDPOINTS.some((endpoint) => url.includes(endpoint)); +} + +async function refreshAccess(): Promise { if (!refreshingPromise) { refreshingPromise = (async () => { try { - const { data } = await api.post<{ access: string}>("/auth/refresh", {});// cookie travels + const { data } = await api.post<{ access: string }>("/auth/refresh", {}); const token = data?.access ?? null; useAuthStore.getState().setAccessToken(token); return token; } catch (error) { - // refresh failed — clear auth useAuthStore.getState().logout(); return null; } finally { - // important: release the lock *after* microtask turn const p = refreshingPromise; setTimeout(() => { if (refreshingPromise === p) refreshingPromise = null; @@ -59,31 +63,36 @@ async function refreshAccess(): Promise | null{ } return refreshingPromise; } + api.interceptors.response.use( (response: AxiosResponse) => response, async (error: AxiosError) => { - const original = error.config as AxiosRequestConfig & { _retry?: boolean}; + const config = error.config as AxiosRequestConfig & { _retry?: boolean }; const status = error.response?.status; - // Only try once per request - if (status === 401 && !original?._retry) { - original._retry = true; + if ( + status === 401 && + !config?._retry && + config && + !isAuthEndpoint(config.url) && + useAuthStore.getState().accessToken + ) { + config._retry = true; const token = await refreshAccess(); if (token) { - // Update header and retry the original request - original.headers = { ...(original.headers || {}), Authorization: `Bearer ${token}`}; - return api.request(original); + if (config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } else { + config.headers = { Authorization: `Bearer ${token}` }; + } + return api.request(config); } else { - window.location.href = "/login"; // or use navigate() + window.location.href = "/login"; + return Promise.reject(error); } } - // if (error.response) { - // if (error.response.status === 401) { - // console.warn("Unauthorized. Redirecting to login..."); - // window.location.href = "/login"; // or use navigate() - // } - // } + return Promise.reject(error); } ); diff --git a/frontend/src/pages/NotFound.tsx b/frontend/src/pages/NotFound.tsx new file mode 100644 index 0000000..df9dea9 --- /dev/null +++ b/frontend/src/pages/NotFound.tsx @@ -0,0 +1,77 @@ +import { Box, Button, Container, Typography, useTheme } from "@mui/material"; +import { useNavigate } from "react-router-dom"; + +export default function NotFound() { + const navigate = useNavigate(); + const theme = useTheme(); + + return ( + + + + 404 + + + + Page Not Found + + + + Sorry, the page you're looking for doesn't exist. It might have been + moved or deleted. + + + + + + + + + ); +}