Skip to content

Commit f968a87

Browse files
committed
fix(auth): remove brevo and add resend mail service
1 parent 20d8bf8 commit f968a87

8 files changed

Lines changed: 194 additions & 330 deletions

File tree

package-lock.json

Lines changed: 72 additions & 201 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"mongoose": "^9.0.0",
3333
"morgan": "^1.10.1",
3434
"razorpay": "^2.9.6",
35-
"sib-api-v3-sdk": "^8.5.0"
35+
"resend": "^6.9.2"
3636
},
3737
"devDependencies": {
3838
"@typescript-eslint/eslint-plugin": "^8.49.0",

src/controllers/authcontroller.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import bcrypt from 'bcrypt';
22
import jwt from 'jsonwebtoken';
33
import User from '../models/User.js';
4-
import { createAndSendOTP, verifyOTP } from '../services/otp.service.js';
4+
import { sendOtpEmail } from "../services/email.service.js";
5+
import { createAndStoreOtp, verifyStoredOtp } from "../services/otp.service.js";
56

67
export const login = async (req, res) => {
78
try {
@@ -58,33 +59,41 @@ export const login = async (req, res) => {
5859
}
5960
};
6061

61-
export const sendOTP = async (req, res, next) => {
62+
export const sendOTP = async (req, res) => {
6263
try {
6364
const { email } = req.body;
6465

65-
await createAndSendOTP(email);
66+
const otp = await createAndStoreOtp(email);
67+
await sendOtpEmail(email, otp);
6668

6769
res.status(200).json({
6870
success: true,
69-
message: 'OTP sent successfully'
71+
message: "OTP sent successfully"
7072
});
73+
7174
} catch (error) {
72-
next(error);
75+
res.status(500).json({
76+
success: false,
77+
message: error.message
78+
});
7379
}
7480
};
7581

76-
77-
export const validateOTP = async (req, res, next) => {
82+
export const verifyOTP = async (req, res) => {
7883
try {
7984
const { email, otp } = req.body;
8085

81-
await verifyOTP(email, otp);
86+
await verifyStoredOtp(email, otp);
8287

8388
res.status(200).json({
8489
success: true,
85-
message: 'OTP verified successfully'
90+
message: "OTP verified successfully"
8691
});
92+
8793
} catch (error) {
88-
next(error);
94+
res.status(400).json({
95+
success: false,
96+
message: error.message
97+
});
8998
}
9099
};

src/models/otp.js

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/models/otp.model.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import mongoose from "mongoose";
2+
3+
const otpSchema = new mongoose.Schema(
4+
{
5+
email: {
6+
type: String,
7+
required: true,
8+
index: true
9+
},
10+
otpHash: {
11+
type: String,
12+
required: true
13+
},
14+
expiresAt: {
15+
type: Date,
16+
required: true
17+
},
18+
attempts: {
19+
type: Number,
20+
default: 0
21+
}
22+
},
23+
{ timestamps: true }
24+
);
25+
26+
otpSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
27+
28+
export default mongoose.model("Otp", otpSchema);

src/routes/auth.routes.js

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,33 @@
1-
import express from 'express';
2-
import * as authController from '../controllers/authcontroller.js';
3-
import User from '../models/User.js';
4-
import bcrypt from 'bcrypt';
5-
import { sendOTP, validateOTP } from '../controllers/authcontroller.js';
6-
7-
const router = express.Router();
1+
import express from 'express';
2+
import * as authController from '../controllers/authcontroller.js';
3+
import User from '../models/User.js';
4+
import bcrypt from 'bcrypt';
85

9-
router.post('/login', authController.login);
10-
router.get('/health', (req, res) => {
6+
const router = express.Router();
7+
8+
router.post('/login', authController.login);
9+
router.post('/send-otp', authController.sendOTP);
10+
router.post('/verify-otp', authController.verifyOTP);
11+
12+
router.get('/health', (req, res) => {
1113
res.json({
1214
status: 'OK',
1315
service: 'hms-backend-node',
1416
time: new Date().toISOString()
1517
});
1618
});
1719

18-
router.post('/signup', async (req, res) => {
19-
try {
20-
const { email, password, role } = req.body;
21-
const hashedPassword = await bcrypt.hash(password, 10);
22-
const user = new User({ email, password: hashedPassword, role });
23-
await user.save();
24-
res.status(201).json({ message: 'User created' });
25-
} catch (error) {
26-
console.error('Signup error:', error);
27-
res.status(500).json({ message: 'Error creating user', error: error.message });
28-
}
29-
});
30-
31-
router.post('/send-otp', sendOTP);
32-
router.post('/verify-otp', validateOTP);
20+
router.post('/signup', async (req, res) => {
21+
try {
22+
const { email, password, role } = req.body;
23+
const hashedPassword = await bcrypt.hash(password, 10);
24+
const user = new User({ email, password: hashedPassword, role });
25+
await user.save();
26+
res.status(201).json({ message: 'User created' });
27+
} catch (error) {
28+
console.error('Signup error:', error);
29+
res.status(500).json({ message: 'Error creating user', error: error.message });
30+
}
31+
});
3332

3433
export default router;

src/services/email.service.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Resend } from "resend";
2+
3+
const resend = new Resend(process.env.RESEND_API_KEY);
4+
5+
export const sendOtpEmail = async (to, otp) => {
6+
return await resend.emails.send({
7+
from: "HMS <onboarding@resend.dev>",
8+
to,
9+
subject: "Your OTP Code",
10+
html: `
11+
<div style="font-family: Arial;">
12+
<h2>OTP Verification</h2>
13+
<p>Your OTP is:</p>
14+
<h1>${otp}</h1>
15+
<p>This OTP expires in 5 minutes.</p>
16+
</div>
17+
`
18+
});
19+
};

src/services/otp.service.js

Lines changed: 32 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,51 @@
1-
import crypto from 'crypto';
2-
import bcrypt from 'bcrypt';
3-
import OTP from '../models/otp.js';
4-
import { brevoEmailClient } from '../config/brevo.js';
1+
import crypto from "crypto";
2+
import Otp from "../models/otp.model.js";
53

6-
export const generateOTP = () => {
7-
return crypto.randomInt(100000, 999999).toString();
8-
};
4+
const OTP_EXPIRY_MINUTES = 5;
5+
const MAX_ATTEMPTS = 5;
96

10-
export const sendOTPEmail = async (email, otp) => {
11-
await brevoEmailClient.sendTransacEmail({
12-
sender: { email: process.env.BREVO_SENDER_EMAIL },
13-
to: [{ email }],
14-
subject: 'Your One-Time Password (OTP) – Secure Verification',
15-
htmlContent: `
16-
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
17-
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
18-
19-
<h2 style="color: #333333; margin-bottom: 10px;">Verification Required</h2>
20-
21-
<p style="color: #555555; font-size: 14px; line-height: 1.6;">
22-
Dear User,
23-
</p>
24-
25-
<p style="color: #555555; font-size: 14px; line-height: 1.6;">
26-
We received a request to verify your identity. Please use the One-Time Password (OTP) below to proceed:
27-
</p>
28-
29-
<div style="text-align: center; margin: 25px 0;">
30-
<span style="display: inline-block; font-size: 28px; font-weight: bold; letter-spacing: 4px; color: #1a73e8; background-color: #f1f3f4; padding: 12px 24px; border-radius: 6px;">
31-
${otp}
32-
</span>
33-
</div>
34-
35-
<p style="color: #555555; font-size: 14px; line-height: 1.6;">
36-
This OTP is valid for <strong>${process.env.OTP_EXPIRE_MINUTES} minutes</strong>.
37-
For security reasons, please do not share this code with anyone.
38-
</p>
39-
40-
<p style="color: #555555; font-size: 14px; line-height: 1.6;">
41-
If you did not request this verification, you may safely ignore this email.
42-
</p>
43-
44-
<hr style="border: none; border-top: 1px solid #eeeeee; margin: 30px 0;" />
45-
46-
<p style="color: #888888; font-size: 12px; line-height: 1.5;">
47-
This is an automated message. Please do not reply to this email.
48-
</p>
49-
50-
<p style="color: #888888; font-size: 12px;">
51-
© ${new Date().getFullYear()} HMS Team. All rights reserved.
52-
</p>
53-
54-
</div>
55-
</div>
56-
`
57-
});
7+
const generateOtp = () => {
8+
return Math.floor(100000 + Math.random() * 900000).toString();
589
};
5910

11+
const hashOtp = (otp) => {
12+
return crypto.createHash("sha256").update(otp).digest("hex");
13+
};
6014

61-
export const createAndSendOTP = async (email) => {
62-
const otp = generateOTP();
63-
const hashedOTP = await bcrypt.hash(otp, 10);
64-
65-
const expireTime = new Date(Date.now() + process.env.OTP_EXPIRE_MINUTES * 60000);
15+
export const createAndStoreOtp = async (email) => {
16+
const otp = generateOtp();
17+
const otpHash = hashOtp(otp);
6618

67-
await OTP.findOneAndDelete({ email });
19+
const expiresAt = new Date(Date.now() + OTP_EXPIRY_MINUTES * 60 * 1000);
6820

69-
await OTP.create({
70-
email,
71-
otp: hashedOTP,
72-
expiresAt: expireTime
73-
});
21+
await Otp.findOneAndUpdate(
22+
{ email },
23+
{ otpHash, expiresAt, attempts: 0 },
24+
{ upsert: true, new: true }
25+
);
7426

75-
await sendOTPEmail(email, otp);
27+
return otp;
7628
};
7729

78-
export const verifyOTP = async (email, enteredOTP) => {
79-
const record = await OTP.findOne({ email });
30+
export const verifyStoredOtp = async (email, otp) => {
31+
const record = await Otp.findOne({ email });
8032

8133
if (!record) {
82-
throw new Error('OTP expired or not found');
34+
throw new Error("OTP not found");
8335
}
8436

85-
const isValid = await bcrypt.compare(enteredOTP, record.otp);
86-
87-
if (!isValid) {
88-
throw new Error('Invalid OTP');
37+
if (record.attempts >= MAX_ATTEMPTS) {
38+
throw new Error("Too many attempts");
8939
}
9040

91-
await OTP.deleteOne({ email });
41+
const hashedInput = hashOtp(otp);
42+
43+
if (hashedInput !== record.otpHash) {
44+
record.attempts += 1;
45+
await record.save();
46+
throw new Error("Invalid OTP");
47+
}
9248

49+
await Otp.deleteOne({ email });
9350
return true;
9451
};

0 commit comments

Comments
 (0)