Merge pull request #1 from pulipakaa24/emailVerifyVersion

Email verify version
This commit is contained in:
2026-01-08 09:16:58 -06:00
committed by GitHub
5 changed files with 1267 additions and 294 deletions

View File

@@ -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('ready', () => console.log('Agenda connected to MongoDB and ready!'));
agenda.on('start', (job) => console.log(`Job "${job.attrs.name}" starting`)); agenda.on('start', (job) => console.log(`Job "${job.attrs.name}" starting`));
agenda.on('complete', (job) => console.log(`Job "${job.attrs.name}" complete`)); 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}`)); agenda.on('fail', (err, job) => console.error(`Job "${job.attrs.name}" failed: ${err.message}`));
await agenda.start(); await agenda.start();
await agenda.cancel({ name: 'delete unverified users' });
await agenda.every('24 hours', 'delete unverified users');
console.log('Agenda job processing started.'); console.log('Agenda job processing started.');
return agenda; return agenda;
}; };

147
index.js
View File

@@ -12,6 +12,8 @@ const format = require('pg-format');
const cronParser = require('cron-parser'); const cronParser = require('cron-parser');
const { ObjectId } = require('mongodb'); const { ObjectId } = require('mongodb');
const { RateLimiterMemory } = require('rate-limiter-flexible'); 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 // HTTP rate limiter: 5 requests per second per IP
const httpRateLimiter = new RateLimiterMemory({ const httpRateLimiter = new RateLimiterMemory({
@@ -25,6 +27,12 @@ const authRateLimiter = new RateLimiterMemory({
duration: 3600, // 1 hour in seconds 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 // WebSocket connection rate limiter: 1 connection per second per IP
const wsConnectionRateLimiter = new RateLimiterMemory({ const wsConnectionRateLimiter = new RateLimiterMemory({
points: 5, 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'}); if (!email || !password) return res.status(400).json({error: 'email and password required'});
try { 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'}); if (rows.length === 0) return res.status(401).json({error: 'Invalid Credentials'});
const user = rows[0] const user = rows[0]
console.log('user found'); console.log('user found');
@@ -534,6 +542,13 @@ app.post('/login', async (req, res) => {
if (!verified) return res.status(401).json({ error: 'Invalid credentials' }); if (!verified) return res.status(401).json({ error: 'Invalid credentials' });
console.log("password correct"); 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 const token = await createToken(user.id); // token is now tied to ID
res.status(200).json({token}); res.status(200).json({token});
@@ -558,23 +573,143 @@ app.post('/create_user', async (req, res) => {
}); });
} }
try { // Compute hash and token once for reuse
const hashedPass = await hash(password); const hashedPass = await hash(password);
const token = crypto.randomBytes(32).toString('hex');
await pool.query("insert into users (name, email, password_hash_string) values (nullif($1, ''), $2, $3)", try {
[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) { } catch (err) {
console.error(err); console.error(err);
if (err.code === '23505') { 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.status(409).json({ error: 'Email already in use' });
} }
return res.sendStatus(500); 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) => { app.get('/verify', authenticateToken, async (req, res) => {
try { try {
// Issue a new token to extend session // Issue a new token to extend session

43
mailer.js Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"@aws-sdk/client-ses": "^3.962.0",
"agenda": "^5.0.0", "agenda": "^5.0.0",
"argon2": "^0.43.0", "argon2": "^0.43.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
@@ -17,6 +18,7 @@
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"mongoose": "^8.16.1", "mongoose": "^8.16.1",
"node-schedule": "^2.1.1", "node-schedule": "^2.1.1",
"nodemailer": "^7.0.12",
"pg": "^8.16.0", "pg": "^8.16.0",
"pg-format": "^1.0.4", "pg-format": "^1.0.4",
"rate-limiter-flexible": "^9.0.1", "rate-limiter-flexible": "^9.0.1",