Skip to content

Commit

Permalink
feat: re-send confirm email
Browse files Browse the repository at this point in the history
  • Loading branch information
fasenderos committed Sep 2, 2023
1 parent 38e730b commit 756cbee
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 35 deletions.
24 changes: 18 additions & 6 deletions packages/api-gateway/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,16 @@ import { Jwt2FAGuard } from './guards/jwt-2fa.guard';
import { VerifyOTPDto } from './dto/verify-otp.dto';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { TrimPipe } from '../common/pipes/trim.pipe';
import { I2FAResponse, I2FaEnabled, ILoginResponse } from './interfaces';
import { ConfirmEmailDto } from './dto/confirm-email.dto';
import {
I2FAResponse,
I2FaEnabled,
IEnable2FAResponse,
ILoginResponse,
} from './interfaces';
import {
ConfirmEmailDto,
ResendConfirmEmailDto,
} from './dto/confirm-email.dto';

@ApiTags('auth')
@Controller('auth')
Expand Down Expand Up @@ -82,14 +90,18 @@ export class AuthController {
return this.auth.confirmEmail(email, code);
}

@Get('resend-confirm-email')
public async resendConfirmEmail(
@Body(ValidationPipe) dto: ResendConfirmEmailDto,
): Promise<void> {
return await this.auth.resendConfirmEmail(dto.email);
}

// Controller that init the request to enable 2FA
@ApiBearerAuth()
@UseGuards(JwtGuard)
@Get('enable2fa')
enable2FA(@GetUser() user: User): Promise<{
secret: string;
qrcode: string;
}> {
enable2FA(@GetUser() user: User): Promise<IEnable2FAResponse> {
return this.auth.enable2FA(user);
}

Expand Down
53 changes: 44 additions & 9 deletions packages/api-gateway/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import {
Injectable,
UnauthorizedException,
UnprocessableEntityException,
} from '@nestjs/common';
import { UserService } from '../user/user.service';
import { SessionService } from './session.service';
import { TokenService } from './token.service';
Expand All @@ -9,11 +13,16 @@ import { ConfigService } from '@nestjs/config';
import { Verify2FADto } from './dto/verify-2fa.dto';
import { CipherService } from '../common/modules/cipher/cipher.service';
import { randomInt } from 'crypto';
import { I2FAResponse, I2FaEnabled, ILoginResponse } from './interfaces';
import {
I2FAResponse,
I2FaEnabled,
IEnable2FAResponse,
ILoginResponse,
} from './interfaces';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { EmailConfirmation } from '../events';
import timestring from 'timestring';
import { UserStatus } from '../common/constants';
import { UserState } from '../common/constants';

@Injectable()
export class AuthService {
Expand Down Expand Up @@ -53,7 +62,7 @@ export class AuthService {
return this.finalizeLogin(user.id);
}

async finalizeLogin(userId: string) {
async finalizeLogin(userId: string): Promise<ILoginResponse> {
const now = Date.now();
const session = await this.session.createSession(userId, now);
const { accessToken, refreshToken } = await this.createTokens(
Expand Down Expand Up @@ -81,7 +90,7 @@ export class AuthService {
return { accessToken, refreshToken };
}

async verifyOTP(userId: string, otp: string) {
async verifyOTP(userId: string, otp: string): Promise<void> {
const user = await this.user.getUserWithUnselected({ id: userId });
if (!user || !user.otpSecret || !user.otp || !user.otpCodes)
throw new UnauthorizedException('Wrong OTP configuration');
Expand All @@ -99,7 +108,7 @@ export class AuthService {
);
}

async enable2FA(user: User) {
async enable2FA(user: User): Promise<IEnable2FAResponse> {
// Generate secret for this user
const secret = authenticator.generateSecret();
// Generate otpauth uri
Expand All @@ -112,7 +121,7 @@ export class AuthService {
};
}

async disable2FA(userId: string) {
async disable2FA(userId: string): Promise<void> {
await this.user.updateById(userId, {
otp: false,
otpSecret: null,
Expand All @@ -138,7 +147,7 @@ export class AuthService {
return { otpCodes };
}

async confirmEmail(email: string, code: number) {
async confirmEmail(email: string, code: number): Promise<void> {
const user = await this.user.getUserWithUnselected({ email, level: 0 });

if (!user || user.otpCodes?.includes(code))
Expand All @@ -153,11 +162,37 @@ export class AuthService {

await this.user.updateById(user.id, {
level: 1,
state: UserStatus.ACTIVE,
state: UserState.ACTIVE,
otpCodes: null,
});
}

async resendConfirmEmail(email: string): Promise<void> {
const user = await this.user.findByEmail(email);
if (!user) return;

if (user.level > 0)
throw new UnprocessableEntityException(
'Your account has already been activated.',
);

if (user.state === UserState.BANNED)
throw new UnprocessableEntityException(
'Sorry, your account is banned. Contact us for more information.',
);

// Wait 5 minutes before request new email varification code
if ((Date.now() - user.updatedAt.getTime()) / 60000 < 5) {
throw new UnprocessableEntityException(
'An email has already been sent. Wait 5 minutes before requesting new one.',
);
}

const code = this.createPinCode();
await this.user.updateById(user.id, { otpCodes: [code] });
this.event.emit(EmailConfirmation, { email, code });
}

private isOTPValid(otp: string, secret: string): boolean {
return authenticator.verify({
token: otp,
Expand Down
4 changes: 3 additions & 1 deletion packages/api-gateway/src/auth/dto/confirm-email.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { ApiProperty } from '@nestjs/swagger';
import { IsEmail } from 'class-validator';
import { IsNotEmptyString } from '../../common/decorators/is-not-empty-string.decorator';

export class ConfirmEmailDto {
export class ResendConfirmEmailDto {
@ApiProperty({ description: 'User email', example: 'email@mysite.com' })
@IsEmail()
@IsNotEmptyString()
readonly email!: string;
}

export class ConfirmEmailDto extends ResendConfirmEmailDto {
@ApiProperty({ description: 'User code', example: '123456' })
@IsNotEmptyString()
readonly code!: number;
Expand Down
5 changes: 5 additions & 0 deletions packages/api-gateway/src/auth/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ export interface ILoginResponse {
export interface I2FaEnabled {
otpCodes: number[];
}

export interface IEnable2FAResponse {
secret: string;
qrcode: string;
}
2 changes: 1 addition & 1 deletion packages/api-gateway/src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export enum UserStatus {
export enum UserState {
ACTIVE = 'active',
PENDING = 'pending',
BANNED = 'banned',
Expand Down
32 changes: 16 additions & 16 deletions packages/api-gateway/src/profiles/entities/profile.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,27 @@ export class Profile extends BaseEntity {
@Column({ type: 'uuid' })
userId!: string;

@Column({ default: null })
firstName!: string;
@Column({ nullable: true })
firstName!: string | null;

@Column({ default: null })
lastName!: string;
@Column({ nullable: true })
lastName!: string | null;

@Column({ default: null })
dob!: Date;
@Column({ nullable: true })
dob!: Date | null;

@Column({ default: null })
address!: string;
@Column({ nullable: true })
address!: string | null;

@Column({ default: null })
postcode!: string;
@Column({ nullable: true })
postcode!: string | null;

@Column({ default: null })
city!: string;
@Column({ nullable: true })
city!: string | null;

@Column({ default: null })
country!: string;
@Column({ nullable: true })
country!: string | null;

@Column({ type: 'jsonb', default: null })
metadata!: string;
@Column({ type: 'jsonb', nullable: true })
metadata!: string | null;
}
4 changes: 2 additions & 2 deletions packages/api-gateway/src/user/entities/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Column, Entity } from 'typeorm';
import { BaseEntity } from '../../base/base.entity';
import { UserStatus } from '../../common/constants';
import { UserState } from '../../common/constants';

@Entity({ name: 'users' })
export class User extends BaseEntity {
Expand All @@ -23,7 +23,7 @@ export class User extends BaseEntity {
level!: number;

/** active, pending, banned */
@Column({ default: UserStatus.PENDING })
@Column({ default: UserState.PENDING })
state!: string;

@Column({ nullable: true, type: 'uuid' })
Expand Down
4 changes: 4 additions & 0 deletions packages/api-gateway/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export class UserService {
return this.user.findOneBy({ id: userId });
}

public findByEmail(email: string) {
return this.user.findOneBy({ email });
}

public async createUser({
email,
password,
Expand Down
4 changes: 4 additions & 0 deletions packages/api-gateway/typings/common/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export interface AppConfig {
password: string;
database: string;
};
email: {
transport: string;
from: string;
};
encryption: {
secret: string;
};
Expand Down

0 comments on commit 756cbee

Please sign in to comment.