From 34d5e9bb52fcd3898fc38414ecd13cdab3300b80 Mon Sep 17 00:00:00 2001 From: yurikakan Date: Sat, 5 Sep 2026 20:58:08 -0400 Subject: [PATCH 1/3] bad deep link fixes for volunteer order/request management --- apps/frontend/src/components/foodRequestManagement.tsx | 3 ++- apps/frontend/src/containers/volunteerOrderManagement.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/components/foodRequestManagement.tsx b/apps/frontend/src/components/foodRequestManagement.tsx index 608d593ab..41825e80b 100644 --- a/apps/frontend/src/components/foodRequestManagement.tsx +++ b/apps/frontend/src/components/foodRequestManagement.tsx @@ -95,9 +95,10 @@ const RequestManagement: React.FC = ({ setCurrentPage(Math.floor(idx / itemsPerPage) + 1); } } else { + setAlertMessage('Request not found.', AlertStatus.ERROR); navigate(location.pathname, { replace: true }); } - }, [initialRequestId, requests, navigate, location]); + }, [initialRequestId, requests, navigate, location, setAlertMessage]); const pantryOptions = [ ...new Set( diff --git a/apps/frontend/src/containers/volunteerOrderManagement.tsx b/apps/frontend/src/containers/volunteerOrderManagement.tsx index f396a2a2b..ff61683b0 100644 --- a/apps/frontend/src/containers/volunteerOrderManagement.tsx +++ b/apps/frontend/src/containers/volunteerOrderManagement.tsx @@ -198,9 +198,10 @@ const VolunteerOrderManagement: React.FC = () => { } } } else { + setAlertMessage('Order not found.', AlertStatus.ERROR); navigate(ROUTES.VOLUNTEER_ORDER_MANAGEMENT, { replace: true }); } - }, [searchParams, statusOrders, navigate]); + }, [searchParams, statusOrders, navigate, setAlertMessage]); // Pre-fill pantry filter from url param, url is kept for back/forward navigation useEffect(() => { From 3b57da39c06c4abdf8afb76e4fcde2e93e381f04 Mon Sep 17 00:00:00 2001 From: yurikakan Date: Sat, 5 Sep 2026 21:35:45 -0400 Subject: [PATCH 2/3] fix: NaN requestId silently skips "Request not found" alert Effect used `!initialRequestId`, which treats NaN as falsy and returns early. Malformed ?requestId= values became NaN via Number(), so the alert never fired. Check for undefined explicitly instead. Co-Authored-By: Claude Sonnet 5 --- apps/frontend/src/components/foodRequestManagement.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/components/foodRequestManagement.tsx b/apps/frontend/src/components/foodRequestManagement.tsx index 41825e80b..62ddb0805 100644 --- a/apps/frontend/src/components/foodRequestManagement.tsx +++ b/apps/frontend/src/components/foodRequestManagement.tsx @@ -78,7 +78,7 @@ const RequestManagement: React.FC = ({ }, [selectedFilteredPantries]); useEffect(() => { - if (!initialRequestId || requests.length === 0) return; + if (initialRequestId === undefined || requests.length === 0) return; const match = requests.find((r) => r.requestId === initialRequestId); if (match) { From 0ab88110fb0ddb546d6c6010e12b01e4403a6a35 Mon Sep 17 00:00:00 2001 From: yurikakan Date: Mon, 7 Sep 2026 20:42:51 -0400 Subject: [PATCH 3/3] frontend gating for ux, loading auth handling, cleaner fetchs --- apps/frontend/src/app.tsx | 53 +++++++++++-------- apps/frontend/src/components/Navbar.tsx | 16 ++---- .../src/components/protectedRoute.tsx | 16 +++++- apps/frontend/src/components/userContext.tsx | 43 +++++++++++++++ apps/frontend/src/containers/root.tsx | 15 +++--- apps/frontend/src/containers/unauthorized.tsx | 27 ++++++---- 6 files changed, 116 insertions(+), 54 deletions(-) create mode 100644 apps/frontend/src/components/userContext.tsx diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index 21874d921..4573f4751 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -41,9 +41,16 @@ import PantryDashboard from '@containers/pantryDashboard'; import VolunteerDashboard from '@containers/volunteerDashboard'; import AdminDashboard from '@containers/adminDashboard'; import FoodManufacturerDashboard from '@containers/foodManufacturerDashboard'; +import { Role } from './types/types'; Amplify.configure(CognitoAuthConfig); +const PANTRY_ONLY = [Role.PANTRY]; +const VOLUNTEER_ONLY = [Role.VOLUNTEER]; +const FOOD_MANUFACTURER_ONLY = [Role.FOODMANUFACTURER]; +const ADMIN_ONLY = [Role.ADMIN]; +const ADMIN_OR_VOLUNTEER = [Role.ADMIN, Role.VOLUNTEER]; + const router = createBrowserRouter([ { path: ROUTES.HOME, @@ -89,7 +96,7 @@ const router = createBrowserRouter([ { path: ROUTES.REQUEST_FORM, element: ( - + ), @@ -97,7 +104,7 @@ const router = createBrowserRouter([ { path: ROUTES.PANTRY_DASHBOARD, element: ( - + ), @@ -105,7 +112,7 @@ const router = createBrowserRouter([ { path: ROUTES.VOLUNTEER_DASHBOARD, element: ( - + ), @@ -113,7 +120,7 @@ const router = createBrowserRouter([ { path: ROUTES.FM_DONATION_MANAGEMENT, element: ( - + ), @@ -121,7 +128,7 @@ const router = createBrowserRouter([ { path: ROUTES.FM_DASHBOARD, element: ( - + ), @@ -129,7 +136,7 @@ const router = createBrowserRouter([ { path: ROUTES.APPROVE_PANTRIES, element: ( - + ), @@ -137,7 +144,7 @@ const router = createBrowserRouter([ { path: ROUTES.APPROVE_FOOD_MANUFACTURERS, element: ( - + ), @@ -145,7 +152,7 @@ const router = createBrowserRouter([ { path: ROUTES.PANTRY_APPLICATION_DETAILS, element: ( - + ), @@ -153,7 +160,7 @@ const router = createBrowserRouter([ { path: ROUTES.PANTRY_MANAGEMENT_DETAILS, element: ( - + ), @@ -161,7 +168,7 @@ const router = createBrowserRouter([ { path: ROUTES.FOOD_MANUFACTURER_APPLICATION_DETAILS, element: ( - + ), @@ -169,7 +176,7 @@ const router = createBrowserRouter([ { path: ROUTES.FOOD_MANUFACTURER_MANAGEMENT_DETAILS, element: ( - + ), @@ -177,7 +184,7 @@ const router = createBrowserRouter([ { path: ROUTES.ADMIN_DONATION, element: ( - + ), @@ -185,7 +192,7 @@ const router = createBrowserRouter([ { path: ROUTES.ADMIN_DONATION_STATS, element: ( - + ), @@ -193,7 +200,7 @@ const router = createBrowserRouter([ { path: ROUTES.VOLUNTEER_MANAGEMENT, element: ( - + ), @@ -201,7 +208,7 @@ const router = createBrowserRouter([ { path: ROUTES.ADMIN_DASHBOARD, element: ( - + ), @@ -209,7 +216,7 @@ const router = createBrowserRouter([ { path: ROUTES.ADMIN_REQUEST_MANAGEMENT, element: ( - + ), @@ -217,7 +224,7 @@ const router = createBrowserRouter([ { path: ROUTES.ADMIN_ORDER_MANAGEMENT, element: ( - + ), @@ -225,7 +232,7 @@ const router = createBrowserRouter([ { path: ROUTES.PANTRY_ORDER_MANAGEMENT, element: ( - + ), @@ -241,7 +248,7 @@ const router = createBrowserRouter([ { path: ROUTES.VOLUNTEER_ASSIGNED_PANTRIES, element: ( - + ), @@ -249,7 +256,7 @@ const router = createBrowserRouter([ { path: ROUTES.VOLUNTEER_REQUEST_MANAGEMENT, element: ( - + ), @@ -257,7 +264,7 @@ const router = createBrowserRouter([ { path: ROUTES.VOLUNTEER_ORDER_MANAGEMENT, element: ( - + ), @@ -265,7 +272,7 @@ const router = createBrowserRouter([ { path: ROUTES.PANTRY_MANAGEMENT, element: ( - + ), @@ -273,7 +280,7 @@ const router = createBrowserRouter([ { path: ROUTES.FOOD_MANUFACTURER_MANAGEMENT, element: ( - + ), diff --git a/apps/frontend/src/components/Navbar.tsx b/apps/frontend/src/components/Navbar.tsx index c6a033478..e0d296971 100644 --- a/apps/frontend/src/components/Navbar.tsx +++ b/apps/frontend/src/components/Navbar.tsx @@ -4,9 +4,9 @@ import { Box, Flex, Text, VStack } from '@chakra-ui/react'; import { useAuthenticator } from '@aws-amplify/ui-react'; import { signOut } from 'aws-amplify/auth'; import { ChevronDown, ChevronRight, LogOut } from 'lucide-react'; -import ApiClient from '@api/apiClient'; -import { Role, User } from '../types/types'; +import { Role } from '../types/types'; import { ROUTES } from '../routes'; +import { useCurrentUser } from './userContext'; const ROLE_MAP: Record = { [Role.ADMIN]: { label: 'Admin' }, @@ -218,21 +218,11 @@ const NavGroup: React.FC = ({ const Navbar: React.FC = () => { const { authStatus } = useAuthenticator((context) => [context.authStatus]); - const [currentUser, setCurrentUser] = useState(null); + const { user: currentUser } = useCurrentUser(); const [openGroups, setOpenGroups] = useState>(new Set()); const location = useLocation(); const navigate = useNavigate(); - useEffect(() => { - if (authStatus === 'authenticated') { - ApiClient.getMe() - .then(setCurrentUser) - .catch(() => setCurrentUser(null)); - } else if (authStatus === 'unauthenticated') { - setCurrentUser(null); - } - }, [authStatus]); - // On reload or navigation, make sure the currently opened groups stays open useEffect(() => { if (!currentUser) return; diff --git a/apps/frontend/src/components/protectedRoute.tsx b/apps/frontend/src/components/protectedRoute.tsx index 52392dd3c..dded565fb 100644 --- a/apps/frontend/src/components/protectedRoute.tsx +++ b/apps/frontend/src/components/protectedRoute.tsx @@ -2,16 +2,24 @@ import { Navigate, useLocation, Outlet } from 'react-router-dom'; import { ROUTES } from '../routes'; import { useAuthenticator } from '@aws-amplify/ui-react'; import { Center, Spinner, Text } from '@chakra-ui/react'; +import { Role } from '../types/types'; +import { useCurrentUser } from './userContext'; type Props = { children?: JSX.Element; + allowedRoles?: Role[]; }; -const ProtectedRoute = ({ children }: Props) => { +const ProtectedRoute = ({ children, allowedRoles }: Props) => { const { authStatus } = useAuthenticator((context) => [context.authStatus]); const location = useLocation(); + const { user, loading: roleLoading } = useCurrentUser(); + const role = user?.role ?? null; - if (authStatus === 'configuring') { + if ( + authStatus === 'configuring' || + (allowedRoles && authStatus === 'authenticated' && roleLoading) + ) { return (
@@ -24,6 +32,10 @@ const ProtectedRoute = ({ children }: Props) => { return ; } + if (allowedRoles && (!role || !allowedRoles.includes(role))) { + return ; + } + return children ?? ; }; diff --git a/apps/frontend/src/components/userContext.tsx b/apps/frontend/src/components/userContext.tsx new file mode 100644 index 000000000..41b2b8207 --- /dev/null +++ b/apps/frontend/src/components/userContext.tsx @@ -0,0 +1,43 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { useAuthenticator } from '@aws-amplify/ui-react'; +import ApiClient from '@api/apiClient'; +import { User } from '../types/types'; + +type UserContextValue = { + user: User | null; + loading: boolean; +}; + +const UserContext = createContext({ + user: null, + loading: false, +}); + +export const UserProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const { authStatus } = useAuthenticator((context) => [context.authStatus]); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (authStatus === 'authenticated') { + setLoading(true); + ApiClient.getMe() + .then(setUser) + .catch(() => setUser(null)) + .finally(() => setLoading(false)); + } else if (authStatus === 'unauthenticated') { + setUser(null); + setLoading(false); + } + }, [authStatus]); + + return ( + + {children} + + ); +}; + +export const useCurrentUser = () => useContext(UserContext); diff --git a/apps/frontend/src/containers/root.tsx b/apps/frontend/src/containers/root.tsx index 9e216caaf..8aec767c8 100644 --- a/apps/frontend/src/containers/root.tsx +++ b/apps/frontend/src/containers/root.tsx @@ -3,6 +3,7 @@ import { Box, Flex } from '@chakra-ui/react'; import { useEffect } from 'react'; import apiClient from '@api/apiClient'; import Navbar from '../components/Navbar'; +import { UserProvider } from '../components/userContext'; const Root: React.FC = () => { const navigate = useNavigate(); @@ -12,12 +13,14 @@ const Root: React.FC = () => { }, [navigate]); return ( - - - - - - + + + + + + + + ); }; diff --git a/apps/frontend/src/containers/unauthorized.tsx b/apps/frontend/src/containers/unauthorized.tsx index d38221f8d..c1d6565f8 100644 --- a/apps/frontend/src/containers/unauthorized.tsx +++ b/apps/frontend/src/containers/unauthorized.tsx @@ -1,17 +1,24 @@ +import { Link as RouterLink } from 'react-router-dom'; +import { Button, Center, Text, VStack } from '@chakra-ui/react'; import { ROUTES } from '../routes'; export const Unauthorized: React.FC = () => { return ( -
-

Oops!

-

You are not an authorized user for this page!

-

- Return to{' '} - - home page - -

-
+
+ + This page is unavailable. + + +
); };