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
16 changes: 9 additions & 7 deletions apps/backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { RefreshTokenDto } from './dtos/refresh-token.dto';
import { ConfirmPasswordDto } from './dtos/confirm-password.dto';
import { ForgotPasswordDto } from './dtos/forgot-password.dto';
import { Role } from '../users/types';
import { userSchemaDto } from '../users/dtos/userSchema.dto';

@Controller('auth')
export class AuthController {
Expand All @@ -29,13 +30,14 @@ export class AuthController {
throw new BadRequestException(e.message);
}

const user = await this.usersService.create(
signUpDto.email,
signUpDto.firstName,
signUpDto.lastName,
signUpDto.phone,
Role.VOLUNTEER,
);
const createUserDto: userSchemaDto = {
email: signUpDto.email,
firstName: signUpDto.firstName,
lastName: signUpDto.lastName,
phone: signUpDto.phone,
role: Role.VOLUNTEER,
};
const user = await this.usersService.create(createUserDto);

return user;
}
Expand Down
38 changes: 37 additions & 1 deletion apps/backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Injectable } from '@nestjs/common';
import {
ConflictException,
Injectable,
InternalServerErrorException,
} from '@nestjs/common';
import {
AdminDeleteUserCommand,
AdminInitiateAuthCommand,
Expand All @@ -7,6 +11,7 @@ import {
ConfirmSignUpCommand,
ForgotPasswordCommand,
SignUpCommand,
AdminCreateUserCommand,
} from '@aws-sdk/client-cognito-identity-provider';

import CognitoAuthConfig from './aws-exports';
Expand Down Expand Up @@ -73,6 +78,37 @@ export class AuthService {
return response.UserConfirmed;
}

async adminCreateUser({
firstName,
lastName,
email,
}: Omit<SignUpDto, 'password' | 'phone'>): Promise<string> {
const createUserCommand = new AdminCreateUserCommand({
UserPoolId: CognitoAuthConfig.userPoolId,
Username: email,
UserAttributes: [
{ Name: 'name', Value: `${firstName} ${lastName}` },
{ Name: 'email', Value: email },
{ Name: 'email_verified', Value: 'true' },
],
DesiredDeliveryMediums: ['EMAIL'],
Copy link
Member

Choose a reason for hiding this comment

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

I think we should change the email from the default to 1) indicate that their application was approved and 2) provide further information other than simply the password, like how to access the signup page, or even a link to the application. When I got the email I wasn't quite sure what to do with it

You can look at my cognito docs for instructions on how to change the default email, lmk if you have any questions

Copy link
Author

Choose a reason for hiding this comment

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

how to access this page will definitely change, but we can just put the link as localhost for right now, and change it later, so i like the idea of more information on what to do with the password as well.

the application approved sounds good too, but im not sure if there is a separate automated email for that. i know in many modern flows (for example, email verification after you submit a job application), you receive 2 emails: one saying your application was submitted, and another being to verify your email. if we have a separate one for this, i suggest we keep the template basic on how to setup the account.

Copy link
Member

Choose a reason for hiding this comment

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

I'm fine to table this discussion for now since the email template can be easily changed without code later.

If the intended flow is to have two emails that's fine with me, although it seems easier to implement and simpler to just have 1 email from cognito rather than having a call to cognito and a call to ses.

});

try {
const response = await this.providerClient.send(createUserCommand);
const sub = response.User?.Attributes?.find(
(attr) => attr.Name === 'sub',
)?.Value;
return sub;
} catch (error) {
if (error.name == 'UsernameExistsException') {
throw new ConflictException('A user with this email already exists');
Copy link
Member

Choose a reason for hiding this comment

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

if there is an email conflict doesn't it make more sense to notify the user when they submit the application rather than the admin who is approving/denying the application? there isn't really anything the admin can do if they find that an email already exists in the db other than reach out directly

Copy link
Author

Choose a reason for hiding this comment

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

hmm this is a good question, and likely related to product.

for the pantry and food manufacturer flow: a DB user should be created with an empty cognito sub when the application is submitted. we could make an api call to check if the primary email exists in cognito and throw an error otherwise (since having a cognito account makes them an official user) and not allow for modal submission.

for admin and volunteers: this is a little harder, since i am not quite sure what our frontend flow is going to be for creating these two yet. how is this email going to be obtained from the frontend?

@sam-schu @Yurika-Kan

Copy link
Member

Choose a reason for hiding this comment

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

I think checking if the primary email already exists in cognito to allow for modal submission is a good solution for pantry and food manufacturer

Copy link
Collaborator

Choose a reason for hiding this comment

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

For admins and volunteers, the email will be provided directly (we do already have volunteer creation in the frontend on the admin volunteer management page). But for all four user types, a user should only have a cognito account if they also have a db user, so checking that the email does not already exist in our own db should be sufficient to know it won't exist in cognito (so the check you added here should hopefully never need to activate, but it should be fine to have it as a failsafe). I did have it written down in my list of future work to deal with the case where we try to create a user with an email that already exists, so I don't think you need to worry about updating the existing db user creation flows to account for that in this PR.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense to not worry about it in this pr. Just want to raise one potential case for us to think about if we decide to check against the db in the future.

If an application gets rejected, and they re-apply, it won't let them submit their application again because they already have an account in the db even if they don't have a cognito account

Copy link
Author

Choose a reason for hiding this comment

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

Based off the client meeting today, our flow of applying/rejecting may change. Based on this, I think it may be best to hold off on this logic for now. Definitely good to keep in mind right now though.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yep! with how they're planning on handling approving/denying the re-application scenario should not be an issue

} else {
throw new InternalServerErrorException('Failed to create user');
}
}
}

async verifyUser(email: string, verificationCode: string): Promise<void> {
const confirmCommand = new ConfirmSignUpCommand({
ClientId: CognitoAuthConfig.userPoolClientId,
Expand Down
9 changes: 6 additions & 3 deletions apps/backend/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
// This function is natively called when we validate a JWT token
// Afer confirming that our jwt is valid and our payload is signed,
// 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);
return dbUser;
async validate(payload: CognitoJwtPayload): Promise<User | null> {
try {
return await this.usersService.findUserByCognitoId(payload.sub);
} catch {
return null; // Passport treats null as unauthenticated → clean 401
}
}
}
1 change: 0 additions & 1 deletion apps/backend/src/donationItems/donationItems.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
Get,
Patch,
ParseIntPipe,
BadRequestException,
} from '@nestjs/common';
import { ApiBody } from '@nestjs/swagger';
import { DonationItemsService } from './donationItems.service';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Allergen, DonateWastedFood } from './types';
import { ApplicationStatus } from '../shared/types';
import { FoodManufacturerApplicationDto } from './dtos/manufacturer-application.dto';
import { Donation } from '../donations/donations.entity';
import { DonationService } from '../donations/donations.service';

const mockManufacturersService = mock<FoodManufacturersService>();

Expand Down
8 changes: 6 additions & 2 deletions apps/backend/src/foodManufacturers/manufacturers.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FoodManufacturer } from './manufacturers.entity';
import { FoodManufacturersController } from './manufacturers.controller';
import { FoodManufacturersService } from './manufacturers.service';
import { UsersModule } from '../users/users.module';
import { Donation } from '../donations/donations.entity';

@Module({
imports: [TypeOrmModule.forFeature([FoodManufacturer, Donation])],
imports: [
TypeOrmModule.forFeature([FoodManufacturer, Donation]),
forwardRef(() => UsersModule),
],
controllers: [FoodManufacturersController],
providers: [FoodManufacturersService],
})
Expand Down
20 changes: 19 additions & 1 deletion apps/backend/src/foodManufacturers/manufacturers.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ import { FoodManufacturerApplicationDto } from './dtos/manufacturer-application.
import { User } from '../users/user.entity';
import { Role } from '../users/types';
import { ApplicationStatus } from '../shared/types';
import { userSchemaDto } from '../users/dtos/userSchema.dto';
import { UsersService } from '../users/users.service';
import { Donation } from '../donations/donations.entity';

@Injectable()
export class FoodManufacturersService {
constructor(
@InjectRepository(FoodManufacturer)
private repo: Repository<FoodManufacturer>,

private usersService: UsersService,

@InjectRepository(Donation)
private donationsRepo: Repository<Donation>,
) {}
Expand Down Expand Up @@ -121,7 +126,20 @@ export class FoodManufacturersService {
throw new NotFoundException(`Food Manufacturer ${id} not found`);
}

await this.repo.update(id, { status: ApplicationStatus.APPROVED });
const createUserDto: userSchemaDto = {
email: foodManufacturer.foodManufacturerRepresentative.email,
firstName: foodManufacturer.foodManufacturerRepresentative.firstName,
lastName: foodManufacturer.foodManufacturerRepresentative.lastName,
phone: foodManufacturer.foodManufacturerRepresentative.phone,
role: Role.FOODMANUFACTURER,
};

const newFoodManufacturer = await this.usersService.create(createUserDto);

await this.repo.update(id, {
status: ApplicationStatus.APPROVED,
foodManufacturerRepresentative: newFoodManufacturer,
});
}

async deny(id: 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
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 @@ -7,11 +7,13 @@ import { AuthModule } from '../auth/auth.module';
import { OrdersModule } from '../orders/order.module';
import { EmailsModule } from '../emails/email.module';
import { User } from '../users/user.entity';
import { UsersModule } from '../users/users.module';

@Module({
imports: [
TypeOrmModule.forFeature([Pantry, User]),
OrdersModule,
forwardRef(() => UsersModule),
EmailsModule,
forwardRef(() => AuthModule),
],
Expand Down
29 changes: 27 additions & 2 deletions apps/backend/src/pantries/pantries.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ import {
AllergensConfidence,
} from './types';
import { ApplicationStatus } from '../shared/types';
import { UsersService } from '../users/users.service';
import { User } from '../users/user.entity';
import { Role } from '../users/types';

const mockRepository = mock<Repository<Pantry>>();
const mockUsersService = mock<UsersService>();

describe('PantriesService', () => {
let service: PantriesService;
Expand Down Expand Up @@ -79,6 +83,10 @@ describe('PantriesService', () => {
provide: getRepositoryToken(Pantry),
useValue: mockRepository,
},
{
provide: UsersService,
useValue: mockUsersService,
},
],
}).compile();

Expand Down Expand Up @@ -163,16 +171,33 @@ describe('PantriesService', () => {
// Approve pantry by ID (status = approved)
describe('approve', () => {
it('should approve a pantry', async () => {
Copy link
Member

Choose a reason for hiding this comment

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

Why was this removed? We now don't have a test for if the pantry approval process works.

I think we should be asserting the following:

mockUsersService.create was called with the right DTO
mockRepository.update was called with { status: 'approved', pantryUser: newPantryUser }

Copy link
Author

Choose a reason for hiding this comment

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

fixed the test

mockRepository.findOne.mockResolvedValueOnce(mockPendingPantry);
const mockPantryUser: Partial<User> = { id: 1, email: 'test@test.com' };
const mockCreatedUser: Partial<User> = { id: 2, role: Role.PANTRY };

const mockPendingPantryWithUser: Partial<Pantry> = {
...mockPendingPantry,
pantryUser: mockPantryUser as User,
};

mockRepository.findOne.mockResolvedValueOnce(
mockPendingPantryWithUser as Pantry,
);
mockUsersService.create.mockResolvedValueOnce(mockCreatedUser as User);
mockRepository.update.mockResolvedValueOnce(undefined);

await service.approve(1);

expect(mockRepository.findOne).toHaveBeenCalledWith({
where: { pantryId: 1 },
relations: ['pantryUser'],
});
expect(mockUsersService.create).toHaveBeenCalledWith({
...mockPantryUser,
role: Role.PANTRY,
});
expect(mockRepository.update).toHaveBeenCalledWith(1, {
status: 'approved',
status: ApplicationStatus.APPROVED,
pantryUser: mockCreatedUser,
});
});

Expand Down
33 changes: 29 additions & 4 deletions apps/backend/src/pantries/pantries.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import {
forwardRef,
Inject,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { Pantry } from './pantries.entity';
Expand All @@ -7,10 +12,17 @@ import { validateId } from '../utils/validation.utils';
import { ApplicationStatus } from '../shared/types';
import { PantryApplicationDto } from './dtos/pantry-application.dto';
import { Role } from '../users/types';
import { userSchemaDto } from '../users/dtos/userSchema.dto';
import { UsersService } from '../users/users.service';

@Injectable()
export class PantriesService {
constructor(@InjectRepository(Pantry) private repo: Repository<Pantry>) {}
constructor(
@InjectRepository(Pantry) private repo: Repository<Pantry>,

@Inject(forwardRef(() => UsersService))
private usersService: UsersService,
) {}

async findOne(pantryId: number): Promise<Pantry> {
validateId(pantryId, 'Pantry');
Expand Down Expand Up @@ -94,12 +106,25 @@ export class PantriesService {
async approve(id: number) {
validateId(id, 'Pantry');

const pantry = await this.repo.findOne({ where: { pantryId: id } });
const pantry = await this.repo.findOne({
where: { pantryId: id },
relations: ['pantryUser'],
});
if (!pantry) {
throw new NotFoundException(`Pantry ${id} not found`);
}

await this.repo.update(id, { status: ApplicationStatus.APPROVED });
const createUserDto: userSchemaDto = {
...pantry.pantryUser,
role: Role.PANTRY,
};

const newPantryUser = await this.usersService.create(createUserDto);

await this.repo.update(id, {
status: ApplicationStatus.APPROVED,
pantryUser: newPantryUser,
});
}

async deny(id: number) {
Expand Down
8 changes: 1 addition & 7 deletions apps/backend/src/users/users.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,7 @@ describe('UsersController', () => {
const result = await controller.createUser(createUserSchema);

expect(result).toEqual(createdUser);
expect(mockUserService.create).toHaveBeenCalledWith(
createUserSchema.email,
createUserSchema.firstName,
createUserSchema.lastName,
createUserSchema.phone,
createUserSchema.role,
);
expect(mockUserService.create).toHaveBeenCalledWith(createUserSchema);
});

it('should handle service errors', async () => {
Expand Down
3 changes: 1 addition & 2 deletions apps/backend/src/users/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ export class UsersController {

@Post('/')
async createUser(@Body() createUserDto: userSchemaDto): Promise<User> {
const { email, firstName, lastName, phone, role } = createUserDto;
return this.usersService.create(email, firstName, lastName, phone, role);
return this.usersService.create(createUserDto);
}

@Post('/:id/pantries')
Expand Down
Loading