feat: add otp and hashing modules

This commit is contained in:
2026-02-21 09:10:45 +03:30
parent e657264807
commit 20c89c472c
12 changed files with 1122 additions and 1298 deletions
+987 -1286
View File
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -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",
+2
View File
@@ -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>;
}
+3 -1
View File
@@ -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],
}) })
+21 -2
View File
@@ -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' };
+2 -2
View File
@@ -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);
} }
} }
+1
View File
@@ -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 {}
+53 -4
View File
@@ -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()),
});
} }
} }
+3
View File
@@ -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;