Skip to content

Commit

Permalink
Merge pull request #13 from Brints/authentication
Browse files Browse the repository at this point in the history
feat: authentication and login
  • Loading branch information
aniebietafia authored Sep 2, 2024
2 parents da40145 + fd06a42 commit 31959a9
Show file tree
Hide file tree
Showing 14 changed files with 436 additions and 27 deletions.
1 change: 1 addition & 0 deletions brints-estate-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@nestjs/common": "^10.4.1",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.4.1",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^10.4.1",
"@nestjs/swagger": "^7.4.0",
Expand Down
19 changes: 16 additions & 3 deletions brints-estate-api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Controller, Post, Body, HttpStatus } from '@nestjs/common';
import { Controller, Post, Body, HttpStatus, HttpCode } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';

import { AuthService } from './providers/auth.service';
import { CreateUserDto } from 'src/users/dto/create-user.dto';
import { CreateUserAuthDto } from 'src/users/dto/create-userauth.dto';
import { ApiTags } from '@nestjs/swagger';
import { LoginUserDto } from './dto/login.dto';

@Controller('auth')
@ApiTags('Auth')
@ApiTags('Authentication')
export class AuthController {
constructor(private readonly authService: AuthService) {}

Expand All @@ -25,4 +27,15 @@ export class AuthController {
data: user,
};
}

@Post('login')
@HttpCode(HttpStatus.OK)
async loginUser(@Body() loginUserDto: LoginUserDto) {
const user = await this.authService.loginUser(loginUserDto);
return {
message: 'Login successful',
status_code: HttpStatus.OK,
data: user,
};
}
}
8 changes: 8 additions & 0 deletions brints-estate-api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ import { AuthService } from './providers/auth.service';
import { UsersModule } from 'src/users/users.module';
import { HashingProvider } from './providers/hashing.provider';
import { BcryptProvider } from './providers/bcrypt.provider';
import { CreateUserProvider } from './providers/create-user.provider';
import { UserHelper } from 'src/utils/userHelper.lib';
import { GenerateTokenHelper } from 'src/utils/generate-token.lib';
import { LoginUserProvider } from './providers/login-user.provider';

@Module({
controllers: [AuthController],
providers: [
AuthService,
CreateUserProvider,
LoginUserProvider,
UserHelper,
GenerateTokenHelper,
{
provide: HashingProvider,
useClass: BcryptProvider,
Expand Down
15 changes: 15 additions & 0 deletions brints-estate-api/src/auth/dto/login.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

export class LoginUserDto {
@ApiProperty()
@IsNotEmpty()
@IsEmail()
@IsString()
email: string;

@ApiProperty()
@IsNotEmpty()
@IsString()
password: string;
}
12 changes: 0 additions & 12 deletions brints-estate-api/src/auth/http/auth.endpoint.http

This file was deleted.

21 changes: 21 additions & 0 deletions brints-estate-api/src/auth/http/auth.post.endpoint.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
POST http://localhost:3001/auth/register
Content-Type: application/json

{
"first_name": "aniebiet",
"last_name": "afia",
"email": "aniebietafia@gmail.com",
"password": "Test1234$",
"confirm_password": "Test1234$",
"phone_number": "08012345678",
"gender": "male",
"country_code": "+234"
}

POST http://localhost:3001/auth/login
Content-Type: application/json

{
"email": "aniebietafia@gmail.com",
"password": "Test1234$"
}
154 changes: 154 additions & 0 deletions brints-estate-api/src/auth/providers/create-user.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { forwardRef, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';

import { User } from 'src/users/entities/user.entity';
import { UserAuth } from 'src/users/entities/userAuth.entity';
import { CreateUserDto } from 'src/users/dto/create-user.dto';
import { CreateUserAuthDto } from 'src/users/dto/create-userauth.dto';
import { CustomException } from 'src/exceptions/custom.exception';
import { UserHelper } from 'src/utils/userHelper.lib';
import { HashingProvider } from './hashing.provider';
import { VerificationStatus } from 'src/enums/roles.model';
import { GenerateTokenHelper } from 'src/utils/generate-token.lib';

@Injectable()
export class CreateUserProvider {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,

@InjectRepository(UserAuth)
private readonly userAuthRepository: Repository<UserAuth>,

@Inject(forwardRef(() => HashingProvider))
private readonly hashingProvider: HashingProvider,

@Inject(forwardRef(() => UserHelper))
private readonly userHelper: UserHelper,

@Inject(forwardRef(() => GenerateTokenHelper))
private readonly generateTokenHelper: GenerateTokenHelper,
) {}

public async createUser(
createUserDto: CreateUserDto,
createUserAuthDto: CreateUserAuthDto,
): Promise<User> {
const {
first_name,
last_name,
email,
password,
confirm_password,
phone_number,
gender,
country_code,
} = createUserDto;

if (gender.toLowerCase() !== 'female' && gender.toLowerCase() !== 'male') {
throw new CustomException(
HttpStatus.BAD_REQUEST,
`${gender} is not a valid gender`,
);
}

if (!country_code.startsWith('+')) {
throw new CustomException(
HttpStatus.BAD_REQUEST,
'Country code must start with a + followed by a number',
);
}

const fullPhoneNumber = this.userHelper.formatPhoneNumber(
country_code,
phone_number,
);

if (password !== confirm_password) {
throw new CustomException(
HttpStatus.BAD_REQUEST,
'Passwords do not match. Please try again',
);
}

if (password === email) {
throw new CustomException(
HttpStatus.BAD_REQUEST,
'Password cannot be the same as email',
);
}

const formattedFirstName =
this.userHelper.capitalizeFirstLetter(first_name);
const formattedLastName = this.userHelper.capitalizeFirstLetter(last_name);

const userExists = await this.userRepository.findOne({
where: { email: email.toLowerCase() },
});
if (userExists) {
throw new CustomException(
HttpStatus.CONFLICT,
'User Exists already. Please login',
);
}

const phoneNumberExists = await this.userRepository.findOne({
where: { phone_number: fullPhoneNumber },
});
if (phoneNumberExists) {
throw new CustomException(
HttpStatus.CONFLICT,
'Phone number Exists already. Use another phone number',
);
}

const user = this.userRepository.create({
...CreateUserDto,
first_name: formattedFirstName,
last_name: formattedLastName,
email: email.toLowerCase(),
phone_number: fullPhoneNumber,
password: await this.hashingProvider.hashPassword(password),
gender,
});

const verificationToken =
this.generateTokenHelper.generateVerificationToken();
const verificationTokenExpiry = new Date();
verificationTokenExpiry.setHours(verificationTokenExpiry.getHours() + 1);

const newOtp = this.generateTokenHelper.generateOTP(6);
const otpExpiry = new Date();
otpExpiry.setMinutes(otpExpiry.getMinutes() + 20);

const emailVerificationToken = verificationToken;
const emailVerificationTokenExpiresIn = verificationTokenExpiry;

const otp = parseInt(newOtp);
const otpExpiresIn = otpExpiry;

const isEmailVerified = false;
const isPhoneNumberVerified = false;
const status = VerificationStatus.PENDING;

const userAuth = this.userAuthRepository.create({
...createUserAuthDto,
emailVerificationToken,
emailVerificationTokenExpiresIn,
otp,
otpExpiresIn,
isEmailVerified,
isPhoneNumberVerified,
status,
user,
});

user.user_auth = userAuth;

await this.userAuthRepository.save(userAuth);
await this.userRepository.save(user);

return user;
}
}
43 changes: 43 additions & 0 deletions brints-estate-api/src/auth/providers/login-user.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { forwardRef, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';

import { User } from 'src/users/entities/user.entity';
import { HashingProvider } from './hashing.provider';
import { LoginUserDto } from '../dto/login.dto';
import { CustomException } from 'src/exceptions/custom.exception';

@Injectable()
export class LoginUserProvider {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,

@Inject(forwardRef(() => HashingProvider))
private readonly hashingProvider: HashingProvider,
) {}

public async loginUser(loginUserDto: LoginUserDto): Promise<User> {
const user = await this.userRepository.findOne({
where: { email: loginUserDto.email },
});

if (!user) {
throw new CustomException(HttpStatus.NOT_FOUND, 'User not found');
}

const passwordMatch = await this.hashingProvider.comparePassword(
loginUserDto.password,
user.password,
);

if (!passwordMatch) {
throw new CustomException(
HttpStatus.BAD_REQUEST,
'Invalid login credentials',
);
}

return user;
}
}
2 changes: 1 addition & 1 deletion brints-estate-api/src/exceptions/custom.exception.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HttpException, HttpStatus } from '@nestjs/common';

export class CustomConflictException extends HttpException {
export class CustomException extends HttpException {
constructor(status_code: HttpStatus, message: string) {
super(message, status_code);
}
Expand Down
10 changes: 9 additions & 1 deletion brints-estate-api/src/users/dto/create-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,15 @@ export class CreateUserDto {
@ApiProperty()
@IsNotEmpty()
@IsString()
@MaxLength(15)
@Matches(/^\+[0-9]{1,3}$/, {
message: 'Country code must start with a + followed by a number',
})
country_code: string;

@ApiProperty()
@IsNotEmpty()
@IsString()
@MaxLength(50)
phone_number: string;

@ApiProperty()
Expand Down
4 changes: 2 additions & 2 deletions brints-estate-api/src/users/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ export class User extends AbstractBaseEntity {
@Column({ type: 'varchar', length: 255, unique: true })
email: string;

@Column({ type: 'varchar', length: 16 })
@Column({ type: 'varchar', length: 255 })
password: string;

@Column({ type: 'varchar', length: 15 })
@Column({ type: 'varchar', length: 50, unique: true })
phone_number: string;

@Column({ type: 'enum', enum: UserGender })
Expand Down
28 changes: 21 additions & 7 deletions brints-estate-api/src/utils/generate-token.lib.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import * as crypto from 'crypto';

export function generateVerificationToken() {
return crypto.randomBytes(40).toString('hex');
}
// export function generateVerificationToken() {
// return crypto.randomBytes(40).toString('hex');
// }

// export function generateOTP(length: number) {
// return Math.floor(100000 + Math.random() * 900000)
// .toString()
// .slice(0, length);
// }

export class GenerateTokenHelper {
constructor() {}

public generateVerificationToken() {
return crypto.randomBytes(40).toString('hex');
}

export function generateOTP(length: number) {
return Math.floor(100000 + Math.random() * 900000)
.toString()
.slice(0, length);
public generateOTP(length: number) {
return Math.floor(100000 + Math.random() * 900000)
.toString()
.slice(0, length);
}
}
Loading

0 comments on commit 31959a9

Please sign in to comment.