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