refactor: Convert monolithic app to modular architecture
This commit refactors the entire codebase from a monolithic structure to a modular one. Key changes include: - Extracting core components (e.g., user authentication, data processing, API handlers) into their own distinct modules. - Implementing a new directory structure to support a modular design. - Updating all internal references and import paths to reflect the new architecture. The new structure improves maintainability, scalability, and allows for easier independent development of each module in the future.
This commit is contained in:
57
controllers/contactController.js
Normal file
57
controllers/contactController.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
module.exports = (pool, transporter) => {
|
||||||
|
|
||||||
|
// The main function that handles the form submission
|
||||||
|
const submitForm = async (req, res) => {
|
||||||
|
const { firstName, lastName, organization, email, phone, contactMethod, message } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Save submission to the database
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO submissions(first_name, last_name, organization, email, phone, contact_method, message)
|
||||||
|
VALUES($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
|
||||||
|
[firstName, lastName, organization, email, phone, contactMethod, message]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Successfully saved submission to the database:', result.rows[0]);
|
||||||
|
|
||||||
|
// 2. Send the email notification
|
||||||
|
const mailOptions = {
|
||||||
|
from: `"Contact Form" <contact@dlseitz.dev>`,
|
||||||
|
to: process.env.EMAIL_RCPT,
|
||||||
|
subject: 'New Contact Form Submission',
|
||||||
|
html: `
|
||||||
|
<h3>New Submission</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>First Name:</strong> ${firstName}</li>
|
||||||
|
<li><strong>Last Name:</strong> ${lastName}</li>
|
||||||
|
<li><strong>Organization:</strong> ${organization}</li>
|
||||||
|
<li><strong>Email:</strong> ${email}</li>
|
||||||
|
<li><strong>Phone:</strong> ${phone}</li>
|
||||||
|
<li><strong>Contact Method:</strong> ${contactMethod}</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Message:</strong></p>
|
||||||
|
<p>${message}</p>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const emailInfo = await transporter.sendMail(mailOptions);
|
||||||
|
console.log('Message sent: %s', emailInfo.messageId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'Form submitted successfully and saved to the database!',
|
||||||
|
dataReceived: req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('An error occurred during form submission:', err.stack);
|
||||||
|
res.status(500).json({
|
||||||
|
message: 'An error occurred. Please try again.',
|
||||||
|
error: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
submitForm,
|
||||||
|
};
|
||||||
|
};
|
45
middleware/securityMw.js
Normal file
45
middleware/securityMw.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
formSecurityCheck: async (req, res, next) => {
|
||||||
|
// 1. Honeypot check (first line of defense)
|
||||||
|
if (req.body.url) {
|
||||||
|
console.warn('Bot detected! Honeypot field was filled.');
|
||||||
|
return res.status(200).json({ success: true, message: 'Thank you for your submission.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. hCaptcha verification (second line of defense)
|
||||||
|
const hCaptchaResponse = req.body.hCaptchaResponse;
|
||||||
|
if (!hCaptchaResponse) {
|
||||||
|
return res.status(400).json({ success: false, message: 'CAPTCHA token missing.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const secretKey = process.env.HCAPTCHA_SECRET;
|
||||||
|
const verificationUrl = 'https://hcaptcha.com/siteverify';
|
||||||
|
|
||||||
|
const verificationResponse = await fetch(verificationUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
secret: secretKey,
|
||||||
|
response: hCaptchaResponse
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const verificationData = await verificationResponse.json();
|
||||||
|
|
||||||
|
if (!verificationData.success) {
|
||||||
|
console.error('hCaptcha verification failed:', verificationData['error-codes']);
|
||||||
|
return res.status(400).json({ success: false, message: 'CAPTCHA verification failed. Please try again.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all checks pass, move to the next middleware or controller
|
||||||
|
next();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('An error occurred during hCaptcha verification:', error);
|
||||||
|
return res.status(500).json({ success: false, message: 'Internal server error during CAPTCHA verification.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
42
routes/contactRoutes.js
Normal file
42
routes/contactRoutes.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const { body, validationResult } = require('express-validator');
|
||||||
|
const contactController = require('../controllers/contactController');
|
||||||
|
const { formSecurityCheck } = require('../middleware/securityMw');
|
||||||
|
|
||||||
|
// 🛡️ Configure rate limiting to prevent DDoS and spamming
|
||||||
|
const apiLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 5,
|
||||||
|
message: "Too many requests from this IP, please try again after 15 minutes."
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the route for form submissions
|
||||||
|
router.post('/submit-form',
|
||||||
|
apiLimiter,
|
||||||
|
[
|
||||||
|
// express-validator: sanitation and validation
|
||||||
|
body('firstName').trim().escape(),
|
||||||
|
body('lastName').trim().escape(),
|
||||||
|
body('email').isEmail().normalizeEmail(),
|
||||||
|
body('organization').trim().escape(),
|
||||||
|
body('phone').trim(),
|
||||||
|
body('message').trim().escape(),
|
||||||
|
],
|
||||||
|
// Middleware to handle the express-validator results
|
||||||
|
(req, res, next) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
console.error('Validation failed:', errors.array());
|
||||||
|
return res.status(400).json({ success: false, message: 'Invalid form data.' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
// The security middleware
|
||||||
|
formSecurityCheck,
|
||||||
|
// The controller, which is the final step
|
||||||
|
contactController.submitForm
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = router;
|
128
server.js
128
server.js
@@ -4,9 +4,6 @@ const path = require('path');
|
|||||||
const { Pool } = require('pg');
|
const { Pool } = require('pg');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const { body, validationResult } = require('express-validator');
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3000;
|
const port = 3000;
|
||||||
|
|
||||||
@@ -35,129 +32,14 @@ const transporter = nodemailer.createTransport({
|
|||||||
user: process.env.EMAIL_USER,
|
user: process.env.EMAIL_USER,
|
||||||
pass: process.env.EMAIL_PASS,
|
pass: process.env.EMAIL_PASS,
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
// A simple test to check if the database connection is working
|
|
||||||
pool.query('SELECT NOW()', (err, res) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Database connection error:', err);
|
|
||||||
} else {
|
|
||||||
console.log('Database connected successfully!');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rate limiting middleware to prevent brute-force attacks
|
// Import contactRoutes and contactController
|
||||||
const formLimiter = rateLimit({
|
const contactRoutes = require('./routes/contactRoutes');
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
const contactController = require('./controllers/contactController')(pool, transporter);
|
||||||
max: 50, // Limit each IP to 50 requests per window
|
|
||||||
message: 'Too many form submissions from this IP. Please try again later.'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the POST route for form submissions with rate limiting applied
|
// Use contactRoutes to connect the modular router to the main app
|
||||||
app.post('/submit-form', formLimiter,
|
app.use(contactRoutes);
|
||||||
|
|
||||||
// Middleware for sanitation and validation
|
|
||||||
[
|
|
||||||
body('firstName').trim().escape(),
|
|
||||||
body('lastName').trim().escape(),
|
|
||||||
body('email').isEmail().normalizeEmail(),
|
|
||||||
body('organization').trim().escape(),
|
|
||||||
body('phone').trim(),
|
|
||||||
body('message').trim().escape(),
|
|
||||||
],
|
|
||||||
async (req, res) => {
|
|
||||||
|
|
||||||
// Check for validation errors from the middleware above
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
console.error('Validation failed:', errors.array());
|
|
||||||
return res.status(400).json({ success: false, message: 'Invalid form data.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Honeypot check (first line of defense)
|
|
||||||
if (req.body.url) {
|
|
||||||
console.warn('Bot detected! Honeypot field was filled.');
|
|
||||||
// Respond with a success status to avoid alerting the bot
|
|
||||||
return res.status(200).json({ success: true, message: 'Thank you for your submission.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the data from the request body
|
|
||||||
const { firstName, lastName, organization, email, phone, contactMethod, message, hCaptchaResponse } = req.body;
|
|
||||||
|
|
||||||
// Use a single try-catch block for all core logic
|
|
||||||
try {
|
|
||||||
// 2. hCaptcha verification (second line of defense)
|
|
||||||
if (!hCaptchaResponse) {
|
|
||||||
return res.status(400).json({ success: false, message: 'CAPTCHA token missing.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretKey = process.env.HCAPTCHA_SECRET;
|
|
||||||
const verificationUrl = 'https://hcaptcha.com/siteverify';
|
|
||||||
|
|
||||||
const verificationResponse = await fetch(verificationUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
||||||
body: new URLSearchParams({
|
|
||||||
secret: secretKey,
|
|
||||||
response: hCaptchaResponse
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const verificationData = await verificationResponse.json();
|
|
||||||
|
|
||||||
if (!verificationData.success) {
|
|
||||||
console.error('hCaptcha verification failed:', verificationData['error-codes']);
|
|
||||||
return res.status(400).json({ success: false, message: 'CAPTCHA verification failed. Please try again.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Insert form data into the database
|
|
||||||
const result = await pool.query(
|
|
||||||
`INSERT INTO submissions(first_name, last_name, organization, email, phone, contact_method, message)
|
|
||||||
VALUES($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
|
|
||||||
[firstName, lastName, organization, email, phone, contactMethod, message]
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Successfully saved submission to the database:', result.rows[0]);
|
|
||||||
|
|
||||||
// 4. Send the email notification
|
|
||||||
const mailOptions = {
|
|
||||||
from: `"Contact Form" <contact@dlseitz.dev>`,
|
|
||||||
to: process.env.EMAIL_RCPT,
|
|
||||||
subject: 'New Contact Form Submission',
|
|
||||||
html: `
|
|
||||||
<h3>New Submission</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>First Name:</strong> ${firstName}</li>
|
|
||||||
<li><strong>Last Name:</strong> ${lastName}</li>
|
|
||||||
<li><strong>Organization:</strong> ${organization}</li>
|
|
||||||
<li><strong>Email:</strong> ${email}</li>
|
|
||||||
<li><strong>Phone:</strong> ${phone}</li>
|
|
||||||
<li><strong>Contact Method:</strong> ${contactMethod}</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>Message:</strong></p>
|
|
||||||
<p>${message}</p>
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const emailInfo = await transporter.sendMail(mailOptions);
|
|
||||||
console.log('Message sent: %s', emailInfo.messageId);
|
|
||||||
|
|
||||||
// 5. Send a success response back to the client
|
|
||||||
res.status(200).json({
|
|
||||||
message: 'Form submitted successfully and saved to the database!',
|
|
||||||
dataReceived: req.body
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
// This single catch block will handle errors from hCaptcha, the database, or nodemailer
|
|
||||||
console.error('An error occurred during form submission:', err.stack);
|
|
||||||
|
|
||||||
res.status(500).json({
|
|
||||||
message: 'An error occurred. Please try again.',
|
|
||||||
error: err.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
|
Reference in New Issue
Block a user