Skip to content

Commit

Permalink
Merge pull request #21 from sauravhathi/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
sauravhathi authored Jan 24, 2024
2 parents d5d90e3 + 32780a5 commit 3e61107
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 9 deletions.
1 change: 1 addition & 0 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ OTP_SIZE = 6
# every 2 minutes
CRON_SCHEDULE = 0 3 * * *
# ALLOWED_DOMAINS = gmail.com, lpu.in, outlook.com, yahoo.com
BLOCK_KEYWORDS_RULES =
GMAIL_PASS =
GMAIL_USER =
CRON_SECRET =
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The OTP (One-Time Password) Free Service is a Node.js-based service that allows
- [Configuration](#configuration)
- [Environment Variables](#environment-variables)
- [Scheduled OTP Cleanup](#scheduled-otp-cleanup)
- [Spam Detection](#spam-detection)
- [Donation](#donation)
- [License](#license)

Expand All @@ -30,6 +31,7 @@ The OTP (One-Time Password) Free Service is a Node.js-based service that allows
| Send OTPs via email | Send OTPs to users via email for authentication or verification. |
| Verify OTPs for user authentication | Verify OTPs provided by users for secure authentication. |
| Automatic cleanup of expired OTPs | Automatically remove expired OTPs from the database based on a configured cron schedule. |
| Spam detection | Detect spam requests and block them from being processed.
| Customizable OTP validity period and size | Adjust the validity period and size (length) of OTPs to match your security requirements. |
| Rate limiting for OTP generation | Implement rate limiting to prevent abuse and ensure the service is used responsibly. |
| Multiple email service providers supported | Choose from multiple email service providers (e.g., Gmail, Outlook) to send OTP emails. |
Expand Down Expand Up @@ -129,11 +131,16 @@ You can customize the OTP service by modifying the environment variables in the
| `ALLOWED_DOMAINS` | Comma-separated list of allowed email domains. |
| `GMAIL_USER` | Gmail username (used for sending emails). |
| `GMAIL_PASS` | Gmail password (used for sending emails). |
| `BLOCK_KEYWORDS_RULES` | Comma-separated list of blocked keywords. |

## Scheduled OTP Cleanup

The service automatically clears expired OTPs based on the configured cron schedule. By default, it runs daily at midnight to remove expired OTPs.

## Spam Detection

The service uses a spam detection mechanism to prevent abuse. It checks the request body for spam keywords and blocks the request if any are found. You can configure the spam keywords by setting the `SPAM_WORDS` environment variable.

## Donation

If you find this project useful and want to support its development, consider buying us a coffee! Your support is greatly appreciated.
Expand Down
12 changes: 9 additions & 3 deletions app/controllers/otpController.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ const otpController = {
}).lean();

if (existingOtp) {
if (existingOtp.attempts >= 3) {
logger.info(`Maximum attempts reached for OTP ${existingOtp.otp} associated with ${email}`);
throw new Error('Maximum attempts reached. Please try again after some time');
}

await Otp.updateOne({ _id: existingOtp._id }, { $inc: { attempts: 1 } });
logger.info(`OTP ${existingOtp.otp} already exists for ${email}`);
return existingOtp.otp;
}

const otp = generateOTP(OTP_SIZE, type);

const otpDocument = new Otp({
Expand All @@ -34,7 +40,7 @@ const otpController = {
return otp;
} catch (error) {
logger.error("Failed to generate OTP", error.message);
throw new Error('Failed to generate OTP');
throw new Error(error.message || 'Failed to generate OTP');
}
},
verifyOtp: async (email, otp) => {
Expand Down Expand Up @@ -67,7 +73,7 @@ const otpController = {
await Otp.deleteMany({ createdAt: { $lt: cutoffTime } });
} catch (error) {
logger.error("Failed to clear expired OTPs", error.message);
throw new Error('Failed to clear expired OTPs');
throw new Error(error.message || 'Failed to clear expired OTPs');
}
},
};
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/sendMailController.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ async function sendMailController(email, otp, organization, subject) {
<div class="container">
<div class="header">
<h1>${organization}</h1>
<p style="font-size: 12px;color: #ffffff;">
You received this email because you (or someone else) requested an OTP.
</p>
</div>
<div class="content">
<p>Your One-Time Password (OTP) is:</p>
Expand Down
9 changes: 7 additions & 2 deletions app/db/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ const dotenv = require('dotenv');

dotenv.config();

let existingConnection;

const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
logger.info('🚀 Connected to MongoDB');
if (!existingConnection) {
existingConnection = await mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
logger.info('🚀 Connected to MongoDB');
}
return existingConnection;
} catch (error) {
console.error(error);
logger.error(error);
Expand Down
4 changes: 2 additions & 2 deletions app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
const connectDB = require('./db/config');
const middleware = require('./middleware/middleware');
const {middleware, validateSpamMiddleware} = require('./middleware/middleware');
const { isValidEmail } = require('./utils/validator');
const otpRoutes = require('./routes/otpRoutes');
const logger = require('./utils/logger');
Expand All @@ -21,7 +21,7 @@ app.get('/', (req, res) => {
res.send('Welcome to OTP service');
});

app.use('/api', middleware, otpRoutes);
app.use('/api', validateSpamMiddleware, middleware, otpRoutes);

app.get('/api/cron', (req, res) => {
try {
Expand Down
44 changes: 43 additions & 1 deletion app/middleware/middleware.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
const { isValidEmail } = require("../utils/validator");
const logger = require("../utils/logger");
const Blocklist = require("../models/blockListModel");

const spma_words = process.env.BLOCK_KEYWORDS_RULES.split(',') || [];

const getIp = async (req) => {
try {
const ipDetails = await fetch("https://ipapi.co/json/");
const ipDetailsJson = await ipDetails.json();
return ipDetailsJson.ip;
} catch (error) {
return null;
}
}

const validateSpamMiddleware = async (req, res, next) => {
const bodyValues = Object.values(req.body);
const bodyText = bodyValues.join(' ');
const ip = await getIp(req);

const blocklist = await Blocklist.findOne({
$or: [
{ email: req.body.email ? req.body.email : null },
{ ip: ip }
]
}, { email: 1, ip: 1, _id: 0 });

if (blocklist) {
logger.error('Spam detected');
return res.status(400).json({ error: 'Spam detected' });
}

if (spma_words.some(word => bodyText.includes(word))) {
await Blocklist.create({ ip: ip, email: req.body.email ? req.body.email : null });
logger.error('Spam detected');
return res.status(400).json({ error: 'Spam detected' });
}

next();
}

const validateEmail = (req, res, next) => {
const { email } = req.body;
Expand Down Expand Up @@ -33,4 +72,7 @@ const middleware = (req, res, next) => {
}
};

module.exports = middleware;
module.exports = {
middleware,
validateSpamMiddleware,
}
20 changes: 20 additions & 0 deletions app/models/blockListModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const mongoose = require('mongoose');

const blocklistSchema = new mongoose.Schema({
ip: {
type: String,
required: [true, 'IP address is required'],
},
email: {
type: String,
required: [true, 'Email is required'],
},
createdAt: {
type: Date,
default: Date.now,
},
});

const Blocklist = mongoose.model('Blocklist', blocklistSchema);

module.exports = Blocklist;
7 changes: 6 additions & 1 deletion app/models/otpModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ const otpSchema = new mongoose.Schema({
type: String,
required: true,
},
attempts: {
type: Number,
default: 0,
},
createdAt: {
type: Date,
default: Date.now,
Expand All @@ -18,4 +22,5 @@ const otpSchema = new mongoose.Schema({

const Otp = mongoose.model('Otp', otpSchema);

module.exports = Otp;
module.exports = Otp;

3 changes: 3 additions & 0 deletions app/routes/otpRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ router.post('/otp/generate', async (req, res) => {
try {
const { email, type = 'numeric', organization = 'Saurav Hathi', subject = 'One-Time Password (OTP)' } = req.body;

// SPAM_WORDS = madarchod,chutiya,bsdk,bsdk,mc,bc,bhoslund,land,behenchod,motherfucker
const spma_words = process.env.SPAM_WORDS

const otp = await otpController.generateOtp(email, type);

await sendMailController(email, otp, organization, subject);
Expand Down

0 comments on commit 3e61107

Please sign in to comment.