Skip to content

Commit

Permalink
Merge pull request #396 from RB-Isiaq/feat/enable-2fa
Browse files Browse the repository at this point in the history
feat: enable 2fa
  • Loading branch information
buka4rill authored Jul 25, 2024
2 parents ca5de50 + 0de2efd commit 0d791d7
Show file tree
Hide file tree
Showing 14 changed files with 338 additions and 163 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class Migration1721761856231 implements MigrationInterface {
name = 'Migration1721761856231';
export class Migration1721827066728 implements MigrationInterface {
name = 'Migration1721827066728';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."user_user_type_enum" AS ENUM('super_admin', 'admin', 'vendor')`);
await queryRunner.query(
`CREATE TABLE "user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "first_name" character varying NOT NULL, "last_name" character varying NOT NULL, "email" character varying NOT NULL, "password" character varying NOT NULL, "is_active" boolean, "attempts_left" integer, "time_left" integer, "user_type" "public"."user_user_type_enum" NOT NULL DEFAULT 'vendor', CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`
`CREATE TABLE "user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "first_name" character varying NOT NULL, "last_name" character varying NOT NULL, "email" character varying NOT NULL, "password" character varying NOT NULL, "is_active" boolean, "attempts_left" integer, "time_left" integer, "secret" character varying, "is_2fa_enabled" boolean NOT NULL DEFAULT false, "user_type" "public"."user_user_type_enum" NOT NULL DEFAULT 'vendor', CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`
);
await queryRunner.query(
`CREATE TABLE "organisation_preference" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, "value" character varying NOT NULL, "organisationId" uuid NOT NULL, CONSTRAINT "PK_3149ecbe39a50d9b76f09b9dd44" PRIMARY KEY ("id"))`
Expand Down
170 changes: 21 additions & 149 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
"passport-jwt": "^4.0.1",
"pg": "^8.12.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"speakeasy": "^2.0.0",
"supertest": "^7.0.0",
"typeorm": "^0.3.20",
"typeorm-extension": "^3.5.1",
Expand Down
9 changes: 7 additions & 2 deletions src/helpers/SystemMessages.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
export const USER_CREATED_SUCCESSFULLY = 'User Created Successfully';
export const FAILED_TO_CREATE_USER = 'Error Occured while creating user, kindly try again';
export const ERROR_OCCURED = 'Error Occured Performing this request';
export const USER_ACCOUNT_EXIST = "Account with the specified email exists"
export const UNAUTHENTICATED_MESSAGE = "User is currently unauthorized, kindly authenticate to continue"
export const USER_ACCOUNT_EXIST = 'Account with the specified email exists';
export const UNAUTHENTICATED_MESSAGE = 'User is currently unauthorized, kindly authenticate to continue';
export const USER_NOT_FOUND = 'User not found!';
export const INVALID_PASSWORD = 'Invalid password';
export const TWO_FA_INITIATED = '2FA setup initiated';
export const TWO_FA_ENABLED = '2FA is already enabled';
export const BAD_REQUEST = 'Bad Request';
4 changes: 4 additions & 0 deletions src/helpers/UpdateRecordGeneric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type UpdateRecordGeneric<IdentifierOptions, UpdateRecordPayload> = {
updatePayload: UpdateRecordPayload;
identifierOptions: IdentifierOptions;
};
19 changes: 17 additions & 2 deletions src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Body, Controller, HttpCode, HttpStatus, Post, Request, Res } from '@nestjs/common';
import { Body, Controller, HttpCode, HttpStatus, Post, Req, Request, Res } from '@nestjs/common';
import { Response } from 'express';
import { CreateUserDTO } from './dto/create-user.dto';
import { skipAuth } from '../../helpers/skipAuth';
import AuthenticationService from './auth.service';
import { BAD_REQUEST, TWO_FA_INITIATED } from 'src/helpers/SystemMessages';
import { Enable2FADto } from './dto/enable-2fa.dto';
import { LoginResponseDto } from './dto/login-response.dto';
import { LoginDto } from './dto/login.dto';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';

@ApiTags('auth')
@Controller('auth')
Expand All @@ -29,4 +31,17 @@ export default class RegistrationController {
async login(@Body() loginDto: LoginDto): Promise<LoginResponseDto> {
return this.authService.loginUser(loginDto);
}

@Post('2fa/enable')
@ApiBody({
description: 'Enable two factor authentication',
type: Enable2FADto,
})
@ApiResponse({ status: 200, description: TWO_FA_INITIATED })
@ApiResponse({ status: 400, description: BAD_REQUEST })
public async enable2FA(@Body() body: Enable2FADto, @Req() request: Request): Promise<any> {
const { password } = body;
const { id: user_id } = request['user'];
return this.authService.enable2FA(user_id, password);
}
}
91 changes: 91 additions & 0 deletions src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
import * as speakeasy from 'speakeasy';
import {
ERROR_OCCURED,
FAILED_TO_CREATE_USER,
INVALID_PASSWORD,
TWO_FA_ENABLED,
TWO_FA_INITIATED,
USER_ACCOUNT_EXIST,
USER_CREATED_SUCCESSFULLY,
USER_NOT_FOUND,
} from '../../helpers/SystemMessages';
import { JwtService } from '@nestjs/jwt';
import { LoginResponseDto } from './dto/login-response.dto';
Expand Down Expand Up @@ -133,4 +138,90 @@ export default class AuthenticationService {
);
}
}

private async validateUserAndPassword(user_id: string, password: string) {
const user = await this.userService.getUserRecord({
identifier: user_id,
identifierType: 'id',
});

if (!user) {
throw new HttpException(
{
status_code: HttpStatus.NOT_FOUND,
message: USER_NOT_FOUND,
},
HttpStatus.NOT_FOUND,
{
cause: USER_NOT_FOUND,
}
);
}

const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new HttpException(
{
status_code: HttpStatus.BAD_REQUEST,
message: INVALID_PASSWORD,
},
HttpStatus.BAD_REQUEST,
{
cause: INVALID_PASSWORD,
}
);
}

if (user.is_2fa_enabled) {
throw new HttpException(
{
status_code: HttpStatus.BAD_REQUEST,
message: TWO_FA_ENABLED,
},
HttpStatus.BAD_REQUEST,
{
cause: TWO_FA_ENABLED,
}
);
}

return { user, isValid: true };
}

async enable2FA(user_id: string, password: string) {
const { user, isValid, ...validationResponse } = await this.validateUserAndPassword(user_id, password);

if (!isValid) {
throw validationResponse;
}

const secret = speakeasy.generateSecret({ length: 32 });
const payload = {
secret: secret.base32,
is_2fa_enabled: true,
};

await this.userService.updateUserRecord({
updatePayload: payload,
identifierOptions: {
identifierType: 'id',
identifier: user.id,
},
});

const qrCodeUrl = speakeasy.otpauthURL({
secret: secret.ascii,
label: `Hng:${user.email}`,
issuer: 'Hng Boilerplate',
});

return {
status_code: HttpStatus.OK,
message: TWO_FA_INITIATED,
data: {
secret: secret.base32,
qr_code_url: qrCodeUrl,
},
};
}
}
Loading

0 comments on commit 0d791d7

Please sign in to comment.