diff --git a/.env.local b/.env.local index 783eb3d..5304ee8 100644 --- a/.env.local +++ b/.env.local @@ -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 = \ No newline at end of file diff --git a/README.md b/README.md index 45159c4..7fd9bd8 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. | @@ -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. diff --git a/app/controllers/otpController.js b/app/controllers/otpController.js index b4a0a35..808b751 100644 --- a/app/controllers/otpController.js +++ b/app/controllers/otpController.js @@ -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({ @@ -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) => { @@ -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'); } }, }; diff --git a/app/controllers/sendMailController.js b/app/controllers/sendMailController.js index f38b13d..fa09f43 100644 --- a/app/controllers/sendMailController.js +++ b/app/controllers/sendMailController.js @@ -98,6 +98,9 @@ async function sendMailController(email, otp, organization, subject) {

${organization}

+

+ You received this email because you (or someone else) requested an OTP. +

Your One-Time Password (OTP) is:

diff --git a/app/db/config.js b/app/db/config.js index a16a359..b8bdb29 100644 --- a/app/db/config.js +++ b/app/db/config.js @@ -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); diff --git a/app/index.js b/app/index.js index 59d1f86..33becac 100644 --- a/app/index.js +++ b/app/index.js @@ -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'); @@ -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 { diff --git a/app/middleware/middleware.js b/app/middleware/middleware.js index 0c139b3..9de0912 100644 --- a/app/middleware/middleware.js +++ b/app/middleware/middleware.js @@ -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; @@ -33,4 +72,7 @@ const middleware = (req, res, next) => { } }; -module.exports = middleware; \ No newline at end of file +module.exports = { + middleware, + validateSpamMiddleware, +} \ No newline at end of file diff --git a/app/models/blockListModel.js b/app/models/blockListModel.js new file mode 100644 index 0000000..5232268 --- /dev/null +++ b/app/models/blockListModel.js @@ -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; \ No newline at end of file diff --git a/app/models/otpModel.js b/app/models/otpModel.js index 5bc9751..1844fed 100644 --- a/app/models/otpModel.js +++ b/app/models/otpModel.js @@ -10,6 +10,10 @@ const otpSchema = new mongoose.Schema({ type: String, required: true, }, + attempts: { + type: Number, + default: 0, + }, createdAt: { type: Date, default: Date.now, @@ -18,4 +22,5 @@ const otpSchema = new mongoose.Schema({ const Otp = mongoose.model('Otp', otpSchema); -module.exports = Otp; \ No newline at end of file +module.exports = Otp; + diff --git a/app/routes/otpRoutes.js b/app/routes/otpRoutes.js index cdeb1e7..ba83427 100644 --- a/app/routes/otpRoutes.js +++ b/app/routes/otpRoutes.js @@ -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);