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
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,11 @@ DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=securing-safe-food
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=PLACEHOLDER_PASSWORD
DATABASE_PASSWORD=PLACEHOLDER_PASSWORD

AWS_ACCESS_KEY_ID = PLACEHOLDER_AWS_ACCESS_KEY
AWS_SECRET_ACCESS_KEY = PLACEHOLDER_AWS_SECRET_KEY
AWS_REGION = PLACEHOLDER_AWS_REGION
COGNITO_CLIENT_SECRET = PLACEHOLDER_COGNITO_CLIENT_SECRET

AWS_BUCKET_NAME = 'confirm-delivery-photos'
41 changes: 40 additions & 1 deletion apps/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,43 @@ You can check that your database connection details are correct by running `nx s
"LOG 🚀 Application is running on: http://localhost:3000/api"
```

Finally, run `yarn run typeorm:migrate` to load all the tables into your database. If everything is set up correctly, you should see "Migration ... has been executed successfully." in the terminal.
Finally, run `yarn run typeorm:migrate` to load all the tables into your database. If everything is set up correctly, you should see "Migration ... has been executed successfully." in the terminal.

# AWS Setup

We have a few environment variables that we utilize to access several AWS services throughout the application. Below is a list of each of them and how to access each after logging in to AWS

1. `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`:
- Click on your username in the top right corner, and navigate to Security Credentials
- Scroll down to access keys, and create a new key
- Select "Local code" as the purpose for the key, and add an optional description
- Replace both the public and secret keys in the .env file to those values. Note that the secret key will not be accessible after you leave this page
- Click done

2. `AWS_REGION`:
This can be found next to your profile name when you login to the main page. Some accounts may be different, but we generally use us-east-1 or us-east-2.
This is the region that you find on the right side after clicking on the location dropdown, usually saying "United States (*some region*)".
For example, if we want to use Ohio as the region, we would put `AWS_REGION="us-east2"`

3. `AWS_BUCKET_NAME`:
This one is already given to you. As of right now, we only use one bucket, confirm-delivery-photos to store photos in a public S3 Bucket. This may be subject to change as we use S3 more in the project.

4. `COGNITO_CLIENT_SECRET`:
This is used to help authenticate you with AWS Cognito and allow you to properly sign in using proper credential. To find this:
- Navigate to AWS Cognito
- Make sure you are on "United States (N. Virginia) as your region
- Go into User pools and click on the one that says "ssf" (NOTE: You can also validate the User pool id in the `auth/aws_exports.ts` file)
- Go to App Clients, and click on 'ssf client w secret'
- There, you can validate the information in `auth/aws_exports.ts` (the `userPoolClientId`), as well as copy the client secret into your env file

5. Creating a new user within AWS Cognito
There are 2 ways you can create a new user in AWS Cognito. The simplest, is through loading the up, going to the landing page, and creating a new account there. If you choose to do it alternatively through the console, follow these steps:
- Navigate to AWS Cognito
- Make sure you are on "United States (N. Virginia) as your region
- Go into User pools and click on the one that says "ssf"
- Go to Users
- If you do not already see your email there, create a new User, setting an email in password (this will be what you login with on the frontend)
- Click 'Create User'
- Load up the app, and go to the landing page
- Verify you are able to login with these new credentials you created

12 changes: 7 additions & 5 deletions apps/backend/src/allocations/allocations.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
9 changes: 6 additions & 3 deletions apps/backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
30 changes: 9 additions & 21 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 All @@ -29,8 +28,8 @@ export class AuthService {
this.providerClient = new CognitoIdentityProviderClient({
region: CognitoAuthConfig.region,
credentials: {
accessKeyId: process.env.NX_AWS_ACCESS_KEY,
secretAccessKey: process.env.NX_AWS_SECRET_ACCESS_KEY,
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});

Expand All @@ -43,28 +42,17 @@ 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[]> {
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;
}

async signup(
{ firstName, lastName, email, password }: SignUpDto,
role: Role = Role.VOLUNTEER,
): 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 +76,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 +88,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 +113,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 +132,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 +146,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
15 changes: 10 additions & 5 deletions apps/backend/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a types package we can add to get better typing here

constructor() {
constructor(
private usersService: UsersService,
) {
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 +29,8 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
}

async validate(payload) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we leave a comment here to clarify that when this function is called, we know the payload parameter represents a valid signed JWT (so the user's sub is actually what they say it is) because the Passport JWT strategy takes care of that for us? Given the name validate I initially assumed we would actually have to do the validation of the JWT here, although after looking at docs I can see that what we have here should be sufficient

return { idUser: payload.sub, email: payload.email };
const dbUser = await this.usersService.findUserByCognitoId(payload.sub);
console.log('Database user retrieved:', dbUser);
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';

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we document this decorator to explain what it does and how it interacts with the roles guard? This will be confusing for people not familiar with how decorators and guards work in Nest

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than adding this to every controller separately, can we set it up as a global guard (as long as we make sure to return true if a route doesn't have any roles metadata set)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we document this to explain what it does and how it interacts with the roles decorator (including the behavior from getAllAndOverride)?

constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a comment clarifying that if this returns false, Nest automatically throws a ForbiddenException (403)?

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);
}
}
4 changes: 3 additions & 1 deletion apps/backend/src/config/typeorm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { RemoveMultipleVolunteerTypes1764811878152 } from '../migrations/1764811
import { RemoveUnusedStatuses1764816885341 } from '../migrations/1764816885341-RemoveUnusedStatuses';
import { UpdatePantryFields1763762628431 } from '../migrations/1763762628431-UpdatePantryFields';
import { PopulateDummyData1768501812134 } from '../migrations/1768501812134-populateDummyData';
import { AddUserPoolId1769189327767 } from '../migrations/1769189327767-AddUserPoolId';

const config = {
type: 'postgres',
Expand All @@ -46,8 +47,8 @@ const config = {
ReviseTables1737522923066,
UpdateUserRole1737816745912,
UpdatePantriesTable1737906317154,
UpdateDonations1738697216020,
UpdateDonationColTypes1741708808976,
UpdateDonations1738697216020,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this intentional?

UpdatePantriesTable1738172265266,
UpdatePantriesTable1739056029076,
AssignmentsPantryIdNotUnique1758384669652,
Expand All @@ -67,6 +68,7 @@ const config = {
RemoveMultipleVolunteerTypes1764811878152,
RemoveUnusedStatuses1764816885341,
PopulateDummyData1768501812134,
AddUserPoolId1769189327767,
],
};

Expand Down
3 changes: 3 additions & 0 deletions apps/backend/src/donationItems/donationItems.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

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,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 {}
2 changes: 2 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,14 @@ 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')
Expand Down
6 changes: 3 additions & 3 deletions apps/backend/src/donations/donations.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
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 {}
Loading
Loading