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 @@ -33,13 +34,14 @@ export class AuthController {
throw new BadRequestException(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
33 changes: 33 additions & 0 deletions apps/backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ConflictException,
Injectable,
InternalServerErrorException,
NotFoundException,
Expand All @@ -12,6 +13,7 @@ import {
ConfirmSignUpCommand,
ForgotPasswordCommand,
SignUpCommand,
AdminCreateUserCommand,
AuthenticationResultType,
} from '@aws-sdk/client-cognito-identity-provider';

Expand Down Expand Up @@ -102,6 +104,37 @@ export class AuthService {
}
}

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'],
});

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 instanceof Error && error.name == 'UsernameExistsException') {
throw new ConflictException('A user with this email already exists');
} 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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { FoodManufacturerApplicationDto } from './dtos/manufacturer-application.
import { ApiBody } from '@nestjs/swagger';
import { Allergen, DonateWastedFood, ManufacturerAttribute } from './types';
import { Donation } from '../donations/donations.entity';
import { Public } from '../auth/public.decorator';

@Controller('manufacturers')
export class FoodManufacturersController {
Expand Down Expand Up @@ -157,6 +158,7 @@ export class FoodManufacturersController {
],
},
})
@Public()
@Post('/application')
async submitFoodManufacturerApplication(
@Body(new ValidationPipe())
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: 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 () => {
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({} as UpdateResult);

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 @@ -97,12 +109,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