feat: Add initial monolithic backend server

This commit is contained in:
2025-08-17 16:42:03 -05:00
parent 3a15b949ad
commit 456ea795e6
4 changed files with 1287 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

1095
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "dlseitz.dev-backend",
"version": "1.0.0",
"description": "Backend server application and modules for https://dlseitz.dev",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "git@git.dsnet.pro:dereklseitz/dlseitz.dev-backend.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^17.2.1",
"express": "^5.1.0",
"express-rate-limit": "^8.0.1",
"express-validator": "^7.2.1",
"node-fetch": "^2.7.0",
"nodemailer": "^7.0.5",
"pg": "^8.16.3"
}
}

165
server.js Normal file
View File

@@ -0,0 +1,165 @@
const nodemailer = require('nodemailer');
const express = require('express');
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;
// Middleware to parse incoming JSON data from the frontend
app.use(express.json());
// Middleware to serve static files (like index.html, styles.css, script.js)
app.use(express.static(path.join(__dirname, 'public')));
// Database connection pool setup using environment variables for security
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_DATABASE,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
});
// Nodemailer transporter setup for sending emails
const transporter = nodemailer.createTransport({
host: 'smtp-relay.brevo.com',
port: 2525,
secure: false,
requireTLS: true,
auth: {
user: process.env.EMAIL_USER,
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.'
});
// 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
});
}
});
// Start the server
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});