Merge pull request #1 from pulipakaa24/emailVerifyVersion
Email verify version
This commit is contained in:
15
agenda.js
15
agenda.js
@@ -244,6 +244,19 @@ const initializeAgenda = async (mongoUri, pool, io) => { // Now accepts pgPool
|
||||
}
|
||||
});
|
||||
|
||||
agenda.define('delete unverified users', async (job) => {
|
||||
try {
|
||||
const result = await sharedPgPool.query(
|
||||
"DELETE FROM users WHERE is_verified = false AND created_at < NOW() - INTERVAL '24 hours'"
|
||||
);
|
||||
if (result.rowCount > 0) {
|
||||
console.log(`Cleanup: Deleted ${result.rowCount} unverified users.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error cleaning up users:", error);
|
||||
}
|
||||
});
|
||||
|
||||
agenda.on('ready', () => console.log('Agenda connected to MongoDB and ready!'));
|
||||
agenda.on('start', (job) => console.log(`Job "${job.attrs.name}" starting`));
|
||||
agenda.on('complete', (job) => console.log(`Job "${job.attrs.name}" complete`));
|
||||
@@ -251,6 +264,8 @@ const initializeAgenda = async (mongoUri, pool, io) => { // Now accepts pgPool
|
||||
agenda.on('fail', (err, job) => console.error(`Job "${job.attrs.name}" failed: ${err.message}`));
|
||||
|
||||
await agenda.start();
|
||||
await agenda.cancel({ name: 'delete unverified users' });
|
||||
await agenda.every('24 hours', 'delete unverified users');
|
||||
console.log('Agenda job processing started.');
|
||||
return agenda;
|
||||
};
|
||||
|
||||
149
index.js
149
index.js
@@ -12,6 +12,8 @@ const format = require('pg-format');
|
||||
const cronParser = require('cron-parser');
|
||||
const { ObjectId } = require('mongodb');
|
||||
const { RateLimiterMemory } = require('rate-limiter-flexible');
|
||||
const crypto = require('crypto');
|
||||
const { sendVerificationEmail } = require('./mailer'); // Import the function
|
||||
|
||||
// HTTP rate limiter: 5 requests per second per IP
|
||||
const httpRateLimiter = new RateLimiterMemory({
|
||||
@@ -25,6 +27,12 @@ const authRateLimiter = new RateLimiterMemory({
|
||||
duration: 3600, // 1 hour in seconds
|
||||
});
|
||||
|
||||
// Resend verification email rate limiter: 1 request per 20 seconds per IP
|
||||
const resendVerificationRateLimiter = new RateLimiterMemory({
|
||||
points: 1,
|
||||
duration: 20,
|
||||
});
|
||||
|
||||
// WebSocket connection rate limiter: 1 connection per second per IP
|
||||
const wsConnectionRateLimiter = new RateLimiterMemory({
|
||||
points: 5,
|
||||
@@ -526,7 +534,7 @@ app.post('/login', async (req, res) => {
|
||||
|
||||
if (!email || !password) return res.status(400).json({error: 'email and password required'});
|
||||
try {
|
||||
const {rows} = await pool.query('select id, password_hash_string from users where email = $1', [email]);
|
||||
const {rows} = await pool.query('select id, password_hash_string, is_verified from users where email = $1', [email]);
|
||||
if (rows.length === 0) return res.status(401).json({error: 'Invalid Credentials'});
|
||||
const user = rows[0]
|
||||
console.log('user found');
|
||||
@@ -534,6 +542,13 @@ app.post('/login', async (req, res) => {
|
||||
|
||||
if (!verified) return res.status(401).json({ error: 'Invalid credentials' });
|
||||
console.log("password correct");
|
||||
|
||||
if (!user.is_verified) {
|
||||
return res.status(403).json({
|
||||
error: 'Email not verified. Please check your email and verify your account before logging in.'
|
||||
});
|
||||
}
|
||||
|
||||
const token = await createToken(user.id); // token is now tied to ID
|
||||
|
||||
res.status(200).json({token});
|
||||
@@ -558,23 +573,143 @@ app.post('/create_user', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Compute hash and token once for reuse
|
||||
const hashedPass = await hash(password);
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
try {
|
||||
|
||||
const hashedPass = await hash(password);
|
||||
|
||||
await pool.query("insert into users (name, email, password_hash_string) values (nullif($1, ''), $2, $3)",
|
||||
[name, email, hashedPass]
|
||||
const newUser = await pool.query(
|
||||
`insert into users (name, email, password_hash_string, verification_token, is_verified)
|
||||
values (nullif($1, ''), $2, $3, $4, false)
|
||||
RETURNING id, email`,
|
||||
[name, email, hashedPass, token]
|
||||
);
|
||||
return res.sendStatus(201);
|
||||
|
||||
await sendVerificationEmail(email, token, name);
|
||||
|
||||
// Create temporary token for verification checking
|
||||
const tempToken = await createToken(newUser.rows[0].id);
|
||||
|
||||
res.status(201).json({ message: "User registered. Please check your email.", token: tempToken });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (err.code === '23505') {
|
||||
// Email already exists - check if verified
|
||||
try {
|
||||
const {rows} = await pool.query('SELECT is_verified FROM users WHERE email = $1', [email]);
|
||||
if (rows.length === 1 && !rows[0].is_verified) {
|
||||
// User exists but not verified - replace their record (reuse hashedPass and token)
|
||||
await pool.query(
|
||||
`UPDATE users
|
||||
SET name = nullif($1, ''), password_hash_string = $2, verification_token = $3
|
||||
WHERE email = $4`,
|
||||
[name, hashedPass, token, email]
|
||||
);
|
||||
|
||||
await sendVerificationEmail(email, token, name);
|
||||
|
||||
// Get user ID and create temp token
|
||||
const {rows: userRows} = await pool.query('SELECT id FROM users WHERE email = $1', [email]);
|
||||
const tempToken = await createToken(userRows[0].id);
|
||||
|
||||
return res.status(201).json({ message: "User registered. Please check your email.", token: tempToken });
|
||||
}
|
||||
} catch (updateErr) {
|
||||
console.error('Error updating unverified user:', updateErr);
|
||||
}
|
||||
|
||||
// User is verified or something went wrong - email is truly in use
|
||||
return res.status(409).json({ error: 'Email already in use' });
|
||||
}
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/verify-email', async (req, res) => {
|
||||
const { token } = req.query;
|
||||
|
||||
if (!token) {
|
||||
return res.status(400).send('Missing token');
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Find the user with this token
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM users WHERE verification_token = $1`,
|
||||
[token]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(400).send('Invalid or expired token.');
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
|
||||
// 2. Verify them and clear the token
|
||||
// We clear the token so the link cannot be used twice
|
||||
await pool.query(
|
||||
`UPDATE users
|
||||
SET is_verified = true, verification_token = NULL
|
||||
WHERE id = $1`,
|
||||
[user.id]
|
||||
);
|
||||
|
||||
// 3. Send them to a success page or back to the app
|
||||
res.send(`
|
||||
<h1>Email Verified!</h1>
|
||||
<p>You can now close this window and log in to the app.</p>
|
||||
`);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send('Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/verification_status', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const {rows} = await pool.query('SELECT is_verified FROM users WHERE id = $1', [req.user]);
|
||||
if (rows.length === 0) return res.status(404).json({error: 'User not found'});
|
||||
res.status(200).json({is_verified: rows[0].is_verified});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/resend_verification', authenticateToken, async (req, res) => {
|
||||
const ip = req.ip || req.connection.remoteAddress;
|
||||
try {
|
||||
await resendVerificationRateLimiter.consume(ip);
|
||||
} catch (rejRes) {
|
||||
return res.status(429).json({
|
||||
error: 'Please wait before requesting another verification email.',
|
||||
retryAfter: Math.ceil(rejRes.msBeforeNext / 1000) // seconds
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const {rows} = await pool.query('SELECT email, name, is_verified FROM users WHERE id = $1', [req.user]);
|
||||
if (rows.length === 0) return res.status(404).json({error: 'User not found'});
|
||||
|
||||
const user = rows[0];
|
||||
|
||||
if (user.is_verified) {
|
||||
return res.status(400).json({error: 'Email already verified'});
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
await pool.query('UPDATE users SET verification_token = $1 WHERE id = $2', [token, req.user]);
|
||||
|
||||
await sendVerificationEmail(user.email, token, user.name);
|
||||
|
||||
res.status(200).json({ message: 'Verification email sent' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/verify', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
// Issue a new token to extend session
|
||||
|
||||
43
mailer.js
Normal file
43
mailer.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const { SESClient, SendRawEmailCommand } = require('@aws-sdk/client-ses');
|
||||
|
||||
const ses = new SESClient({
|
||||
region: process.env.AWS_REGION,
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
// Create the transporter
|
||||
const transporter = nodemailer.createTransport({
|
||||
SES: { ses, aws: { SendRawEmailCommand } },
|
||||
});
|
||||
|
||||
// Helper function to send email
|
||||
async function sendVerificationEmail(toEmail, token, name) {
|
||||
const verificationLink = `https://wahwa.com/verify-email?token=${token}`;
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail({
|
||||
from: `"BlindMaster" <${process.env.EMAIL_FROM}>`, // Sender address
|
||||
to: toEmail,
|
||||
subject: "Verify your BlindMaster account",
|
||||
html: `
|
||||
<div style="font-family: sans-serif; padding: 20px;">
|
||||
<h2>Welcome${name && name.trim() ? `, ${name.trim()}` : ''}!</h2>
|
||||
<p>Please verify your email address to complete your registration.</p>
|
||||
<a href="${verificationLink}" style="padding: 10px 20px; background-color: #007bff; color: white; text-decoration: none; border-radius: 5px;">Verify Email</a>
|
||||
<p style="margin-top: 20px; font-size: 12px; color: #888;">Link expires in 24 hours.</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
console.log("Email sent successfully:", info.messageId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { sendVerificationEmail };
|
||||
1352
package-lock.json
generated
1352
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-ses": "^3.962.0",
|
||||
"agenda": "^5.0.0",
|
||||
"argon2": "^0.43.0",
|
||||
"dotenv": "^16.5.0",
|
||||
@@ -17,6 +18,7 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongoose": "^8.16.1",
|
||||
"node-schedule": "^2.1.1",
|
||||
"nodemailer": "^7.0.12",
|
||||
"pg": "^8.16.0",
|
||||
"pg-format": "^1.0.4",
|
||||
"rate-limiter-flexible": "^9.0.1",
|
||||
|
||||
Reference in New Issue
Block a user