Compare commits
22 Commits
5c0b291b4a
...
refactor-m
Author | SHA1 | Date | |
---|---|---|---|
|
7a00cf7581 | ||
|
c31e6290ec | ||
|
167b06b0de | ||
|
9e7ab1e78d | ||
|
efc045ede4 | ||
|
8c3c574ddc | ||
|
161dd3dd46 | ||
|
0b00a63ce6 | ||
|
98b38278b3 | ||
|
d1e5c63dd2 | ||
|
f9b4eb68f1 | ||
|
cae3c892be | ||
|
5a29578c7d | ||
|
e86ca9e10c | ||
|
40f3f101d7 | ||
|
01d7bccfe1 | ||
|
5a1aae19c0 | ||
|
dfea256a61 | ||
|
d5dea7b42a | ||
|
0b7d7cb774 | ||
|
d11d943615 | ||
|
9cf06401f0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
.env
|
@@ -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
|
||||||
|
@@ -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
27
example.env
Normal 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
|
@@ -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) {
|
||||||
|
@@ -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;
|
||||||
};
|
};
|
||||||
|
23
server.js
23
server.js
@@ -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}`);
|
||||||
});
|
});
|
@@ -1 +0,0 @@
|
|||||||
this is a test file
|
|
Reference in New Issue
Block a user