Skip to content
18 changes: 9 additions & 9 deletions apps/backend/src/allocations/allocations.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,28 @@ import { Order } from '../orders/order.entity';
@Entity('allocations')
export class Allocation {
@PrimaryGeneratedColumn({ name: 'allocation_id' })
allocationId: number;
allocationId!: number;

@Column({ name: 'order_id', type: 'int', nullable: false })
orderId: number;
orderId!: number;

@ManyToOne(() => Order, (order) => order.allocations)
@JoinColumn({ name: 'order_id' })
order: Order;
order!: Order;

@Column({ name: 'item_id', type: 'int', nullable: false })
itemId: number;
itemId!: number;

@ManyToOne(() => DonationItem, (item) => item.allocations)
@JoinColumn({ name: 'item_id' })
item: DonationItem;
item!: DonationItem;

@Column({ name: 'allocated_quantity', type: 'int' })
allocatedQuantity: number;
allocatedQuantity!: number;

@Column({ name: 'reserved_at', type: 'timestamp' })
reservedAt: Date;
reservedAt!: Date;

@Column({ name: 'fulfilled_at', type: 'timestamp' })
fulfilledAt: Date;
@Column({ name: 'fulfilled_at', type: 'timestamp', nullable: true })
fulfilledAt?: Date | null;
}
2 changes: 1 addition & 1 deletion apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { ScheduleModule } from '@nestjs/schedule';
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: async (configService: ConfigService) =>
configService.get('typeorm'),
configService.getOrThrow('typeorm'),
}),
ScheduleModule.forRoot(),
UsersModule,
Expand Down
24 changes: 18 additions & 6 deletions apps/backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ export class AuthController {
// By default, creates a standard user
try {
await this.authService.signup(signUpDto);
} catch (e) {
throw new BadRequestException(e.message);
} catch (e: unknown) {
const message =
e instanceof Error
? e.message
: 'Unexpected error occurred when signing up user';
throw new BadRequestException(message);
}

const user = await this.usersService.create(
Expand All @@ -45,8 +49,12 @@ export class AuthController {
verifyUser(@Body() body: VerifyUserDto): void {
try {
this.authService.verifyUser(body.email, body.verificationCode);
} catch (e) {
throw new BadRequestException(e.message);
} catch (e: unknown) {
const message =
e instanceof Error
? e.message
: 'Unexpected error occurred when verifying user';
throw new BadRequestException(message);
}
}

Expand Down Expand Up @@ -76,8 +84,12 @@ export class AuthController {

try {
await this.authService.deleteUser(user.email);
} catch (e) {
throw new BadRequestException(e.message);
} catch (e: unknown) {
const message =
e instanceof Error
? e.message
: 'Unexpected error occurred when deleting user';
throw new BadRequestException(message);
}

this.usersService.remove(user.id);
Expand Down
5 changes: 5 additions & 0 deletions apps/backend/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ describe('AuthService', () => {
let service: AuthService;

beforeEach(async () => {
process.env.AWS_ACCESS_KEY_ID = 'test';
process.env.AWS_SECRET_ACCESS_KEY = 'test';
process.env.COGNITO_CLIENT_SECRET = 'test';
process.env.AWS_REGION = 'us-east-1';

const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
Expand Down
109 changes: 97 additions & 12 deletions apps/backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Injectable } from '@nestjs/common';
import {
BadRequestException,
Injectable,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import {
AdminDeleteUserCommand,
AdminInitiateAuthCommand,
AdminInitiateAuthCommandOutput,
AttributeType,
CognitoIdentityProviderClient,
ConfirmForgotPasswordCommand,
Expand Down Expand Up @@ -29,12 +35,22 @@ 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: this.validateEnv('AWS_ACCESS_KEY_ID'),
secretAccessKey: this.validateEnv('AWS_SECRET_ACCESS_KEY'),
},
});

this.clientSecret = process.env.COGNITO_CLIENT_SECRET;
this.clientSecret = this.validateEnv('COGNITO_CLIENT_SECRET');
}

validateEnv(name: string): string {
const v = process.env[name];

if (!v) {
throw new InternalServerErrorException(`Missing env var: ${name}`);
}

return v;
}

// Computes secret hash to authenticate this backend to Cognito
Expand All @@ -55,7 +71,18 @@ export class AuthService {

// TODO need error handling
const { Users } = await this.providerClient.send(listUsersCommand);
return Users[0].Attributes;

const user = Users?.[0];
if (!user) {
throw new NotFoundException(`Cognito user with sub ${userSub} not found`);
}

const userAttributes = Users[0].Attributes;
if (!userAttributes) {
throw new NotFoundException(`Cognito user attributes not found`);
}

return userAttributes;
}

async signup(
Expand All @@ -82,8 +109,19 @@ export class AuthService {
],
});

const response = await this.providerClient.send(signUpCommand);
return response.UserConfirmed;
try {
const response = await this.providerClient.send(signUpCommand);

if (response.UserConfirmed == null) {
throw new InternalServerErrorException(
'Missing UserConfirmed from Cognito',
);
}

return response.UserConfirmed;
} catch (err: unknown) {
throw new BadRequestException('Failed to sign up user');
}
}

async verifyUser(email: string, verificationCode: string): Promise<void> {
Expand Down Expand Up @@ -111,10 +149,14 @@ export class AuthService {

const response = await this.providerClient.send(signInCommand);

this.validateAuthenticationResultTokensForSignIn(response);

const authResult = response.AuthenticationResult!;

return {
accessToken: response.AuthenticationResult.AccessToken,
refreshToken: response.AuthenticationResult.RefreshToken,
idToken: response.AuthenticationResult.IdToken,
accessToken: authResult.AccessToken!,
refreshToken: authResult.RefreshToken!,
idToken: authResult.IdToken!,
};
}

Expand All @@ -135,10 +177,14 @@ export class AuthService {

const response = await this.providerClient.send(refreshCommand);

this.validateAuthenticationResultTokensForRefresh(response);

const authResult = response.AuthenticationResult!;

return {
accessToken: response.AuthenticationResult.AccessToken,
accessToken: authResult.AccessToken!,
refreshToken: refreshToken,
idToken: response.AuthenticationResult.IdToken,
idToken: authResult.IdToken!,
};
}

Expand Down Expand Up @@ -176,4 +222,43 @@ export class AuthService {

await this.providerClient.send(adminDeleteUserCommand);
}

validateAuthenticationResultTokensForSignIn(
commandOutput: AdminInitiateAuthCommandOutput,
): void {
if (commandOutput.AuthenticationResult == null) {
throw new NotFoundException(
'No associated authentication result for sign in',
);
}

if (
commandOutput.AuthenticationResult.AccessToken == null ||
commandOutput.AuthenticationResult.RefreshToken == null ||
commandOutput.AuthenticationResult.IdToken == null
) {
throw new NotFoundException(
'Necessary Authentication Result tokens not found for sign in ',
);
}
}

validateAuthenticationResultTokensForRefresh(
commandOutput: AdminInitiateAuthCommandOutput,
): void {
if (commandOutput.AuthenticationResult == null) {
throw new NotFoundException(
'No associated authentication result for refresh',
);
}

if (
commandOutput.AuthenticationResult.AccessToken == null ||
commandOutput.AuthenticationResult.IdToken == null
) {
throw new NotFoundException(
'Necessary Authentication Result tokens not found for refresh',
);
}
}
}
6 changes: 3 additions & 3 deletions apps/backend/src/auth/dtos/confirm-password.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { IsEmail, IsString } from 'class-validator';

export class ConfirmPasswordDto {
@IsEmail()
email: string;
email!: string;

@IsString()
newPassword: string;
newPassword!: string;

@IsString()
confirmationCode: string;
confirmationCode!: string;
}
2 changes: 1 addition & 1 deletion apps/backend/src/auth/dtos/delete-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { IsPositive } from 'class-validator';

export class DeleteUserDto {
@IsPositive()
userId: number;
userId!: number;
}
2 changes: 1 addition & 1 deletion apps/backend/src/auth/dtos/forgot-password.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { IsEmail } from 'class-validator';

export class ForgotPasswordDto {
@IsEmail()
email: string;
email!: string;
}
4 changes: 2 additions & 2 deletions apps/backend/src/auth/dtos/refresh-token.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { IsString } from 'class-validator';

export class RefreshTokenDto {
@IsString()
refreshToken: string;
refreshToken!: string;

@IsString()
userSub: string;
userSub!: string;
}
6 changes: 3 additions & 3 deletions apps/backend/src/auth/dtos/sign-in-response.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export class SignInResponseDto {
accessToken: string;
accessToken!: string;

refreshToken: string;
refreshToken!: string;

idToken: string;
idToken!: string;
}
4 changes: 2 additions & 2 deletions apps/backend/src/auth/dtos/sign-in.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { IsEmail, IsString } from 'class-validator';

export class SignInDto {
@IsEmail()
email: string;
email!: string;

@IsString()
password: string;
password!: string;
}
10 changes: 5 additions & 5 deletions apps/backend/src/auth/dtos/sign-up.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ import { IsEmail, IsNotEmpty, IsString, IsPhoneNumber } from 'class-validator';

export class SignUpDto {
@IsString()
firstName: string;
firstName!: string;

@IsString()
lastName: string;
lastName!: string;

@IsEmail()
email: string;
email!: string;

@IsString()
password: string;
password!: string;

@IsString()
@IsNotEmpty()
@IsPhoneNumber('US', {
message:
'phone must be a valid phone number (make sure all the digits are correct)',
})
phone: string;
phone!: string;
}
4 changes: 2 additions & 2 deletions apps/backend/src/auth/dtos/verify-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { IsEmail, IsString } from 'class-validator';

export class VerifyUserDto {
@IsEmail()
email: string;
email!: string;

@IsString()
verificationCode: string;
verificationCode!: string;
}
7 changes: 6 additions & 1 deletion apps/backend/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { ExtractJwt, Strategy } from 'passport-jwt';

import CognitoAuthConfig from './aws-exports';

type JwtPayload = {
sub: string;
email: string;
};

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
Expand All @@ -25,7 +30,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
});
}

async validate(payload) {
async validate(payload: JwtPayload) {
return { idUser: payload.sub, email: payload.email };
}
}
Loading
Loading