From 562d831ddf34fa07318ba670c54f5077745d1c07 Mon Sep 17 00:00:00 2001 From: dereklseitz Date: Sun, 17 Aug 2025 16:54:07 -0500 Subject: [PATCH] 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. --- controllers/contactController.js | 57 ++++++++++++++ middleware/securityMw.js | 45 +++++++++++ routes/contactRoutes.js | 42 ++++++++++ server.js | 130 ++----------------------------- 4 files changed, 150 insertions(+), 124 deletions(-) create mode 100644 controllers/contactController.js create mode 100644 middleware/securityMw.js create mode 100644 routes/contactRoutes.js diff --git a/controllers/contactController.js b/controllers/contactController.js new file mode 100644 index 0000000..e504b0c --- /dev/null +++ b/controllers/contactController.js @@ -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" `, + to: process.env.EMAIL_RCPT, + subject: 'New Contact Form Submission', + html: ` +

New Submission

+ +

Message:

+

${message}

+ `, + }; + + 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, + }; +}; \ No newline at end of file diff --git a/middleware/securityMw.js b/middleware/securityMw.js new file mode 100644 index 0000000..2f04ed6 --- /dev/null +++ b/middleware/securityMw.js @@ -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.' }); + } + } +}; \ No newline at end of file diff --git a/routes/contactRoutes.js b/routes/contactRoutes.js new file mode 100644 index 0000000..263f830 --- /dev/null +++ b/routes/contactRoutes.js @@ -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; \ No newline at end of file diff --git a/server.js b/server.js index e50c8e8..1ca1c4c 100644 --- a/server.js +++ b/server.js @@ -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; @@ -33,131 +30,16 @@ const transporter = nodemailer.createTransport({ requireTLS: true, auth: { 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 -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.' -}); +// Import contactRoutes and contactController +const contactRoutes = require('./routes/contactRoutes'); +const contactController = require('./controllers/contactController')(pool, transporter); -// 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" `, - to: process.env.EMAIL_RCPT, - subject: 'New Contact Form Submission', - html: ` -

New Submission

-
    -
  • First Name: ${firstName}
  • -
  • Last Name: ${lastName}
  • -
  • Organization: ${organization}
  • -
  • Email: ${email}
  • -
  • Phone: ${phone}
  • -
  • Contact Method: ${contactMethod}
  • -
-

Message:

-

${message}

- `, - }; - - 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, () => {