Skip to content
Open
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
4 changes: 3 additions & 1 deletion apps/backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { AuthService } from './auth.service';
import { UsersService } from '../users/users.service';
import { User } from '../users/user.entity';
import { JwtStrategy } from './jwt.strategy';
import { RolesGuard } from './roles.guard';

@Module({
imports: [
TypeOrmModule.forFeature([User]),
PassportModule.register({ defaultStrategy: 'jwt' }),
],
controllers: [AuthController],
providers: [AuthService, UsersService, JwtStrategy],
providers: [AuthService, UsersService, JwtStrategy, RolesGuard],
exports: [AuthService, JwtStrategy],
})
export class AuthModule {}
36 changes: 25 additions & 11 deletions apps/backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import {
AdminDeleteUserCommand,
AdminInitiateAuthCommand,
AttributeType,
CognitoIdentityProviderClient,
ConfirmForgotPasswordCommand,
ConfirmSignUpCommand,
Expand Down Expand Up @@ -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<AttributeType[]> {
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<Role> {
const { role } = await this.getUser(userSub);
return role as Role;
}

async signup(
Expand All @@ -64,7 +78,7 @@ export class AuthService {
): Promise<boolean> {
// Needs error handling
const signUpCommand = new SignUpCommand({
ClientId: CognitoAuthConfig.clientId,
ClientId: CognitoAuthConfig.userPoolClientId,
SecretHash: this.calculateHash(email),
Username: email,
Password: password,
Expand All @@ -88,7 +102,7 @@ export class AuthService {

async verifyUser(email: string, verificationCode: string): Promise<void> {
const confirmCommand = new ConfirmSignUpCommand({
ClientId: CognitoAuthConfig.clientId,
ClientId: CognitoAuthConfig.userPoolClientId,
SecretHash: this.calculateHash(email),
Username: email,
ConfirmationCode: verificationCode,
Expand All @@ -100,7 +114,7 @@ export class AuthService {
async signin({ email, password }: SignInDto): Promise<SignInResponseDto> {
const signInCommand = new AdminInitiateAuthCommand({
AuthFlow: 'ADMIN_USER_PASSWORD_AUTH',
ClientId: CognitoAuthConfig.clientId,
ClientId: CognitoAuthConfig.userPoolClientId,
UserPoolId: CognitoAuthConfig.userPoolId,
AuthParameters: {
USERNAME: email,
Expand All @@ -125,7 +139,7 @@ export class AuthService {
}: RefreshTokenDto): Promise<SignInResponseDto> {
const refreshCommand = new AdminInitiateAuthCommand({
AuthFlow: 'REFRESH_TOKEN_AUTH',
ClientId: CognitoAuthConfig.clientId,
ClientId: CognitoAuthConfig.userPoolClientId,
UserPoolId: CognitoAuthConfig.userPoolId,
AuthParameters: {
REFRESH_TOKEN: refreshToken,
Expand All @@ -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),
});
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/auth/aws-exports.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const CognitoAuthConfig = {
userPoolId: 'us-east-1_oshVQXLX6',
clientId: '42bfm2o2pmk57mpm5399s0e9no',
userPoolClientId: '1kehn2mr64h94mire6os55bib7',
userPoolId: 'us-east-1_StSYXMibq',
region: 'us-east-1',
};

Expand Down
16 changes: 11 additions & 5 deletions apps/backend/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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;
}
}
5 changes: 5 additions & 0 deletions apps/backend/src/auth/roles.decorator.ts
Original file line number Diff line number Diff line change
@@ -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);
24 changes: 24 additions & 0 deletions apps/backend/src/auth/roles.guard.ts
Original file line number Diff line number Diff line change
@@ -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<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);

if (!requiredRoles) {
return true;
}

const { user } = context.switchToHttp().getRequest();

return requiredRoles.some((role) => user.role === role);
}
}
2 changes: 1 addition & 1 deletion apps/backend/src/aws/aws-s3.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class AWSS3Service {
}
return uploadedFileUrls;
} catch (error) {
throw new Error('File upload to AWS failed');
throw new Error(`File upload to AWS failed: ${error}`);
}
}
}
1 change: 0 additions & 1 deletion apps/backend/src/config/typeorm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const config = {
ReviseTables1737522923066,
UpdateUserRole1737816745912,
UpdatePantriesTable1737906317154,
UpdateDonations1738697216020,
UpdateDonationColTypes1741708808976,
UpdatePantriesTable1738172265266,
UpdatePantriesTable1739056029076,
Expand Down
12 changes: 11 additions & 1 deletion apps/backend/src/donationItems/donationItems.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { Controller, Post, Body, Param, Get, Patch } from '@nestjs/common';
import {
Controller,
Post,
Body,
Param,
Get,
Patch,
UseGuards,
} from '@nestjs/common';
import { ApiBody } from '@nestjs/swagger';
import { DonationItemsService } from './donationItems.service';
import { DonationItem } from './donationItems.entity';
import { AuthGuard } from '@nestjs/passport';

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

Expand Down
7 changes: 3 additions & 4 deletions apps/backend/src/donationItems/donationItems.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ 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';

@Module({
imports: [TypeOrmModule.forFeature([DonationItem])],
imports: [TypeOrmModule.forFeature([DonationItem]), AuthModule],
controllers: [DonationItemsController],
providers: [DonationItemsService, AuthService, JwtStrategy],
providers: [DonationItemsService],
})
export class DonationItemsModule {}
3 changes: 3 additions & 0 deletions apps/backend/src/donations/donations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import {
Patch,
Param,
NotFoundException,
UseGuards,
} from '@nestjs/common';
import { ApiBody } from '@nestjs/swagger';
import { Donation } from './donations.entity';
import { DonationService } from './donations.service';
import { AuthGuard } from '@nestjs/passport';

@Controller('donations')
@UseGuards(AuthGuard('jwt'))
export class DonationsController {
constructor(private donationService: DonationService) {}

Expand Down
11 changes: 7 additions & 4 deletions apps/backend/src/donations/donations.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
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';

@Module({
imports: [TypeOrmModule.forFeature([Donation]), ManufacturerModule],
imports: [
TypeOrmModule.forFeature([Donation]),
ManufacturerModule,
AuthModule,
],
controllers: [DonationsController],
providers: [DonationService, AuthService, JwtStrategy],
providers: [DonationService],
})
export class DonationModule {}
3 changes: 2 additions & 1 deletion apps/backend/src/foodManufacturers/manufacturer.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
10 changes: 10 additions & 0 deletions apps/backend/src/foodRequests/request.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,38 @@ import {
Body,
UploadedFiles,
UseInterceptors,
UseGuards,
} from '@nestjs/common';
import { ApiBody } from '@nestjs/swagger';
import { RequestsService } from './request.service';
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';

@Controller('requests')
// @UseInterceptors()
@UseGuards(RolesGuard)
@UseGuards(AuthGuard('jwt'))
export class FoodRequestsController {
constructor(
private requestsService: RequestsService,
private awsS3Service: AWSS3Service,
) {}

@Roles(Role.PANTRY)
@Get('/:pantryId')
async getAllPantryRequests(
@Param('pantryId', ParseIntPipe) pantryId: number,
): Promise<FoodRequest[]> {
return this.requestsService.find(pantryId);
}

@Roles(Role.PANTRY)
@Post('/create')
@ApiBody({
description: 'Details for creating a food request',
Expand Down Expand Up @@ -93,6 +102,7 @@ export class FoodRequestsController {
);
}

@Roles(Role.PANTRY)
@Post('/:requestId/confirm-delivery')
@ApiBody({
description: 'Details for a confirmation form',
Expand Down
6 changes: 3 additions & 3 deletions apps/backend/src/foodRequests/request.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { FoodRequestsController } 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';

@Module({
imports: [
AWSS3Module,
MulterModule.register({ dest: './uploads' }),
TypeOrmModule.forFeature([FoodRequest]),
AuthModule,
],
controllers: [FoodRequestsController],
providers: [RequestsService, AuthService, JwtStrategy],
providers: [RequestsService],
})
export class RequestsModule {}
15 changes: 5 additions & 10 deletions apps/backend/src/interceptors/current-user.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading