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:
2025-08-17 16:54:07 -05:00
parent 456ea795e6
commit 562d831ddf
4 changed files with 150 additions and 124 deletions

View 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
View 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
View 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
View File

@@ -4,9 +4,6 @@ const path = require('path');
const { Pool } = require('pg');
const rateLimit = require('express-rate-limit');
require('dotenv').config();
const { body, validationResult } = require('express-validator');
const app = express();
const port = 3000;
@@ -37,127 +34,12 @@ const transporter = nodemailer.createTransport({
},
});
// 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!');
}
});
// Import contactRoutes and contactController
const contactRoutes = require('./routes/contactRoutes');
const contactController = require('./controllers/contactController')(pool, transporter);
// Rate limiting middleware to prevent brute-force attacks
const formLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
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
app.post('/submit-form', formLimiter,
// 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
});
}
});
// Use contactRoutes to connect the modular router to the main app
app.use(contactRoutes);
// Start the server
app.listen(port, () => {