diff --git a/apps/backend/src/allocations/allocations.module.ts b/apps/backend/src/allocations/allocations.module.ts index fed7360b..3284e1af 100644 --- a/apps/backend/src/allocations/allocations.module.ts +++ b/apps/backend/src/allocations/allocations.module.ts @@ -1,15 +1,17 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Allocation } from './allocations.entity'; import { AllocationsController } from './allocations.controller'; import { AllocationsService } from './allocations.service'; -import { AuthService } from '../auth/auth.service'; -import { JwtStrategy } from '../auth/jwt.strategy'; +import { AuthModule } from '../auth/auth.module'; @Module({ - imports: [TypeOrmModule.forFeature([Allocation])], + imports: [ + TypeOrmModule.forFeature([Allocation]), + forwardRef(() => AuthModule), + ], controllers: [AllocationsController], - providers: [AllocationsService, AuthService, JwtStrategy], + providers: [AllocationsService], exports: [AllocationsService], }) export class AllocationModule {} diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts index eac8a5b6..3af03db0 100644 --- a/apps/backend/src/auth/auth.module.ts +++ b/apps/backend/src/auth/auth.module.ts @@ -1,14 +1,17 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; - import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { JwtStrategy } from './jwt.strategy'; import { UsersModule } from '../users/users.module'; @Module({ - imports: [UsersModule, PassportModule.register({ defaultStrategy: 'jwt' })], + imports: [ + forwardRef(() => UsersModule), + PassportModule.register({ defaultStrategy: 'jwt' }), + ], controllers: [AuthController], providers: [AuthService, JwtStrategy], + exports: [AuthService, JwtStrategy], }) export class AuthModule {} diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index a0bae3ad..e88d532e 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { AdminDeleteUserCommand, AdminInitiateAuthCommand, - AttributeType, CognitoIdentityProviderClient, ConfirmForgotPasswordCommand, ConfirmSignUpCommand, @@ -43,19 +42,34 @@ export class AuthService { // (see https://docs.aws.amazon.com/cognito/latest/developerguide/signing-up-users-in-your-app.html#cognito-user-pools-computing-secret-hash) calculateHash(username: string): string { const hmac = createHmac('sha256', this.clientSecret); - hmac.update(username + CognitoAuthConfig.clientId); + hmac.update(username + CognitoAuthConfig.userPoolClientId); return hmac.digest('base64'); } - async getUser(userSub: string): Promise { + async getUser(userSub: string): Promise<{ email: string; role: string }> { const listUsersCommand = new ListUsersCommand({ UserPoolId: CognitoAuthConfig.userPoolId, Filter: `sub = "${userSub}"`, }); - // TODO need error handling const { Users } = await this.providerClient.send(listUsersCommand); - return Users[0].Attributes; + if (!Users || Users.length === 0) { + throw new Error('User not found'); + } + + const attributes = Users[0].Attributes; + + const email = attributes.find((attr) => attr.Name === 'email')?.Value || ''; + const role = + attributes.find((attr) => attr.Name === 'custom:role')?.Value || + 'VOLUNTEER'; + + return { email, role }; + } + + async getUserRole(userSub: string): Promise { + const { role } = await this.getUser(userSub); + return role as Role; } async signup( @@ -64,7 +78,7 @@ export class AuthService { ): Promise { // Needs error handling const signUpCommand = new SignUpCommand({ - ClientId: CognitoAuthConfig.clientId, + ClientId: CognitoAuthConfig.userPoolClientId, SecretHash: this.calculateHash(email), Username: email, Password: password, @@ -88,7 +102,7 @@ export class AuthService { async verifyUser(email: string, verificationCode: string): Promise { const confirmCommand = new ConfirmSignUpCommand({ - ClientId: CognitoAuthConfig.clientId, + ClientId: CognitoAuthConfig.userPoolClientId, SecretHash: this.calculateHash(email), Username: email, ConfirmationCode: verificationCode, @@ -100,7 +114,7 @@ export class AuthService { async signin({ email, password }: SignInDto): Promise { const signInCommand = new AdminInitiateAuthCommand({ AuthFlow: 'ADMIN_USER_PASSWORD_AUTH', - ClientId: CognitoAuthConfig.clientId, + ClientId: CognitoAuthConfig.userPoolClientId, UserPoolId: CognitoAuthConfig.userPoolId, AuthParameters: { USERNAME: email, @@ -125,7 +139,7 @@ export class AuthService { }: RefreshTokenDto): Promise { const refreshCommand = new AdminInitiateAuthCommand({ AuthFlow: 'REFRESH_TOKEN_AUTH', - ClientId: CognitoAuthConfig.clientId, + ClientId: CognitoAuthConfig.userPoolClientId, UserPoolId: CognitoAuthConfig.userPoolId, AuthParameters: { REFRESH_TOKEN: refreshToken, @@ -144,7 +158,7 @@ export class AuthService { async forgotPassword(email: string) { const forgotCommand = new ForgotPasswordCommand({ - ClientId: CognitoAuthConfig.clientId, + ClientId: CognitoAuthConfig.userPoolClientId, Username: email, SecretHash: this.calculateHash(email), }); @@ -158,7 +172,7 @@ export class AuthService { newPassword, }: ConfirmPasswordDto) { const confirmComamnd = new ConfirmForgotPasswordCommand({ - ClientId: CognitoAuthConfig.clientId, + ClientId: CognitoAuthConfig.userPoolClientId, SecretHash: this.calculateHash(email), Username: email, ConfirmationCode: confirmationCode, diff --git a/apps/backend/src/auth/aws-exports.ts b/apps/backend/src/auth/aws-exports.ts index 48541a19..97711cea 100644 --- a/apps/backend/src/auth/aws-exports.ts +++ b/apps/backend/src/auth/aws-exports.ts @@ -1,6 +1,6 @@ const CognitoAuthConfig = { - userPoolId: 'us-east-1_oshVQXLX6', - clientId: '42bfm2o2pmk57mpm5399s0e9no', + userPoolClientId: '1kehn2mr64h94mire6os55bib7', + userPoolId: 'us-east-1_StSYXMibq', region: 'us-east-1', }; diff --git a/apps/backend/src/auth/jwt.strategy.ts b/apps/backend/src/auth/jwt.strategy.ts index 44d8789d..439acf6c 100644 --- a/apps/backend/src/auth/jwt.strategy.ts +++ b/apps/backend/src/auth/jwt.strategy.ts @@ -1,19 +1,23 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { passportJwtSecret } from 'jwks-rsa'; import { ExtractJwt, Strategy } from 'passport-jwt'; - +import { UsersService } from '../users/users.service'; import CognitoAuthConfig from './aws-exports'; +import { AuthService } from './auth.service'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { - constructor() { + constructor( + private usersService: UsersService, + private authService: AuthService, + ) { const cognitoAuthority = `https://cognito-idp.${CognitoAuthConfig.region}.amazonaws.com/${CognitoAuthConfig.userPoolId}`; super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - _audience: CognitoAuthConfig.clientId, + _audience: CognitoAuthConfig.userPoolClientId, issuer: cognitoAuthority, algorithms: ['RS256'], secretOrKeyProvider: passportJwtSecret({ @@ -26,6 +30,8 @@ export class JwtStrategy extends PassportStrategy(Strategy) { } async validate(payload) { - return { idUser: payload.sub, email: payload.email }; + const user = await this.authService.getUser(payload.sub); + const dbUser = await this.usersService.findByEmail(user.email); + return dbUser; } } diff --git a/apps/backend/src/auth/roles.decorator.ts b/apps/backend/src/auth/roles.decorator.ts new file mode 100644 index 00000000..a28701d2 --- /dev/null +++ b/apps/backend/src/auth/roles.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; +import { Role } from '../users/types'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); diff --git a/apps/backend/src/auth/roles.guard.ts b/apps/backend/src/auth/roles.guard.ts new file mode 100644 index 00000000..b2e71324 --- /dev/null +++ b/apps/backend/src/auth/roles.guard.ts @@ -0,0 +1,24 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Role } from '../users/types'; +import { ROLES_KEY } from './roles.decorator'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (!requiredRoles) { + return true; + } + + const { user } = context.switchToHttp().getRequest(); + + return requiredRoles.some((role) => user.role === role); + } +} diff --git a/apps/backend/src/config/typeorm.ts b/apps/backend/src/config/typeorm.ts index 82384673..b467c967 100644 --- a/apps/backend/src/config/typeorm.ts +++ b/apps/backend/src/config/typeorm.ts @@ -46,8 +46,8 @@ const config = { ReviseTables1737522923066, UpdateUserRole1737816745912, UpdatePantriesTable1737906317154, - UpdateDonations1738697216020, UpdateDonationColTypes1741708808976, + UpdateDonations1738697216020, UpdatePantriesTable1738172265266, UpdatePantriesTable1739056029076, AssignmentsPantryIdNotUnique1758384669652, diff --git a/apps/backend/src/donationItems/donationItems.controller.ts b/apps/backend/src/donationItems/donationItems.controller.ts index 191b53fb..56e83278 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -5,16 +5,19 @@ 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'; @Controller('donation-items') //@UseInterceptors() +@UseGuards(AuthGuard('jwt')) export class DonationItemsController { constructor(private donationItemsService: DonationItemsService) {} diff --git a/apps/backend/src/donationItems/donationItems.module.ts b/apps/backend/src/donationItems/donationItems.module.ts index a416372f..ef377d2b 100644 --- a/apps/backend/src/donationItems/donationItems.module.ts +++ b/apps/backend/src/donationItems/donationItems.module.ts @@ -2,14 +2,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { DonationItemsService } from './donationItems.service'; import { DonationItem } from './donationItems.entity'; -import { JwtStrategy } from '../auth/jwt.strategy'; -import { AuthService } from '../auth/auth.service'; import { DonationItemsController } from './donationItems.controller'; +import { AuthModule } from '../auth/auth.module'; import { Donation } from '../donations/donations.entity'; @Module({ - imports: [TypeOrmModule.forFeature([DonationItem, Donation])], + imports: [TypeOrmModule.forFeature([DonationItem, Donation]), AuthModule], controllers: [DonationItemsController], - providers: [DonationItemsService, AuthService, JwtStrategy], + providers: [DonationItemsService], }) export class DonationItemsModule {} diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts index 6bcd2a7e..c0050c45 100644 --- a/apps/backend/src/donations/donations.controller.ts +++ b/apps/backend/src/donations/donations.controller.ts @@ -6,15 +6,18 @@ import { Patch, Param, NotFoundException, + UseGuards, ParseIntPipe, BadRequestException, } from '@nestjs/common'; import { ApiBody } from '@nestjs/swagger'; import { Donation } from './donations.entity'; import { DonationService } from './donations.service'; +import { AuthGuard } from '@nestjs/passport'; import { DonationStatus } from './types'; @Controller('donations') +@UseGuards(AuthGuard('jwt')) export class DonationsController { constructor(private donationService: DonationService) {} diff --git a/apps/backend/src/donations/donations.module.ts b/apps/backend/src/donations/donations.module.ts index 311971d1..79ee1a2e 100644 --- a/apps/backend/src/donations/donations.module.ts +++ b/apps/backend/src/donations/donations.module.ts @@ -1,19 +1,19 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { JwtStrategy } from '../auth/jwt.strategy'; -import { AuthService } from '../auth/auth.service'; import { Donation } from './donations.entity'; import { DonationService } from './donations.service'; import { DonationsController } from './donations.controller'; import { ManufacturerModule } from '../foodManufacturers/manufacturer.module'; +import { AuthModule } from '../auth/auth.module'; import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; @Module({ imports: [ TypeOrmModule.forFeature([Donation, FoodManufacturer]), ManufacturerModule, + AuthModule, ], controllers: [DonationsController], - providers: [DonationService, AuthService, JwtStrategy], + providers: [DonationService], }) export class DonationModule {} diff --git a/apps/backend/src/foodManufacturers/manufacturer.module.ts b/apps/backend/src/foodManufacturers/manufacturer.module.ts index 2ba2b117..dcb5a32f 100644 --- a/apps/backend/src/foodManufacturers/manufacturer.module.ts +++ b/apps/backend/src/foodManufacturers/manufacturer.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { FoodManufacturer } from './manufacturer.entity'; +import { AuthModule } from '../auth/auth.module'; @Module({ - imports: [TypeOrmModule.forFeature([FoodManufacturer])], + imports: [TypeOrmModule.forFeature([FoodManufacturer]), AuthModule], }) export class ManufacturerModule {} diff --git a/apps/backend/src/foodRequests/request.controller.ts b/apps/backend/src/foodRequests/request.controller.ts index 1f449491..a34e6cb3 100644 --- a/apps/backend/src/foodRequests/request.controller.ts +++ b/apps/backend/src/foodRequests/request.controller.ts @@ -7,6 +7,7 @@ import { Body, UploadedFiles, UseInterceptors, + UseGuards, BadRequestException, } from '@nestjs/common'; import { ApiBody } from '@nestjs/swagger'; @@ -15,6 +16,10 @@ import { FoodRequest } from './request.entity'; import { AWSS3Service } from '../aws/aws-s3.service'; import { FilesInterceptor } from '@nestjs/platform-express'; import * as multer from 'multer'; +import { AuthGuard } from '@nestjs/passport'; +import { Roles } from '../auth/roles.decorator'; +import { Role } from '../users/types'; +import { RolesGuard } from '../auth/roles.guard'; import { OrdersService } from '../orders/order.service'; import { Order } from '../orders/order.entity'; import { RequestSize } from './types'; @@ -22,6 +27,8 @@ import { OrderStatus } from '../orders/types'; @Controller('requests') // @UseInterceptors() +@UseGuards(RolesGuard) +@UseGuards(AuthGuard('jwt')) export class RequestsController { constructor( private requestsService: RequestsService, @@ -29,6 +36,7 @@ export class RequestsController { private ordersService: OrdersService, ) {} + @Roles(Role.PANTRY) @Get('/:requestId') async getRequest( @Param('requestId', ParseIntPipe) requestId: number, @@ -43,6 +51,7 @@ export class RequestsController { return this.requestsService.find(pantryId); } + @Roles(Role.PANTRY) @Post('/create') @ApiBody({ description: 'Details for creating a food request', @@ -109,6 +118,7 @@ export class RequestsController { ); } + @Roles(Role.PANTRY) @Post('/:requestId/confirm-delivery') @ApiBody({ description: 'Details for a confirmation form', diff --git a/apps/backend/src/foodRequests/request.module.ts b/apps/backend/src/foodRequests/request.module.ts index 14a605d8..0e5dc280 100644 --- a/apps/backend/src/foodRequests/request.module.ts +++ b/apps/backend/src/foodRequests/request.module.ts @@ -3,10 +3,9 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { RequestsController } from './request.controller'; import { FoodRequest } from './request.entity'; import { RequestsService } from './request.service'; -import { JwtStrategy } from '../auth/jwt.strategy'; -import { AuthService } from '../auth/auth.service'; import { AWSS3Module } from '../aws/aws-s3.module'; import { MulterModule } from '@nestjs/platform-express'; +import { AuthModule } from '../auth/auth.module'; import { OrdersService } from '../orders/order.service'; import { Order } from '../orders/order.entity'; import { Pantry } from '../pantries/pantries.entity'; @@ -16,8 +15,9 @@ import { Pantry } from '../pantries/pantries.entity'; AWSS3Module, MulterModule.register({ dest: './uploads' }), TypeOrmModule.forFeature([FoodRequest, Order, Pantry]), + AuthModule, ], controllers: [RequestsController], - providers: [RequestsService, OrdersService, AuthService, JwtStrategy], + providers: [RequestsService, OrdersService], }) export class RequestsModule {} diff --git a/apps/backend/src/interceptors/current-user.interceptor.ts b/apps/backend/src/interceptors/current-user.interceptor.ts index e60b545a..02becfde 100644 --- a/apps/backend/src/interceptors/current-user.interceptor.ts +++ b/apps/backend/src/interceptors/current-user.interceptor.ts @@ -16,18 +16,13 @@ export class CurrentUserInterceptor implements NestInterceptor { async intercept(context: ExecutionContext, handler: CallHandler) { const request = context.switchToHttp().getRequest(); - const cognitoUserAttributes = await this.authService.getUser( - request.user.userId, - ); - const userEmail = cognitoUserAttributes.find( - (attribute) => attribute.Name === 'email', - ).Value; - const users = await this.usersService.find(userEmail); - if (users.length > 0) { - const user = users[0]; + if (request.user) { + const user = await this.authService.getUser(request.user.sub); - request.user = user; + const dbUser = await this.usersService.findByEmail(user.email); + console.log(dbUser); + request.currentUser = dbUser; } return handler.handle(); diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index c6b307a0..ca2c5ddd 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -1,16 +1,19 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; 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 { AllocationModule } from '../allocations/allocations.module'; +import { AuthModule } from '../auth/auth.module'; @Module({ - imports: [TypeOrmModule.forFeature([Order]), AllocationModule], + imports: [ + TypeOrmModule.forFeature([Order]), + AllocationModule, + forwardRef(() => AuthModule), + ], controllers: [OrdersController], - providers: [OrdersService, AuthService, JwtStrategy], + providers: [OrdersService], exports: [OrdersService], }) export class OrdersModule {} diff --git a/apps/backend/src/pantries/pantries.controller.ts b/apps/backend/src/pantries/pantries.controller.ts index ee8287ce..54765cdc 100644 --- a/apps/backend/src/pantries/pantries.controller.ts +++ b/apps/backend/src/pantries/pantries.controller.ts @@ -5,10 +5,15 @@ import { Param, ParseIntPipe, Post, - ValidationPipe, + UseGuards, } from '@nestjs/common'; import { Pantry } from './pantries.entity'; import { PantriesService } from './pantries.service'; +import { RolesGuard } from '../auth/roles.guard'; +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'; import { @@ -23,17 +28,21 @@ import { Order } from '../orders/order.entity'; import { OrdersService } from '../orders/order.service'; @Controller('pantries') +// @UseInterceptors(CurrentUserInterceptor) +@UseGuards(AuthGuard('jwt'), RolesGuard) export class PantriesController { constructor( private pantriesService: PantriesService, private ordersService: OrdersService, ) {} + @Roles(Role.ADMIN) @Get('/pending') async getPendingPantries(): Promise { return this.pantriesService.getPendingPantries(); } + @Roles(Role.PANTRY, Role.ADMIN) @Get('/:pantryId') async getPantry( @Param('pantryId', ParseIntPipe) pantryId: number, @@ -41,6 +50,7 @@ export class PantriesController { return this.pantriesService.findOne(pantryId); } + @Roles(Role.ADMIN) @Get('/:pantryId/orders') async getOrders( @Param('pantryId', ParseIntPipe) pantryId: number, @@ -219,6 +229,7 @@ export class PantriesController { return this.pantriesService.approve(pantryId); } + @Roles(Role.ADMIN) @Post('/deny/:pantryId') async denyPantry( @Param('pantryId', ParseIntPipe) pantryId: number, diff --git a/apps/backend/src/pantries/pantries.module.ts b/apps/backend/src/pantries/pantries.module.ts index 3de2a4c5..5e60b78d 100644 --- a/apps/backend/src/pantries/pantries.module.ts +++ b/apps/backend/src/pantries/pantries.module.ts @@ -1,12 +1,18 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { PantriesService } from './pantries.service'; import { PantriesController } from './pantries.controller'; import { Pantry } from './pantries.entity'; +import { AuthModule } from '../auth/auth.module'; import { OrdersModule } from '../orders/order.module'; +import { User } from '../users/user.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Pantry]), OrdersModule], + imports: [ + TypeOrmModule.forFeature([Pantry, User]), + OrdersModule, + forwardRef(() => AuthModule), + ], controllers: [PantriesController], providers: [PantriesService], exports: [PantriesService], diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index 6f11265d..41716e89 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -4,6 +4,8 @@ import { Get, Param, ParseIntPipe, + UseGuards, + UseInterceptors, Put, Post, BadRequestException, @@ -12,15 +14,13 @@ import { //UseInterceptors, } from '@nestjs/common'; import { UsersService } from './users.service'; -//import { AuthGuard } from '@nestjs/passport'; +import { AuthGuard } from '@nestjs/passport'; import { User } from './user.entity'; import { Role } from './types'; import { userSchemaDto } from './dtos/userSchema.dto'; import { Pantry } from '../pantries/pantries.entity'; -//import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; @Controller('users') -//@UseInterceptors(CurrentUserInterceptor) export class UsersController { constructor(private usersService: UsersService) {} diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts index 6a780a8d..23177621 100644 --- a/apps/backend/src/users/users.module.ts +++ b/apps/backend/src/users/users.module.ts @@ -1,17 +1,19 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { User } from './user.entity'; -import { JwtStrategy } from '../auth/jwt.strategy'; -import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; -import { AuthService } from '../auth/auth.service'; import { PantriesModule } from '../pantries/pantries.module'; +import { AuthModule } from '../auth/auth.module'; @Module({ - imports: [TypeOrmModule.forFeature([User]), PantriesModule], - exports: [UsersService], + imports: [ + TypeOrmModule.forFeature([User]), + forwardRef(() => PantriesModule), + forwardRef(() => AuthModule), + ], controllers: [UsersController], - providers: [UsersService, AuthService, JwtStrategy, CurrentUserInterceptor], + providers: [UsersService], + exports: [UsersService], }) export class UsersModule {} diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 65f90ae1..5d4e359d 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -5,7 +5,6 @@ import { } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; - import { User } from './user.entity'; import { Role } from './types'; import { validateId } from '../utils/validation.utils'; @@ -66,8 +65,8 @@ export class UsersService { return volunteer; } - find(email: string) { - return this.repo.find({ where: { email } }); + async findByEmail(email: string): Promise { + return this.repo.findOneBy({ email }); } async update(id: number, attrs: Partial) { diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index e20724bb..fd45e7a9 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -1,4 +1,9 @@ -import axios, { type AxiosInstance, AxiosResponse } from 'axios'; +import axios, { + AxiosError, + AxiosResponse, + type AxiosInstance, + type InternalAxiosRequestConfig, +} from 'axios'; import { User, Order, @@ -19,9 +24,37 @@ const defaultBaseUrl = export class ApiClient { private axiosInstance: AxiosInstance; + private accessToken: string | undefined; constructor() { this.axiosInstance = axios.create({ baseURL: defaultBaseUrl }); + + this.axiosInstance.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + const token = this.accessToken || localStorage.getItem('accessToken'); + if (token) { + config.headers = config.headers || {}; + config.headers['Authorization'] = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error), + ); + + 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'); + } + return Promise.reject(error); + }, + ); + } + + public setAccessToken(token: string | undefined) { + this.accessToken = token; } public async getHello(): Promise { @@ -229,10 +262,21 @@ export class ApiClient { requestId: number, data: FormData, ): Promise { - await this.axiosInstance.post( - `/api/requests/${requestId}/confirm-delivery`, - data, - ); + try { + const response = await this.axiosInstance.post( + `/api/requests/${requestId}/confirm-delivery`, + data, + ); + + if (response.status === 200) { + alert('Delivery confirmation submitted successfully'); + window.location.href = '/request-form/1'; + } else { + alert(`Failed to submit: ${response.statusText}`); + } + } catch (error) { + alert(`Error submitting delivery confirmation: ${error}`); + } } } diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index e31dc284..3527142a 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -1,7 +1,4 @@ -import { useEffect } from 'react'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; - -import apiClient from '@api/apiClient'; import Root from '@containers/root'; import NotFound from '@containers/404'; import LandingPage from '@containers/landingPage'; @@ -29,6 +26,7 @@ import { Authenticator } from '@aws-amplify/ui-react'; import { Amplify } from 'aws-amplify'; import CognitoAuthConfig from './aws-exports'; import { Button } from '@chakra-ui/react'; +import Unauthorized from '@containers/unauthorized'; Amplify.configure(CognitoAuthConfig); @@ -82,6 +80,54 @@ const router = createBrowserRouter([ element: , action: submitPantryApplicationForm, }, + + { + path: '/unauthorized', + element: , + }, + + // Private routes (protected by auth) + + { + path: '/landing-page', + element: ( + + + + ), + }, + { + path: '/pantry-overview', + element: ( + + + + ), + }, + { + path: '/pantry-dashboard/:pantryId', + element: ( + + + + ), + }, + { + path: '/pantry-past-orders', + element: ( + + + + ), + }, + { + path: '/pantries', + element: ( + + + + ), + }, { path: '/pantry-application/submitted', element: , @@ -143,21 +189,21 @@ const router = createBrowserRouter([ ), - loader: pantryIdLoader, }, { - path: '/donation-management', + path: '/approve-pantries', element: ( - + ), + loader: pantryIdLoader, }, { - path: '/approve-pantries', + path: '/donation-management', element: ( - + ), }, @@ -199,10 +245,10 @@ const router = createBrowserRouter([ ]); export const App: React.FC = () => { - useEffect(() => { - document.title = 'SSF'; - apiClient.getHello().then((res) => console.log(res)); - }, []); + // useEffect(() => { + // document.title = 'SSF'; + // apiClient.getHello().then((res) => console.log(res)); + // }, []); return ( diff --git a/apps/frontend/src/aws-exports.ts b/apps/frontend/src/aws-exports.ts index e27da0b4..ad17bcd3 100644 --- a/apps/frontend/src/aws-exports.ts +++ b/apps/frontend/src/aws-exports.ts @@ -4,6 +4,9 @@ const CognitoAuthConfig = { userPoolClientId: '198bdfe995p1kb4jnopt3sk6i1', userPoolId: 'us-east-1_StSYXMibq', region: 'us-east-1', + loginWith: { + email: true, + }, }, }, }; diff --git a/apps/frontend/src/containers/approvePantries.tsx b/apps/frontend/src/containers/approvePantries.tsx index bbe3882a..f2042add 100644 --- a/apps/frontend/src/containers/approvePantries.tsx +++ b/apps/frontend/src/containers/approvePantries.tsx @@ -22,8 +22,8 @@ const ApprovePantries: React.FC = () => { try { const data = await ApiClient.getAllPendingPantries(); setPendingPantries(data); - } catch (error) { - alert('Error fetching unapproved pantries: ' + error); + } catch (err) { + console.log(err); } }; diff --git a/apps/frontend/src/containers/landingPage.tsx b/apps/frontend/src/containers/landingPage.tsx index 0e1a72b4..98bbd7ed 100644 --- a/apps/frontend/src/containers/landingPage.tsx +++ b/apps/frontend/src/containers/landingPage.tsx @@ -1,5 +1,18 @@ +import { Button } from '@chakra-ui/react'; +import SignOutButton from '@components/signOutButton'; +import { signOut } from 'aws-amplify/auth'; + const LandingPage: React.FC = () => { - return <>Landing page; + const handleSignOut = async () => { + await signOut(); + }; + + return ( + <> + Landing page + + + ); }; export default LandingPage; diff --git a/apps/frontend/src/containers/unauthorized.tsx b/apps/frontend/src/containers/unauthorized.tsx new file mode 100644 index 00000000..83c687b7 --- /dev/null +++ b/apps/frontend/src/containers/unauthorized.tsx @@ -0,0 +1,10 @@ +export const Unauthorized: React.FC = () => { + return ( +
+

Oops!

+

You are not an authorized user for this page!

+
+ ); +}; + +export default Unauthorized;