feat: Implement OTP-based authentication with Redis, SMS, and email services, including new controllers and providers.

This commit is contained in:
2026-02-09 16:59:07 +03:00
parent 817be5e8c9
commit e87a5b507e
9 changed files with 176 additions and 3 deletions
+1
View File
@@ -26,6 +26,7 @@
"@nestjs/platform-express": "^11.0.1",
"africastalking": "^0.7.8",
"bcrypt": "^6.0.0",
"dotenv": "^17.2.4",
"ioredis": "^5.9.2",
"nodemailer": "^8.0.1",
"reflect-metadata": "^0.2.2",
+9
View File
@@ -26,6 +26,9 @@ importers:
bcrypt:
specifier: ^6.0.0
version: 6.0.0
dotenv:
specifier: ^17.2.4
version: 17.2.4
ioredis:
specifier: ^5.9.2
version: 5.9.2
@@ -1594,6 +1597,10 @@ packages:
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
engines: {node: '>=12'}
[email protected]:
resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==}
engines: {node: '>=12'}
[email protected]:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -4826,6 +4833,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
call-bind-apply-helpers: 1.0.2
+8 -3
View File
@@ -1,10 +1,15 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthController } from './auth.controller';
import { OtpService } from './otp.service';
import { SmsService } from './sms.service';
import { MailService } from './mail.service';
import { AppRedisModule } from './redis.provider';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
imports: [AppRedisModule],
controllers: [AuthController],
providers: [OtpService,SmsService,MailService],
})
export class AppModule {}
+18
View File
@@ -0,0 +1,18 @@
// auth.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { OtpService } from './otp.service';
@Controller('auth')
export class AuthController {
constructor(private readonly otpService: OtpService) {}
@Post('request-otp')
request(@Body() body: { identifier: string }) {
return this.otpService.requestOtp(body.identifier);
}
@Post('verify-otp')
verify(@Body() body: { identifier: string; otp: string }) {
return this.otpService.verifyOtp(body.identifier, body.otp);
}
}
+25
View File
@@ -0,0 +1,25 @@
// mail.service.ts
import { Injectable } from '@nestjs/common';
import * as nodemailer from 'nodemailer';
@Injectable()
export class MailService {
private transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
async sendOtp(email: string, otp: string) {
await this.transporter.sendMail({
from: '"Your App" <[email protected]>',
to: email,
subject: 'Your OTP Code',
text: `Your verification code is ${otp}. It expires in 5 minutes.`
});
}
}
+5
View File
@@ -1,6 +1,11 @@
import { NestFactory } from '@nestjs/core';
import { config } from 'dotenv';
import { AppModule } from './app.module';
config();
console.log('API Key:', process.env.AT_API_KEY);
console.log('Username:', process.env.AT_USERNAME);
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
+74
View File
@@ -0,0 +1,74 @@
// otp.service.ts
import { BadRequestException, Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import { Redis } from 'ioredis';
import { randomInt } from 'crypto';
import * as bcrypt from 'bcrypt';
import { SmsService } from './sms.service';
import { MailService } from './mail.service';
@Injectable()
export class OtpService {
constructor(
@InjectRedis() private readonly redis: Redis,
private readonly smsService: SmsService,
private readonly mailService: MailService
) {}
async requestOtp(identifier: string) {
const isEmail = this.isEmail(identifier);
const isPhone = this.isPhone(identifier);
if (!isEmail && !isPhone) {
throw new BadRequestException('Invalid email or phone number');
}
const cooldownKey = `otp:cooldown:${identifier}`;
const isCooling = await this.redis.get(cooldownKey);
if (isCooling) {
throw new HttpException('Please wait before requesting another OTP', HttpStatus.TOO_MANY_REQUESTS);
}
const otp = randomInt(100000, 999999).toString();
const hash = await bcrypt.hash(otp, 10);
const otpKey = `otp:code:${identifier}`;
await this.redis.set(otpKey, hash, 'EX', 300);
await this.redis.set(cooldownKey, '1', 'EX', 60);
if (isEmail) {
await this.mailService.sendOtp(identifier, otp);
} else {
await this.smsService.sendOtp(identifier, otp);
}
return { message: 'OTP sent' };
}
async verifyOtp(identifier: string, otp: string) {
const key = `otp:code:${identifier}`;
const hash = await this.redis.get(key);
if (!hash) {
throw new BadRequestException('OTP expired or invalid');
}
const isValid = await bcrypt.compare(otp, hash);
if (!isValid) {
throw new BadRequestException('OTP expired or invalid');
}
await this.redis.del(key);
return { message: 'OTP verified' };
}
private isEmail(value: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
private isPhone(value: string): boolean {
return /^\+2547\d{8}$/.test(value);
}
}
+14
View File
@@ -0,0 +1,14 @@
// redis.provider.ts
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-modules/ioredis';
@Module({
imports: [
RedisModule.forRoot({
type: 'single',
url: process.env.REDIS_URL
})
],
exports: [RedisModule]
})
export class AppRedisModule {}
+22
View File
@@ -0,0 +1,22 @@
// sms.service.ts
import { Injectable } from '@nestjs/common';
import AfricasTalking from 'africastalking';
@Injectable()
export class SmsService {
private client;
constructor() {
this.client = AfricasTalking({
apiKey: process.env.AT_API_KEY,
username: process.env.AT_USERNAME
});
}
async sendOtp(phone: string, otp: string) {
await this.client.SMS.send({
to: [phone],
message: `Your verification code is ${otp}. Expires in 5 minutes.`
});
}
}