Skip to content

Commit

Permalink
fix: reset try count on new code request (#190)
Browse files Browse the repository at this point in the history
- refactor code service
  • Loading branch information
h4l-yup committed Feb 20, 2024
1 parent a228560 commit 73e4f81
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 51 deletions.
15 changes: 7 additions & 8 deletions apps/api/src/domains/admin/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,11 @@ import { UserService } from '../user/user.service';
import type {
JwtDto,
SendEmailCodeDto,
SignUpInvitationUserDto,
ValidateEmailUserDto,
VerifyEmailCodeDto,
} from './dtos';
import {
SignUpEmailUserDto,
SignUpInvitationUserDto,
SignUpOauthUserDto,
} from './dtos';
import { SignUpEmailUserDto, SignUpOauthUserDto } from './dtos';
import { PasswordNotMatchException, UserBlockedException } from './exceptions';

@Injectable()
Expand Down Expand Up @@ -96,11 +93,13 @@ export class AuthService {
}

async verifyEmailCode({ code, email }: VerifyEmailCodeDto) {
await this.codeService.verifyCode({
const { error } = await this.codeService.verifyCode({
type: CodeTypeEnum.EMAIL_VEIRIFICATION,
key: email,
code,
});

if (error) throw error;
}

async validateEmailUser({ email, password }: ValidateEmailUserDto) {
Expand Down Expand Up @@ -131,15 +130,15 @@ export class AuthService {
return await this.createUserService.createEmailUser(dto);
}

@Transactional()
async signUpInvitationUser(dto: SignUpInvitationUserDto) {
const { code, ...rest } = dto;

await this.codeService.verifyCode({
const { error } = await this.codeService.verifyCode({
type: CodeTypeEnum.USER_INVITATION,
key: dto.email,
code,
});
if (error) throw error;

const data = await this.codeService.getDataByCodeAndType(
CodeTypeEnum.USER_INVITATION,
Expand Down
8 changes: 5 additions & 3 deletions apps/api/src/domains/admin/user/user-password.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { Transactional } from 'typeorm-transactional';
import { CodeTypeEnum } from '@/shared/code/code-type.enum';
import { CodeService } from '@/shared/code/code.service';
import { ResetPasswordMailingService } from '@/shared/mailing/reset-password-mailing.service';
import { ChangePasswordDto, ResetPasswordDto } from './dtos';
import type { ResetPasswordDto } from './dtos';
import { ChangePasswordDto } from './dtos';
import { UserEntity } from './entities/user.entity';
import { InvalidPasswordException, UserNotFoundException } from './exceptions';

Expand All @@ -48,17 +49,18 @@ export class UserPasswordService {
await this.resetPasswordMailingService.send({ email, code });
}

@Transactional()
async resetPassword({ email, code, password }: ResetPasswordDto) {
const user = await this.userRepo.findOneBy({ email });
if (!user) throw new UserNotFoundException();

await this.codeService.verifyCode({
const { error } = await this.codeService.verifyCode({
type: CodeTypeEnum.RESET_PASSWORD,
key: email,
code,
});

if (error) throw error;

return await this.userRepo.save(
Object.assign(user, {
hashPassword: await this.createHashPassword(password),
Expand Down
44 changes: 20 additions & 24 deletions apps/api/src/shared/code/code.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,12 @@ describe('CodeService', () => {
const invalidCode = faker.string.sample(6);
jest.spyOn(codeRepo, 'findOneBy').mockResolvedValue(codeEntity);

await expect(
codeService.verifyCode({
code: invalidCode,
key,
type,
}),
).rejects.toThrow(new BadRequestException('invalid code'));
const { error } = await codeService.verifyCode({
code: invalidCode,
key,
type,
});
expect(error).toEqual(new BadRequestException('invalid code'));
});
it('verifying code fails with an invalid code more than 5 times', async () => {
const { type } = codeEntity;
Expand All @@ -176,35 +175,32 @@ describe('CodeService', () => {
.spyOn(codeRepo, 'findOneBy')
.mockResolvedValue({ ...codeEntity, tryCount: 5 } as CodeEntity);

await expect(
codeService.verifyCode({
code: invalidCode,
key,
type,
}),
).rejects.toThrow(new BadRequestException('code expired'));
const { error } = await codeService.verifyCode({
code: invalidCode,
key,
type,
});
expect(error).toEqual(new BadRequestException('code expired'));
});
it('verifying code fails with an invalid key', async () => {
const { code, type } = codeEntity;
const invalidKey = faker.string.sample(6);
jest.spyOn(codeRepo, 'findOneBy').mockResolvedValue(null);

await expect(
codeService.verifyCode({
code,
key: invalidKey,
type,
}),
).rejects.toThrow(NotFoundException);
const { error } = await codeService.verifyCode({
code,
key: invalidKey,
type,
});
expect(error).toEqual(new NotFoundException('not found code'));
});
it('verifying code fails at expired date', async () => {
MockDate.set(new Date(Date.now() + 5 * 60 * 1000 + 1000));
const { code, type } = codeEntity;
jest.spyOn(codeRepo, 'findOneBy').mockResolvedValue(codeEntity);

await expect(codeService.verifyCode({ code, key, type })).rejects.toThrow(
new BadRequestException('code expired'),
);
const { error } = await codeService.verifyCode({ code, key, type });
expect(error).toEqual(new BadRequestException('code expired'));
MockDate.reset();
});
});
Expand Down
23 changes: 9 additions & 14 deletions apps/api/src/shared/code/code.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
Injectable,
NotFoundException,
} from '@nestjs/common';
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
import { InjectRepository } from '@nestjs/typeorm';
import { DateTime } from 'luxon';
import { Repository } from 'typeorm';
Expand All @@ -37,7 +36,6 @@ export class CodeService {
constructor(
@InjectRepository(CodeEntity)
private readonly codeRepo: Repository<CodeEntity>,
private readonly eventEmitter: EventEmitter2,
) {}

@Transactional()
Expand All @@ -59,6 +57,7 @@ export class CodeService {
.plus({ seconds: durationSec ?? SECONDS })
.toJSDate(),
data: type === CodeTypeEnum.USER_INVITATION ? dto.data : undefined,
tryCount: 0,
}),
);

Expand All @@ -69,24 +68,26 @@ export class CodeService {
async verifyCode({ code, key, type }: VerifyCodeDto) {
const codeEntity = await this.codeRepo.findOneBy({ key, type });

if (!codeEntity) throw new NotFoundException('not found code');
if (!codeEntity) return { error: new NotFoundException('not found code') };
if (codeEntity.isVerified)
throw new BadRequestException('already verified');
return { error: new BadRequestException('already verified') };

if (codeEntity.tryCount >= 5) {
throw new BadRequestException('code expired');
return { error: new BadRequestException('code expired') };
}

if (codeEntity.code !== code) {
this.eventEmitter.emit('code.verify.failed', codeEntity);
throw new BadRequestException('invalid code');
codeEntity.tryCount += 1;
await this.codeRepo.save(codeEntity);
return { error: new BadRequestException('invalid code') };
}

if (DateTime.utc() > DateTime.fromJSDate(codeEntity.expiredAt)) {
throw new BadRequestException('code expired');
return { error: new BadRequestException('code expired') };
}

await this.codeRepo.save(Object.assign(codeEntity, { isVerified: true }));
return { error: null };
}

async getDataByCodeAndType(
Expand All @@ -112,10 +113,4 @@ export class CodeService {
private createCode() {
return String(crypto.randomInt(1, 1000000)).padStart(6, '0');
}

@OnEvent('code.verify.failed', { async: true })
private async handleCodeVerificationFailure(codeEntity: CodeEntity) {
codeEntity.tryCount += 1;
await this.codeRepo.save(codeEntity);
}
}
2 changes: 0 additions & 2 deletions apps/api/src/test-utils/providers/code.service.providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* under the License.
*/

import { EventEmitter2 } from '@nestjs/event-emitter';
import { getRepositoryToken } from '@nestjs/typeorm';

import { mockRepository } from '@/test-utils/util-functions';
Expand All @@ -23,6 +22,5 @@ import { CodeService } from '../../shared/code/code.service';

export const CodeServiceProviders = [
CodeService,
EventEmitter2,
{ provide: getRepositoryToken(CodeEntity), useValue: mockRepository() },
];

0 comments on commit 73e4f81

Please sign in to comment.