Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
34edd92
Initial JWT Auth for backend
dburkhart07 Mar 21, 2025
2011b66
Added guards for jwt auth
dburkhart07 Mar 27, 2025
bb180d4
Updated auth
dburkhart07 Mar 29, 2025
1a95f15
Tried fixing JWT Strategy
dburkhart07 Mar 29, 2025
8587514
Updated auth
dburkhart07 Mar 30, 2025
8f251c8
Finished general authentication for both frontend and backend pages
dburkhart07 Mar 31, 2025
89677e3
Final commit for this branch
dburkhart07 Apr 1, 2025
267b6eb
Revisions made with Sam!!!
dburkhart07 Aug 10, 2025
44f537d
Resolved merge conflicts
dburkhart07 Jan 19, 2026
a5a7852
Resolved merge conflicts
dburkhart07 Jan 19, 2026
20a26ba
Another main merge
dburkhart07 Jan 19, 2026
88f8d3c
Fixed all errors with modules
dburkhart07 Jan 19, 2026
5421fe6
prettier
dburkhart07 Jan 19, 2026
6aea768
Fixed module importing
dburkhart07 Jan 19, 2026
caa33e9
prettier
dburkhart07 Jan 19, 2026
390b380
Added back in donation migration
dburkhart07 Jan 19, 2026
f1bad91
Full implementation of backend role-based auth
dburkhart07 Jan 19, 2026
f7621f5
prettier
dburkhart07 Jan 19, 2026
1331bbb
[SSF 17] - environment variables updates (#44)
dburkhart07 Jan 21, 2026
d69b3c8
Fixed user flow to use a cognito id hardcoded into the database
dburkhart07 Jan 23, 2026
75c3f95
Messy first attempt. Working for single service validation
dburkhart07 Jan 24, 2026
36b5c88
Messy first attempt. Working for single service validation
dburkhart07 Jan 24, 2026
c40096d
Messy first attempt. Working for single service validation
dburkhart07 Jan 24, 2026
6a02f87
Messy first attempt. Working for single service validation
dburkhart07 Jan 24, 2026
27ca72c
Working version, precleanup
dburkhart07 Jan 24, 2026
8958137
prettier
dburkhart07 Jan 24, 2026
9a1aee6
Added documentation to make things clearer
dburkhart07 Jan 25, 2026
3d9da87
Merged in main
dburkhart07 Feb 6, 2026
4d81c5a
prettier
dburkhart07 Feb 6, 2026
b907c10
Merged main
dburkhart07 Feb 7, 2026
5bdfa4c
Merged main
dburkhart07 Feb 15, 2026
bafc312
Merged main
dburkhart07 Feb 15, 2026
866f758
Merged main
dburkhart07 Feb 15, 2026
f6d2931
Addressed comments
dburkhart07 Feb 16, 2026
1663cb4
Addressed comments
dburkhart07 Feb 16, 2026
685a69f
Final commit
dburkhart07 Feb 16, 2026
6a8dc5b
Final commit
dburkhart07 Feb 16, 2026
fd294fa
prettier
dburkhart07 Feb 16, 2026
289bf33
Fixed navigation and form request modal frontend
dburkhart07 Feb 16, 2026
af97e51
prettier
dburkhart07 Feb 16, 2026
54023b5
Final commit
dburkhart07 Feb 16, 2026
b6fd816
Removed console logs
dburkhart07 Feb 16, 2026
a7e63bf
Removed another console log
dburkhart07 Feb 16, 2026
d031f84
Merged main
dburkhart07 Feb 17, 2026
8113934
Merged main
dburkhart07 Feb 18, 2026
5730dda
Merged main
dburkhart07 Feb 18, 2026
78cfd6a
Merged main
dburkhart07 Feb 18, 2026
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
8 changes: 7 additions & 1 deletion apps/backend/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { passportJwtSecret } from 'jwks-rsa';
import { ExtractJwt, Strategy } from 'passport-jwt';
Expand Down Expand Up @@ -31,6 +31,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
// we use the sub field in the payload to find the user in our database
async validate(payload: CognitoJwtPayload): Promise<User> {
const dbUser = await this.usersService.findUserByCognitoId(payload.sub);
// If an exception is thrown, throw something here
if (!dbUser) {
throw new NotFoundException(
`User with payload sub ${payload.sub} not found`,
);
}
return dbUser;
}
}
25 changes: 25 additions & 0 deletions apps/backend/src/auth/ownership.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SetMetadata, Type } from '@nestjs/common';

// Resolver function type to get the owner user ID for a given entity ID
export type OwnerIdResolver = (params: {
entityId: number;
services: ServiceRegistry;
}) => Promise<number | null>;

// Registry of services that can be easily resolved
// Eliminates the issues with circular dependencies
// allowing the lambdas to resolve only the services they need
export interface ServiceRegistry {
get<T>(serviceClass: Type<T>): T;
}

// Configuration for ownership check
export interface OwnershipConfig {
idParam: string;
resolver: OwnerIdResolver;
}

export const OWNERSHIP_CHECK_KEY = 'ownership_check';

export const CheckOwnership = (config: OwnershipConfig) =>
SetMetadata(OWNERSHIP_CHECK_KEY, config);
98 changes: 98 additions & 0 deletions apps/backend/src/auth/ownership.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
NotFoundException,
Type,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ModuleRef } from '@nestjs/core';
import {
OWNERSHIP_CHECK_KEY,
OwnershipConfig,
ServiceRegistry,
} from './ownership.decorator';

@Injectable()
export class OwnershipGuard implements CanActivate {
constructor(private reflector: Reflector, private moduleRef: ModuleRef) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const config = this.reflector.get<OwnershipConfig>(
OWNERSHIP_CHECK_KEY,
context.getHandler(),
);

if (!config) {
return true;
}

// Process all request information and the logged in user
const req = context.switchToHttp().getRequest();
const user = req.user;

// Admins bypass ownership checks
if (user.role === 'ADMIN') {
return true;
}

if (!user) {
throw new ForbiddenException('Not authenticated');
}

// Get the id from the parameters
const entityId = Number(req.params[config.idParam]);

if (isNaN(entityId)) {
throw new ForbiddenException(`Invalid ${config.idParam}`);
}

// Create a service registry that easily resolves services
const services = this.createServiceRegistry();

try {
// Execute the lambda function to get the owner user ID
const ownerId = await config.resolver({
entityId,
services,
});

if (ownerId === null || ownerId === undefined) {
throw new ForbiddenException('Unable to determine resource ownership');
}

if (ownerId !== user.id) {
throw new ForbiddenException('Access denied');
}

return true;
} catch (error) {
throw new ForbiddenException('Error verifying resource ownership');
}
}

// Use a service registry for easy service resolution and caching
private createServiceRegistry(): ServiceRegistry {
const cache = new Map<Type<unknown>, unknown>();
const moduleRef = this.moduleRef;

return {
get<T>(serviceClass: Type<T>): T {
// Return cached service if already resolved before
if (cache.has(serviceClass)) {
return cache.get(serviceClass) as T;
}

// Resolve and cache the service
try {
const service = moduleRef.get(serviceClass, { strict: false });
cache.set(serviceClass, service);
return service;
} catch (error) {
throw new Error(`Could not resolve service: ${serviceClass.name}`);
}
},
};
}
}
8 changes: 8 additions & 0 deletions apps/backend/src/auth/sharedAuth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { OwnershipGuard } from './ownership.guard';

@Module({
providers: [OwnershipGuard],
exports: [OwnershipGuard],
})
export class SharedAuthModule {}
5 changes: 3 additions & 2 deletions apps/backend/src/donationItems/donationItems.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ import {
Param,
Get,
Patch,
UseGuards,
ParseIntPipe,
BadRequestException,
} from '@nestjs/common';
import { ApiBody } from '@nestjs/swagger';
import { DonationItemsService } from './donationItems.service';
import { DonationItem } from './donationItems.entity';
import { AuthGuard } from '@nestjs/passport';
import { FoodType } from './types';
import { CreateMultipleDonationItemsDto } from './dtos/create-donation-items.dto';

@Controller('donation-items')
//@UseInterceptors()
@UseGuards(AuthGuard('jwt'))
export class DonationItemsController {
constructor(private donationItemsService: DonationItemsService) {}

Expand Down
10 changes: 10 additions & 0 deletions apps/backend/src/donationItems/donationItems.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ export class DonationItemsService {
@InjectRepository(Donation) private donationRepo: Repository<Donation>,
) {}

async findOne(itemId: number): Promise<DonationItem> {
validateId(itemId, 'Donation Item');

const donationItem = await this.repo.findOneBy({ itemId });
if (!donationItem) {
throw new NotFoundException(`Donation item ${itemId} not found`);
}
return donationItem;
}

async getAllDonationItems(donationId: number): Promise<DonationItem[]> {
validateId(donationId, 'Donation');
return this.repo.find({ where: { donation: { donationId } } });
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/src/foodRequests/request.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import { AuthModule } from '../auth/auth.module';
import { OrdersService } from '../orders/order.service';
import { Order } from '../orders/order.entity';
import { Pantry } from '../pantries/pantries.entity';
import { SharedAuthModule } from '../auth/sharedAuth.module';

@Module({
imports: [
AWSS3Module,
MulterModule.register({ dest: './uploads' }),
TypeOrmModule.forFeature([FoodRequest, Order, Pantry]),
AuthModule,
SharedAuthModule,
],
controllers: [RequestsController],
providers: [RequestsService, OrdersService],
Expand Down
28 changes: 28 additions & 0 deletions apps/backend/src/orders/order.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Body,
Query,
BadRequestException,
UseGuards,
ValidationPipe,
} from '@nestjs/common';
import { OrdersService } from './order.service';
Expand All @@ -16,6 +17,10 @@ import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity';
import { FoodRequest } from '../foodRequests/request.entity';
import { AllocationsService } from '../allocations/allocations.service';
import { OrderStatus } from './types';
import { AuthGuard } from '@nestjs/passport';
import { OwnershipGuard } from '../auth/ownership.guard';
import { CheckOwnership } from '../auth/ownership.decorator';
import { PantriesService } from '../pantries/pantries.service';
import { TrackingCostDto } from './dtos/tracking-cost.dto';

@Controller('orders')
Expand Down Expand Up @@ -56,6 +61,29 @@ export class OrdersController {
return this.ordersService.findOrderPantry(orderId);
}

// Test endpoint for right now
@UseGuards(AuthGuard('jwt'), OwnershipGuard)
@CheckOwnership({
idParam: 'orderId',
resolver: async ({ entityId, services }) => {
const request = await services
.get(OrdersService)
.findOrderFoodRequest(entityId);

if (!request) {
return null;
}

const pantry = await services
.get(PantriesService)
.findOne(request.pantryId);

if (!pantry) {
return null;
}
return pantry?.pantryUser?.id ?? null;
},
})
@Get('/:orderId/request')
async getRequestFromOrder(
@Param('orderId', ParseIntPipe) orderId: number,
Expand Down
2 changes: 0 additions & 2 deletions apps/backend/src/orders/order.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { OrdersController } from './order.controller';
import { Order } from './order.entity';
import { OrdersService } from './order.service';
import { JwtStrategy } from '../auth/jwt.strategy';
import { AuthService } from '../auth/auth.service';
import { Pantry } from '../pantries/pantries.entity';
import { AllocationModule } from '../allocations/allocations.module';
import { AuthModule } from '../auth/auth.module';
Expand Down
12 changes: 12 additions & 0 deletions apps/backend/src/pantries/pantries.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import {
ParseIntPipe,
Patch,
Post,
UseGuards,
Req,
UnauthorizedException,
} from '@nestjs/common';
import { Pantry } from './pantries.entity';
import { PantriesService } from './pantries.service';
import { Role } from '../users/types';
import { Roles } from '../auth/roles.decorator';
import { AuthGuard } from '@nestjs/passport';
import { ValidationPipe } from '@nestjs/common';
import { PantryApplicationDto } from './dtos/pantry-application.dto';
import { ApiBody } from '@nestjs/swagger';
Expand All @@ -26,6 +28,8 @@ import {
} from './types';
import { Order } from '../orders/order.entity';
import { OrdersService } from '../orders/order.service';
import { OwnershipGuard } from '../auth/ownership.guard';
import { CheckOwnership } from '../auth/ownership.decorator';
import { Public } from '../auth/public.decorator';

@Controller('pantries')
Expand Down Expand Up @@ -53,6 +57,14 @@ export class PantriesController {
return this.pantriesService.getPendingPantries();
}

@UseGuards(AuthGuard('jwt'), OwnershipGuard)
@CheckOwnership({
idParam: 'pantryId',
resolver: async ({ entityId, services }) => {
const pantry = await services.get(PantriesService).findOne(entityId);
return pantry?.pantryUser?.id ?? null;
},
})
@Roles(Role.PANTRY, Role.ADMIN)
@Get('/:pantryId')
async getPantry(
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/src/pantries/pantries.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { Pantry } from './pantries.entity';
import { AuthModule } from '../auth/auth.module';
import { OrdersModule } from '../orders/order.module';
import { User } from '../users/user.entity';
import { SharedAuthModule } from '../auth/sharedAuth.module';

@Module({
imports: [
TypeOrmModule.forFeature([Pantry, User]),
OrdersModule,
forwardRef(() => AuthModule),
SharedAuthModule,
],
controllers: [PantriesController],
providers: [PantriesService],
Expand Down
17 changes: 14 additions & 3 deletions apps/frontend/src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import axios, {
type AxiosInstance,
type InternalAxiosRequestConfig,
} from 'axios';
import { NavigateFunction } from 'react-router-dom';
import {
User,
Order,
Expand All @@ -27,6 +28,7 @@ const defaultBaseUrl =
export class ApiClient {
private axiosInstance: AxiosInstance;
private accessToken: string | undefined;
private navigate: NavigateFunction | null = null;

constructor() {
this.axiosInstance = axios.create({ baseURL: defaultBaseUrl });
Expand All @@ -48,15 +50,24 @@ export class ApiClient {
this.axiosInstance.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response?.status === 403) {
// TODO: For a future ticket, figure out a better method than renavigation on failure (or a better place to check than in the api requests)
window.location.replace('/unauthorized');
if (error.response?.status === 403 && this.navigate) {
const errorData = error.response?.data as { message?: string };
this.navigate('/unauthorized', {
replace: true,
state: {
errorMessage: errorData?.message || 'Access forbidden',
},
});
}
return Promise.reject(error);
},
);
}

public setNavigate(navigate: NavigateFunction) {
this.navigate = navigate;
}

public setAccessToken(token: string | undefined) {
this.accessToken = token;
}
Expand Down
10 changes: 8 additions & 2 deletions apps/frontend/src/components/forms/deliveryConfirmationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
Text,
Dialog,
} from '@chakra-ui/react';
import { Form, ActionFunction, ActionFunctionArgs } from 'react-router-dom';
import {
Form,
ActionFunction,
ActionFunctionArgs,
redirect,
} from 'react-router-dom';
import ApiClient from '@api/apiClient';

interface DeliveryConfirmationModalProps {
Expand Down Expand Up @@ -151,7 +156,7 @@ export const submitDeliveryConfirmationFormModal: ActionFunction = async ({
const form = await request.formData();
const confirmDeliveryData = new FormData();

const pantryId = form.get('pantryId');
const pantryId = form.get('pantryId') as string;
const requestId = form.get('requestId') as string;
confirmDeliveryData.append('requestId', requestId);

Expand All @@ -161,6 +166,7 @@ export const submitDeliveryConfirmationFormModal: ActionFunction = async ({
confirmDeliveryData.append('dateReceived', formattedDate);
} else {
alert('Delivery date is missing or invalid.');
return redirect(`/request-form/${pantryId}`);
}

confirmDeliveryData.append('feedback', form.get('feedback') as string);
Expand Down
Loading