Compare commits

..

22 Commits

Author SHA1 Message Date
dereklseitz
7a00cf7581 fix: update links to reflect new repo server location 2025-08-27 17:41:08 -05:00
dereklseitz
c31e6290ec fix: remove 'NOW()' command from SQL INSERT because a timestamp is inserted into the time_submitted property of the record by default through the schema 2025-08-26 12:19:29 -05:00
dereklseitz
167b06b0de fix: remove extra comma from formSubmit async block in contactController 2025-08-26 12:08:59 -05:00
dereklseitz
9e7ab1e78d fix: remove extra placeholder in SQL INSERT command to match payload array. 2025-08-26 12:00:52 -05:00
dereklseitz
efc045ede4 fix: Remove references to "privacyAccepted" and "privacy_accepted" from contactController and contactRoutes 2025-08-26 11:48:04 -05:00
dereklseitz
8c3c574ddc Revert: Bring backend application to its last known working state
This commit reverts the changes made in commit f9b4eb68f1

due to incorrect database schema changes that caused a backend error. The change

was made to prepare for a more robust solution that will be implemented in a

future commit.Revert "chore: update README.md"

This reverts commit f9b4eb68f1.
2025-08-26 11:04:15 -05:00
dereklseitz
161dd3dd46 a 2025-08-26 03:08:35 -05:00
dereklseitz
0b00a63ce6 h 2025-08-26 02:01:10 -05:00
dereklseitz
98b38278b3 . 2025-08-26 01:44:02 -05:00
dereklseitz
d1e5c63dd2 feat: Add privacyAccepted and timeSubmitted properties to DB and mailer 2025-08-25 23:34:03 -05:00
dereklseitz
f9b4eb68f1 chore: update README.md 2025-08-23 00:46:06 -05:00
dereklseitz
cae3c892be chore: add example env file, improve documentation and code comments 2025-08-23 00:27:01 -05:00
dereklseitz
5a29578c7d add .env to .gitignore in preparation for repo becoming public 2025-08-22 22:25:13 -05:00
dereklseitz
e86ca9e10c fix: change nodemailer port to 2525 2025-08-21 19:26:10 -05:00
dereklseitz
40f3f101d7 refactor: add commented titles to each file 2025-08-21 18:59:43 -05:00
dereklseitz
01d7bccfe1 add logging for debugging 2025-08-21 18:53:39 -05:00
dereklseitz
5a1aae19c0 add logging for debugging 2025-08-21 18:47:55 -05:00
dereklseitz
dfea256a61 fix: correct app.use() 2025-08-21 18:40:25 -05:00
dereklseitz
d5dea7b42a fix: correct app.use() 2025-08-21 17:34:17 -05:00
dereklseitz
0b7d7cb774 fix: correct app.use() 2025-08-21 17:29:51 -05:00
dereklseitz
d11d943615 fix: correct route for form submission 2025-08-21 16:51:52 -05:00
dereklseitz
9cf06401f0 fix: add /api prefix to contactRoutes call 2025-08-21 16:25:18 -05:00
8 changed files with 90 additions and 39 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
node_modules/ node_modules/
.env

View File

@@ -1,5 +1,6 @@
# dlseitz.dev: A Backend Demonstration # dlseitz.dev: A Backend Demonstration
To learn about the front end of this two-part project, check out [**dlseitz.dev A Frontend Demonstration**](https://gitea.dlseitz.dev/dereklseitz/dlseitz.dev-frontend/src/branch/main/README.md).
--- ---
## Table of Contents ## Table of Contents

View File

@@ -1,20 +1,48 @@
module.exports = (pool, transporter) => { module.exports = (pool, transporter) => {
// The main function that handles the form submission
const submitForm = async (req, res) => { const submitForm = async (req, res) => {
const { firstName, lastName, organization, email, phone, contactMethod, message } = req.body; const {
firstName,
lastName,
organization,
email,
phone,
contactMethod,
message
} = req.body;
if (
!firstName || !lastName || !email || !message ||
typeof contactMethod === 'undefined'
) {
console.error('Missing required fields in submission:', req.body);
return res.status(400).json({ message: 'Missing required form fields.' });
}
try { try {
// 1. Save submission to the database
const result = await pool.query( const result = await pool.query(
`INSERT INTO submissions(first_name, last_name, organization, email, phone, contact_method, message) `INSERT INTO submissions(
VALUES($1, $2, $3, $4, $5, $6, $7) RETURNING *`, first_name,
[firstName, lastName, organization, email, phone, contactMethod, message] 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]); console.log('Successfully saved submission to the database:', result.rows[0]);
// 2. Send the email notification
const mailOptions = { const mailOptions = {
from: `"Contact Form" <contact@dlseitz.dev>`, from: `"Contact Form" <contact@dlseitz.dev>`,
to: process.env.EMAIL_RCPT, to: process.env.EMAIL_RCPT,
@@ -28,6 +56,7 @@ module.exports = (pool, transporter) => {
<li><strong>Email:</strong> ${email}</li> <li><strong>Email:</strong> ${email}</li>
<li><strong>Phone:</strong> ${phone}</li> <li><strong>Phone:</strong> ${phone}</li>
<li><strong>Contact Method:</strong> ${contactMethod}</li> <li><strong>Contact Method:</strong> ${contactMethod}</li>
<li><strong>Submission Time:</strong> ${result.rows[0].time_submitted}</li>
</ul> </ul>
<p><strong>Message:</strong></p> <p><strong>Message:</strong></p>
<p>${message}</p> <p>${message}</p>
@@ -43,10 +72,10 @@ module.exports = (pool, transporter) => {
}); });
} catch (err) { } catch (err) {
console.error('An error occurred during form submission:', err.stack); console.error('Error occurred during form submission:', err.stack || err);
res.status(500).json({ res.status(500).json({
message: 'An error occurred. Please try again.', message: 'An error occurred while submitting the form.',
error: err.message error: err.message || 'Unknown error'
}); });
} }
}; };

27
example.env Normal file
View File

@@ -0,0 +1,27 @@
# Example environment variables for dlseitz.dev backend
# DO NOT PUT REAL CREDENTIALS IN HERE
# DO NOT COMMIT ACTUAL .env FILES TO VERSION CONTROL
# Database-related
#-----------------
DB_HOST=your_database_host
DB_PORT=5432
DB_USER=your_database_user
DB_PASSWORD=your_database_password
DB_NAME=your_database_name
# Email-related
#--------------
EMAIL_HOST=smtp.yourmail.com
EMAIL_PORT=587
EMAIL_USER=your_email_username
EMAIL_PASS=your_email_password
EMAIL_RCPT=recipient@example.com
# Security-related
#-----------------
HCAPTCHA_SECRET=your_hcaptcha_secret_key
# Other
#------
NODE_ENV=production

View File

@@ -1,15 +1,16 @@
// securityMw.js
require('dotenv').config(); require('dotenv').config();
const fetch = require('node-fetch'); const fetch = require('node-fetch');
module.exports = { module.exports = {
formSecurityCheck: async (req, res, next) => { formSecurityCheck: async (req, res, next) => {
// 1. Honeypot check (first line of defense) // 1. Honeypot check
if (req.body.url) { if (req.body.url) {
console.warn('Bot detected! Honeypot field was filled.'); console.warn('Bot detected! Honeypot field was filled.');
return res.status(200).json({ success: true, message: 'Thank you for your submission.' }); return res.status(200).json({ success: true, message: 'Thank you for your submission.' });
} }
// 2. hCaptcha verification (second line of defense) // 2. hCaptcha verification
const hCaptchaResponse = req.body.hCaptchaResponse; const hCaptchaResponse = req.body.hCaptchaResponse;
if (!hCaptchaResponse) { if (!hCaptchaResponse) {
return res.status(400).json({ success: false, message: 'CAPTCHA token missing.' }); return res.status(400).json({ success: false, message: 'CAPTCHA token missing.' });
@@ -35,7 +36,6 @@ module.exports = {
return res.status(400).json({ success: false, message: 'CAPTCHA verification failed. Please try again.' }); 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(); next();
} catch (error) { } catch (error) {

View File

@@ -1,45 +1,40 @@
// The entire module is now a function that accepts 'contactController' and security middleware as an argument.
module.exports = (contactController, securityMw) => { module.exports = (contactController, securityMw) => {
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const rateLimit = require('express-rate-limit'); const rateLimit = require('express-rate-limit');
const { body, validationResult } = require('express-validator'); const { body, validationResult } = require('express-validator');
// 🛡️ Configure rate limiting to prevent DDoS and spamming // Rate limiter to prevent spam
const apiLimiter = rateLimit({ const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, max: 5,
message: "Too many requests from this IP, please try again after 15 minutes." message: "Too many requests from this IP, please try again after 15 minutes."
}); });
// Define the route for form submissions with all middleware
router.post('/submit-form', router.post('/submit-form',
apiLimiter, apiLimiter,
// The security middleware is now a separate step,
// containing both the honeypot check and hCaptcha verification.
securityMw.formSecurityCheck, securityMw.formSecurityCheck,
[ [
// express-validator: sanitation and validation
body('firstName').trim().escape(), body('firstName').trim().escape(),
body('lastName').trim().escape(), body('lastName').trim().escape(),
body('email').isEmail().normalizeEmail(), body('email').isEmail().normalizeEmail(),
body('organization').trim().escape(), body('organization').trim().escape(),
body('phone').trim(), body('phone').trim(),
body('message').trim().escape(), body('message').trim().escape(),
body('contactMethod')
.notEmpty().withMessage('Contact method is required.')
.isIn(['email', 'phone']).withMessage('Contact method must be email or phone.'),
], ],
// Middleware to handle the express-validator results
(req, res, next) => { (req, res, next) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
console.error('Validation failed:', errors.array()); console.error('Validation failed:', errors.array());
return res.status(400).json({ success: false, message: 'Invalid form data.' }); return res.status(400).json({ success: false, message: 'Invalid form data.', errors: errors.array() });
} }
next(); next();
}, },
// The controller, which is the final step
contactController.submitForm contactController.submitForm
); );
// Return the configured router
return router; return router;
}; };

View File

@@ -1,3 +1,4 @@
//server.js
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
@@ -7,14 +8,18 @@ require('dotenv').config();
const app = express(); const app = express();
const port = process.env.SERVER_PORT || 3000; const port = process.env.SERVER_PORT || 3000;
// Middleware to parse incoming JSON data from the frontend app.use((req, res, next) => {
console.log(`Incoming request: ${req.method} ${req.url}`);
next();
});
app.set('trust proxy', 1);
app.use(express.json()); app.use(express.json());
// Middleware to serve static files (like index.html, styles.css, script.js)
const STATIC_DIR = process.env.STATIC_DIR || 'public' const STATIC_DIR = process.env.STATIC_DIR || 'public'
app.use(express.static(path.join(__dirname, STATIC_DIR))); app.use(express.static(path.join(__dirname, STATIC_DIR)));
// Database connection pool setup using environment variables for security // Setup database connection pool using environment variables
const pool = new Pool({ const pool = new Pool({
user: process.env.DB_USER, user: process.env.DB_USER,
host: process.env.DB_HOST, host: process.env.DB_HOST,
@@ -23,10 +28,10 @@ const pool = new Pool({
port: process.env.DB_PORT, port: process.env.DB_PORT,
}); });
// Nodemailer transporter setup for sending emails // Configure Nodemailer for sending emails via Brevo relay
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST, host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT, port: 2525,
secure: false, secure: false,
requireTLS: true, requireTLS: true,
auth: { auth: {
@@ -36,17 +41,11 @@ const transporter = nodemailer.createTransport({
}); });
const contactController = require('./controllers/contactController')(pool, transporter); const contactController = require('./controllers/contactController')(pool, transporter);
// Import the security middleware
const securityMw = require('./middleware/securityMw'); const securityMw = require('./middleware/securityMw');
// Import contactRoutes and contactController, and pass in securityMw
const contactRoutes = require('./routes/contactRoutes')(contactController, securityMw); const contactRoutes = require('./routes/contactRoutes')(contactController, securityMw);
// Use contactRoutes to connect the modular router to the main app app.use('/api', contactRoutes);
app.use(contactRoutes);
// Start the server
app.listen(port, () => { app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`); console.log(`Server listening at http://localhost:${port}`);
}); });

View File

@@ -1 +0,0 @@
this is a test file