Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 30 additions & 23 deletions apps/frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -89,143 +96,143 @@ const router = createBrowserRouter([
{
path: ROUTES.REQUEST_FORM,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={PANTRY_ONLY}>
<FormRequests />
</ProtectedRoute>
),
},
{
path: ROUTES.PANTRY_DASHBOARD,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={PANTRY_ONLY}>
<PantryDashboard />
</ProtectedRoute>
),
},
{
path: ROUTES.VOLUNTEER_DASHBOARD,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={VOLUNTEER_ONLY}>
<VolunteerDashboard />
</ProtectedRoute>
),
},
{
path: ROUTES.FM_DONATION_MANAGEMENT,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={FOOD_MANUFACTURER_ONLY}>
<FoodManufacturerDonationManagement />
</ProtectedRoute>
),
},
{
path: ROUTES.FM_DASHBOARD,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={FOOD_MANUFACTURER_ONLY}>
<FoodManufacturerDashboard />
</ProtectedRoute>
),
},
{
path: ROUTES.APPROVE_PANTRIES,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_ONLY}>
<ApprovePantries />
</ProtectedRoute>
),
},
{
path: ROUTES.APPROVE_FOOD_MANUFACTURERS,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_ONLY}>
<ApproveFoodManufacturers />
</ProtectedRoute>
),
},
{
path: ROUTES.PANTRY_APPLICATION_DETAILS,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_ONLY}>
<PantryApplicationDetails />
</ProtectedRoute>
),
},
{
path: ROUTES.PANTRY_MANAGEMENT_DETAILS,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_OR_VOLUNTEER}>
<PantryApplicationDetails />
</ProtectedRoute>
),
},
{
path: ROUTES.FOOD_MANUFACTURER_APPLICATION_DETAILS,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_ONLY}>
<FoodManufacturerApplicationDetails />
</ProtectedRoute>
),
},
{
path: ROUTES.FOOD_MANUFACTURER_MANAGEMENT_DETAILS,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_ONLY}>
<FoodManufacturerApplicationDetails />
</ProtectedRoute>
),
},
{
path: ROUTES.ADMIN_DONATION,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_ONLY}>
<AdminDonation />
</ProtectedRoute>
),
},
{
path: ROUTES.ADMIN_DONATION_STATS,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_ONLY}>
<AdminDonationStats />
</ProtectedRoute>
),
},
{
path: ROUTES.VOLUNTEER_MANAGEMENT,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_ONLY}>
<VolunteerManagement />
</ProtectedRoute>
),
},
{
path: ROUTES.ADMIN_DASHBOARD,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_ONLY}>
<AdminDashboard />
</ProtectedRoute>
),
},
{
path: ROUTES.ADMIN_REQUEST_MANAGEMENT,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_ONLY}>
<AdminRequestManagement />
</ProtectedRoute>
),
},
{
path: ROUTES.ADMIN_ORDER_MANAGEMENT,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_ONLY}>
<AdminOrderManagement />
</ProtectedRoute>
),
},
{
path: ROUTES.PANTRY_ORDER_MANAGEMENT,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={PANTRY_ONLY}>
<PantryOrderManagement />
</ProtectedRoute>
),
Expand All @@ -241,39 +248,39 @@ const router = createBrowserRouter([
{
path: ROUTES.VOLUNTEER_ASSIGNED_PANTRIES,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={VOLUNTEER_ONLY}>
<AssignedPantries />
</ProtectedRoute>
),
},
{
path: ROUTES.VOLUNTEER_REQUEST_MANAGEMENT,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={VOLUNTEER_ONLY}>
<VolunteerRequestManagement />
</ProtectedRoute>
),
},
{
path: ROUTES.VOLUNTEER_ORDER_MANAGEMENT,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={VOLUNTEER_ONLY}>
<VolunteerOrderManagement />
</ProtectedRoute>
),
},
{
path: ROUTES.PANTRY_MANAGEMENT,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_ONLY}>
<AdminPantryManagement />
</ProtectedRoute>
),
},
{
path: ROUTES.FOOD_MANUFACTURER_MANAGEMENT,
element: (
<ProtectedRoute>
<ProtectedRoute allowedRoles={ADMIN_ONLY}>
<AdminFoodManufacturerManagement />
</ProtectedRoute>
),
Expand Down
16 changes: 3 additions & 13 deletions apps/frontend/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, { label: string }> = {
[Role.ADMIN]: { label: 'Admin' },
Expand Down Expand Up @@ -218,21 +218,11 @@ const NavGroup: React.FC<NavGroupProps> = ({

const Navbar: React.FC = () => {
const { authStatus } = useAuthenticator((context) => [context.authStatus]);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const { user: currentUser } = useCurrentUser();
const [openGroups, setOpenGroups] = useState<Set<string>>(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;
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/src/components/foodRequestManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const RequestManagement: React.FC<RequestManagementProps> = ({
}, [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) {
Expand All @@ -95,9 +95,10 @@ const RequestManagement: React.FC<RequestManagementProps> = ({
setCurrentPage(Math.floor(idx / itemsPerPage) + 1);
}
} else {
setAlertMessage('Request not found.', AlertStatus.ERROR);
navigate(location.pathname, { replace: true });
}
Comment on lines 97 to 100
}, [initialRequestId, requests, navigate, location]);
}, [initialRequestId, requests, navigate, location, setAlertMessage]);

const pantryOptions = [
...new Set(
Expand Down
16 changes: 14 additions & 2 deletions apps/frontend/src/components/protectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Center h="100vh" flexDirection="column">
<Spinner size="lg" />
Expand All @@ -24,6 +32,10 @@ const ProtectedRoute = ({ children }: Props) => {
return <Navigate to={ROUTES.LOGIN} replace state={{ from: location }} />;
}

if (allowedRoles && (!role || !allowedRoles.includes(role))) {
return <Navigate to={ROUTES.UNAUTHORIZED} replace />;
}

return children ?? <Outlet />;
};

Expand Down
43 changes: 43 additions & 0 deletions apps/frontend/src/components/userContext.tsx
Original file line number Diff line number Diff line change
@@ -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<UserContextValue>({
user: null,
loading: false,
});

export const UserProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const { authStatus } = useAuthenticator((context) => [context.authStatus]);
const [user, setUser] = useState<User | null>(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 (
<UserContext.Provider value={{ user, loading }}>
{children}
</UserContext.Provider>
);
};

export const useCurrentUser = () => useContext(UserContext);
15 changes: 9 additions & 6 deletions apps/frontend/src/containers/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -12,12 +13,14 @@ const Root: React.FC = () => {
}, [navigate]);

return (
<Flex minH="100vh">
<Navbar />
<Box flex={1} overflow="auto">
<Outlet />
</Box>
</Flex>
<UserProvider>
<Flex minH="100vh">
<Navbar />
<Box flex={1} overflow="auto">
<Outlet />
</Box>
</Flex>
</UserProvider>
);
};

Expand Down
Loading
Loading