feat: Add initial monolithic backend server
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
1095
package-lock.json
generated
Normal file
1095
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
package.json
Normal file
26
package.json
Normal 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
165
server.js
Normal 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}`);
|
||||||
|
});
|
Reference in New Issue
Block a user