mirror of
https://github.com/andwati/nestjs-auth-email-phone.git
synced 2026-10-18 02:52:52 +00:00
feat: Implement OTP-based authentication with Redis, SMS, and email services, including new controllers and providers.
This commit is contained in:
@@ -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",
|
||||
|
||||
Generated
+9
@@ -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
@@ -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 {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.`
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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.`
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user