feat: add otp and hashing modules
This commit is contained in:
Generated
+987
-1286
File diff suppressed because it is too large
Load Diff
+6
-3
@@ -24,8 +24,10 @@
|
|||||||
"@nestjs/config": "^4.0.3",
|
"@nestjs/config": "^4.0.3",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/schedule": "^6.1.1",
|
||||||
"@nestjs/swagger": "^11.2.6",
|
"@nestjs/swagger": "^11.2.6",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
"joi": "^18.0.2",
|
"joi": "^18.0.2",
|
||||||
@@ -41,8 +43,9 @@
|
|||||||
"@nestjs/cli": "^11.0.0",
|
"@nestjs/cli": "^11.0.0",
|
||||||
"@nestjs/schematics": "^11.0.0",
|
"@nestjs/schematics": "^11.0.0",
|
||||||
"@nestjs/testing": "^11.0.1",
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
@@ -50,11 +53,11 @@
|
|||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-prettier": "^5.2.2",
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jest": "^30.0.0",
|
"jest": "^29.7.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.4.6",
|
||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import databaseConfig from './config/database.config';
|
|||||||
import { HeaderResolver, I18nJsonLoader, I18nModule } from 'nestjs-i18n';
|
import { HeaderResolver, I18nJsonLoader, I18nModule } from 'nestjs-i18n';
|
||||||
import { AuthModule } from './modules/auth/auth.module';
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
import { OtpModule } from './modules/otp/otp.module';
|
import { OtpModule } from './modules/otp/otp.module';
|
||||||
|
import { HashingModule } from './common/modules/hashing/hashing.module';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const ENV = process.env.NODE_ENV;
|
const ENV = process.env.NODE_ENV;
|
||||||
@@ -59,6 +60,7 @@ const ENV = process.env.NODE_ENV;
|
|||||||
UsersModule,
|
UsersModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
OtpModule,
|
OtpModule,
|
||||||
|
HashingModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { HashingProvider } from './providers/hashing.provider';
|
||||||
|
import { BcryptProvider } from './providers/bcrypt.provider';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: HashingProvider,
|
||||||
|
useClass: BcryptProvider,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [HashingProvider],
|
||||||
|
})
|
||||||
|
export class HashingModule {}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { HashingProvider } from './hashing.provider';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BcryptProvider implements HashingProvider {
|
||||||
|
public async hash(data: string | Buffer): Promise<string> {
|
||||||
|
const salt = (await bcrypt.genSalt()) as string;
|
||||||
|
|
||||||
|
return (await bcrypt.hash(data, salt)) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async compare(
|
||||||
|
data: string | Buffer,
|
||||||
|
encrypted: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return await bcrypt.compare(data, encrypted);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export abstract class HashingProvider {
|
||||||
|
abstract hash(data: string | Buffer): Promise<string>;
|
||||||
|
|
||||||
|
abstract compare(data: string | Buffer, encrypted: string): Promise<boolean>;
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
|
|||||||
import { AuthController } from './auth.controller';
|
import { AuthController } from './auth.controller';
|
||||||
import { AuthService } from './providers/auth.service';
|
import { AuthService } from './providers/auth.service';
|
||||||
import { UsersModule } from '../users/users.module';
|
import { UsersModule } from '../users/users.module';
|
||||||
|
import { OtpModule } from '../otp/otp.module';
|
||||||
|
import { HashingModule } from '@/common/modules/hashing/hashing.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UsersModule],
|
imports: [UsersModule, OtpModule, HashingModule],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [AuthService],
|
providers: [AuthService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { UsersService } from '@/modules/users/providers/users.service';
|
import { UsersService } from '@/modules/users/providers/users.service';
|
||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { LoginDTO } from '../dtos/login.dto';
|
import { LoginDTO } from '../dtos/login.dto';
|
||||||
|
import { OtpService } from '@/modules/otp/providers/otp.service';
|
||||||
|
import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -9,6 +11,16 @@ export class AuthService {
|
|||||||
* Inject Users Service
|
* Inject Users Service
|
||||||
*/
|
*/
|
||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject OTP Service
|
||||||
|
*/
|
||||||
|
private readonly otpService: OtpService,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject Hashing Provider
|
||||||
|
*/
|
||||||
|
private readonly hashingProvider: HashingProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async logIn(loginDto: LoginDTO) {
|
public async logIn(loginDto: LoginDTO) {
|
||||||
@@ -17,10 +29,13 @@ export class AuthService {
|
|||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// TODO: Send OTP
|
// TODO: Send OTP
|
||||||
|
const otpCode = await this.otpService.createOTP(phone);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newUser: true,
|
newUser: true,
|
||||||
message: `Otp sent to ${phone}`,
|
message: `Otp sent to ${phone}`,
|
||||||
|
// Just for development
|
||||||
|
code: otpCode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,8 +46,12 @@ export class AuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Hash password and compare
|
const isPasswordCorrect = await this.hashingProvider.compare(
|
||||||
if (password === user.password) {
|
password,
|
||||||
|
user.password || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isPasswordCorrect) {
|
||||||
// TODO: Create JWT
|
// TODO: Create JWT
|
||||||
|
|
||||||
return { access: 'ACCESS_TOKEN', refresh: 'REFRESH_TOKEN' };
|
return { access: 'ACCESS_TOKEN', refresh: 'REFRESH_TOKEN' };
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ export class OTP {
|
|||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column({ type: 'timestamptz' })
|
@Column({ type: 'timestamptz' })
|
||||||
expiredAt: Date;
|
expiresAt: Date;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
used: boolean;
|
used: boolean;
|
||||||
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
setExpiry() {
|
setExpiry() {
|
||||||
this.expiredAt = new Date(Date.now() + 2 * 60 * 1000);
|
this.expiresAt = new Date(Date.now() + 2 * 60 * 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ import { OTP } from './otp.entity';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([OTP])],
|
imports: [TypeOrmModule.forFeature([OTP])],
|
||||||
providers: [OtpService],
|
providers: [OtpService],
|
||||||
|
exports: [OtpService],
|
||||||
})
|
})
|
||||||
export class OtpModule {}
|
export class OtpModule {}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import {
|
||||||
import { Repository } from 'typeorm';
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { LessThan, Repository } from 'typeorm';
|
||||||
import { OTP } from '../otp.entity';
|
import { OTP } from '../otp.entity';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Cron } from '@nestjs/schedule';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OtpService {
|
export class OtpService {
|
||||||
@@ -31,9 +37,52 @@ export class OtpService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (lastOTP && Date.now() - lastOTP.createdAt.getTime() < 60_000) {
|
if (lastOTP && Date.now() - lastOTP.createdAt.getTime() < 60_000) {
|
||||||
// throw new TooMany();
|
throw new HttpException(
|
||||||
|
'Too many requests! Try again after 1 min.',
|
||||||
|
HttpStatus.TOO_MANY_REQUESTS,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
const code = this.generateOTPCode();
|
||||||
|
// TODO: Hash the code
|
||||||
|
|
||||||
|
const otp = this.otpRepository.create({ phone, code });
|
||||||
|
|
||||||
|
await this.otpRepository.save(otp);
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify OTP
|
||||||
|
*/
|
||||||
|
public async verifyOTP(phone: string, code: string): Promise<OTP> {
|
||||||
|
const otp = await this.otpRepository.findOne({
|
||||||
|
where: { phone, used: false },
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!otp) throw new UnauthorizedException('Invalid OTP');
|
||||||
|
|
||||||
|
if (otp.expiresAt < new Date()) {
|
||||||
|
throw new UnauthorizedException('OTP expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Compared hashed otp
|
||||||
|
if (otp.code !== code) {
|
||||||
|
throw new UnauthorizedException('Invalid OTP');
|
||||||
|
}
|
||||||
|
|
||||||
|
otp.used = true;
|
||||||
|
await this.otpRepository.save(otp);
|
||||||
|
|
||||||
|
return otp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cron('0 */10 * * * *')
|
||||||
|
public async cleanupExpired(): Promise<void> {
|
||||||
|
await this.otpRepository.delete({
|
||||||
|
expiresAt: LessThan(new Date()),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
Index,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
@@ -25,6 +26,7 @@ export class User {
|
|||||||
})
|
})
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
|
|
||||||
|
@Index({ unique: true })
|
||||||
@Column({
|
@Column({
|
||||||
type: 'varchar',
|
type: 'varchar',
|
||||||
length: 11,
|
length: 11,
|
||||||
@@ -37,6 +39,7 @@ export class User {
|
|||||||
type: 'varchar',
|
type: 'varchar',
|
||||||
length: 96,
|
length: 96,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
select: false,
|
||||||
})
|
})
|
||||||
password?: string;
|
password?: string;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user