Compare commits
35 Commits
main
...
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 | ||
|
5c0b291b4a | ||
|
0dea7fcaec | ||
|
b1093f3cfc | ||
|
3f4c21cebd | ||
|
ac51cd7493 | ||
|
83bd7b97f6 | ||
|
9f47800032 | ||
|
5116cdc445 | ||
|
e8b55caaed | ||
|
9637221240 | ||
|
800db12439 | ||
|
0b48a79bec | ||
|
562d831ddf |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
.env
|
146
README.md
146
README.md
@@ -1,3 +1,145 @@
|
|||||||
# dlseitz.dev-backend
|
# dlseitz.dev: A Backend Demonstration
|
||||||
|
|
||||||
Backend server application and modules for https://dlseitz.dev
|
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
|
||||||
|
|
||||||
|
* [dlseitz.dev: A Backend Demonstration](#dlseitzdev-a-backend-demonstration)
|
||||||
|
* [Introduction](#introduction)
|
||||||
|
* [The Problem](#the-problem)
|
||||||
|
* [The Solution](#the-solution)
|
||||||
|
* [The Vision](#the-vision)
|
||||||
|
* [Core Architecture](#core-architecture)
|
||||||
|
* [Technology Stack](#technology-stack)
|
||||||
|
* [Data Flow](#data-flow)
|
||||||
|
* [Security & Reliability](#security--reliability)
|
||||||
|
* [Threat Mitigation](#threat-mitigation)
|
||||||
|
* [Code Integrity](#code-integrity)
|
||||||
|
* [Deployment & Infrastructure](#deployment--infrastructure)
|
||||||
|
* [The Ecosystem](#the-ecosystem)
|
||||||
|
* [Hosting](#hosting)
|
||||||
|
* [Issues & Lessons Learned](#issues--lessons-learned)
|
||||||
|
* [Challenges Overcome](#challenges-overcome)
|
||||||
|
* [Reflections](#reflections)
|
||||||
|
* [Looking to the Future](#looking-to-the-future)
|
||||||
|
* [Blog & Content Management](#blog--content-management)
|
||||||
|
* [Enhanced Security](#enhanced-security)
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
While the frontend project demonstrated a strong grasp of design and client-side development, a true showcase of full-stack capabilities required more than just a static website. The primary challenge was creating a secure and reliable system to handle dynamic data, specifically to capture and process inquiries from potential clients via the website's contact form. This project serves to prove my ability to bridge the gap between user-facing interactions and the server-side logic required to manage and store that data effectively.
|
||||||
|
|
||||||
|
### The Solution
|
||||||
|
|
||||||
|
To address this, I designed and built a dedicated backend server from scratch. This solution provides a secure, lightweight, and purpose-built endpoint for the contact form submissions. By separating the backend from the frontend, I was able to create a highly focused and scalable service. The solution is built with a commitment to **efficiency**, **security**, and **reliability**, ensuring that every client inquiry is handled with integrity and that the system remains resilient under real-world conditions.
|
||||||
|
|
||||||
|
### The Vision
|
||||||
|
|
||||||
|
The backend is a critical piece of the overall business infrastructure. It's designed to be a long-term asset that not only facilitates client engagement but also serves as a robust demonstration of server-side development skills. This project provides a clear path for future expansion, whether that involves adding new API endpoints for dynamic content, integrating with third-party services, or scaling the database to handle increased traffic. The ultimate vision is to have a professional brand that is built on the values of **accessibility**, **equity**, and **transparency**, and a pipeline for future client work.
|
||||||
|
|
||||||
|
[Back to Top](#dlseitzdev--a-backend-demonstration)
|
||||||
|
---
|
||||||
|
|
||||||
|
# Core Architecture
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
|
||||||
|
The backend is built using a modern, efficient, and reliable technology stack.
|
||||||
|
|
||||||
|
* **Node.js/Express.js:** I chose Node.js for its non-blocking, asynchronous architecture, which is highly efficient for I/O-heavy tasks like handling form submissions. The Express.js framework provides a minimal and flexible foundation, allowing me to build a custom API without unnecessary overhead.
|
||||||
|
* **PostgreSQL:** PostgreSQL was selected as the database for its reputation as a powerful, reliable, and standards-compliant relational database. It provides a secure and organized way to store and manage the structured data from client inquiries.
|
||||||
|
|
||||||
|
This combination of technologies creates a backend that is lightweight, scalable, and secure, ensuring that the system is both performant and maintainable for the long term.
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
The process begins when a user submits the contact form on the static frontend.
|
||||||
|
|
||||||
|
1. **Client-Side Validation and Submission:** The user's input is first validated on the frontend to ensure it meets the required format and that the user isn't a bot. Once validated, the data is sent to the backend as a JSON object using an asynchronous `fetch` request.
|
||||||
|
|
||||||
|
2. **API Endpoint Reception and Processing:** The Express.js server receives the incoming JSON payload at a dedicated API endpoint. The server then validates the data again to ensure its integrity and security before processing.
|
||||||
|
|
||||||
|
3. **Database Storage:** The validated data is then saved into a table in the PostgreSQL database. This step ensures that a permanent record of the client inquiry is maintained for future reference.
|
||||||
|
|
||||||
|
4. **Email Service Integration:** After the data is successfully stored in the database, the backend uses a secure email service to send a notification to a pre-defined email address. This step provides an immediate alert for new client inquiries.
|
||||||
|
|
||||||
|
5. **Confirmation to Frontend:** The backend server sends a response back to the frontend, indicating that the form submission was successful. The frontend then presents a confirmation message to the user, completing the cycle.
|
||||||
|
|
||||||
|
[Back to Top](#dlseitzdev--a-backend-demonstration)
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security & Reliability
|
||||||
|
|
||||||
|
### Threat Mitigation
|
||||||
|
|
||||||
|
Building a reliable and trustworthy system required a proactive approach to security, with measures implemented at multiple layers to mitigate potential threats. The contact form is a critical access point, and as such, it's fortified with several defenses to ensure the integrity of the data and the security of the backend.
|
||||||
|
|
||||||
|
First, **client-side validation** acts as the initial barrier. While it is not a foolproof security measure, it provides a seamless user experience by catching malformed or missing input before a request is even sent to the server.
|
||||||
|
|
||||||
|
Second, the backend performs its own rigorous **server-side input validation**. This is the definitive security step. Every piece of data received from the frontend is sanitized and validated against a strict schema to prevent common injection attacks, such as SQL injection.
|
||||||
|
|
||||||
|
To protect against automated bot submissions and spam, the form uses two distinct methods: a **hCaptcha** and a **honeypot field**. The hCaptcha requires user interaction to verify that they are human, effectively stopping most automated scripts. The honeypot field is a hidden input that, if filled, immediately flags the submission as spam, as a human user would never see it.
|
||||||
|
|
||||||
|
Finally, to prevent resource exhaustion from denial-of-service (DoS) attacks, the API is protected with **rate limiting**. This ensures that no single user or IP address can make an excessive number of requests in a short period, preserving the server's availability and stability for legitimate users.
|
||||||
|
|
||||||
|
### Code Integrity
|
||||||
|
|
||||||
|
The integrity and security of the codebase are maintained by the strategic use of **environment variables**. All sensitive information, such as database credentials and API keys, are stored in a separate `.env` file that is kept out of the public codebase and git repository. This practice ensures that confidential data remains secure, even if the code is made public.
|
||||||
|
|
||||||
|
[Back to Top](#dlseitzdev--a-backend-demonstration)
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment & Infrastructure
|
||||||
|
|
||||||
|
### The Ecosystem
|
||||||
|
|
||||||
|
The backend server is deployed within a robust and efficient ecosystem designed for reliability and ease of maintenance. This setup includes three key components: NGINX, Ubuntu, and PM2.
|
||||||
|
|
||||||
|
* **NGINX (Reverse Proxy):** NGINX is a lightweight, high-performance web server that acts as a reverse proxy in this infrastructure. It is the public-facing entry point for all incoming HTTP requests, which it then forwards to the Node.js application running on the server. This setup provides a crucial layer of security, as NGINX can handle tasks like SSL termination and request buffering, while also hiding the underlying application from direct public access. It also serves static content, which reduces the load on the backend server.
|
||||||
|
* **Ubuntu (Server OS):** Ubuntu Server was chosen as the operating system for its stability, widespread community support, and robust security features. As a Debian-based Linux distribution, it provides a secure and reliable foundation for the entire application, and its long-term support (LTS) versions ensure that the system receives security updates for an extended period without the need for frequent upgrades.
|
||||||
|
* **PM2 (Process Manager):** To ensure the application remains available 24/7, I used PM2. This process manager for Node.js applications is configured to keep the backend server running indefinitely. If the application crashes for any reason, PM2 will automatically restart it without any downtime. It also simplifies the management of the application by providing a dashboard to monitor its health, manage logs, and handle server restarts.
|
||||||
|
|
||||||
|
### Hosting
|
||||||
|
|
||||||
|
The project is hosted on a cost-effective cloud provider. This decision was a direct response to the initial project constraints, allowing me to deploy a full-stack application with minimal financial investment. Opting for a solution that is both professional and budget-friendly demonstrates a key value of the project: resourcefulness. It shows the ability to provide a complete, real-world solution while adhering to situational constraints. This strategic choice reinforces my commitment to building pragmatic solutions that are not only technically sound but also economically viable.
|
||||||
|
|
||||||
|
[Back to Top](#dlseitzdev--a-backend-demonstration)
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issues & Lessons Learned
|
||||||
|
|
||||||
|
### Challenges Overcome
|
||||||
|
|
||||||
|
A significant challenge during development was ensuring reliable and secure email delivery for client inquiries. Initially, I attempted to send emails directly from the server on port 587, a common practice for SMTP. However, the hosting provider actively blocks this port to prevent spam, which resulted in all contact form submissions failing to trigger email notifications.
|
||||||
|
|
||||||
|
To overcome this, I had to pivot the email delivery strategy. The solution was to implement a third-party mail relay service. I chose **Brevo** (formerly Sendinblue) as an intermediary to handle all outgoing mail. This required a re-architecting of the application's email functionality to integrate with Brevo's API. This not only solved the port-blocking issue but also added a layer of professionalism by using a dedicated service, improving deliverability and providing valuable analytics and logs.
|
||||||
|
|
||||||
|
Another challenge involved refining the backend's structure. As the project grew, it became clear that the monolithic codebase was becoming difficult to manage and scale. I made the decision to refactor the entire application into a more modular and organized architecture. This involved separating concerns, such as routing, database interactions, and API logic, into distinct files and directories. This restructuring will make the application more maintainable and easier to debug for future development.
|
||||||
|
|
||||||
|
The most difficult challenge so far has been migrating from a simple `.env` file to a more secure secrets manager. This is a critical security upgrade, but it has introduced significant complexity into the deployment process, requiring changes to how the application accesses and manages sensitive data. The process has been a valuable lesson in balancing development speed with robust security practices.
|
||||||
|
|
||||||
|
### Reflections
|
||||||
|
|
||||||
|
This project has been a valuable exercise in understanding that a full-stack solution is more than just connecting a frontend to a backend. It requires a holistic and integrated approach to software development, where every decision—from the initial architecture to the final deployment strategy—is interconnected. The experience has underscored the importance of anticipating and mitigating infrastructure-level challenges, such as blocked ports, and the necessity of building an application with scalability and maintainability in mind from day one. Ultimately, these struggles and their resolutions have solidified a key lesson: the most effective solutions are not just functional; they are resilient, secure, and thoughtfully planned.
|
||||||
|
|
||||||
|
[Back to Top](#dlseitzdev--a-backend-demonstration)
|
||||||
|
---
|
||||||
|
|
||||||
|
## Looking to the Future
|
||||||
|
|
||||||
|
This project serves as a foundational component for future development, and the current architecture provides a clear path for expansion. The strategic design of this system is meant to demonstrate a forward-thinking approach, proving that the solution is not just functional but also scalable and adaptable for future needs.
|
||||||
|
|
||||||
|
#### Blog & Content Management
|
||||||
|
|
||||||
|
I plan to add a dynamic blog to the website. This will involve expanding the backend to include new API endpoints that will handle a full content management system. These endpoints will allow for the secure creation, editing, and publishing of blog posts. The content will be stored in the PostgreSQL database, enabling me to manage and display new articles without the need for a full site rebuild. This expansion would demonstrate a deeper understanding of RESTful API design and database schema management for a multi-purpose application.
|
||||||
|
|
||||||
|
#### Enhanced Security
|
||||||
|
|
||||||
|
While the current security measures are robust, I have planned for further enhancements to harden the system against potential threats. A critical next step is to migrate from a simple `.env` file to a dedicated secrets manager. This will ensure that sensitive data, such as API keys and database credentials, are not stored on the file system and are instead accessed securely at runtime. Additionally, implementing an in-depth security monitoring and logging system would provide real-time visibility into application access and potential malicious activity, allowing for a more proactive defense strategy
|
||||||
|
|
||||||
|
[Back to Top](#dlseitzdev--a-backend-demonstration)
|
||||||
|
---
|
86
controllers/contactController.js
Normal file
86
controllers/contactController.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
module.exports = (pool, transporter) => {
|
||||||
|
const submitForm = async (req, res) => {
|
||||||
|
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 {
|
||||||
|
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]);
|
||||||
|
|
||||||
|
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>
|
||||||
|
<li><strong>Submission Time:</strong> ${result.rows[0].time_submitted}</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Message:</strong></p>
|
||||||
|
<p>${message}</p>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const emailInfo = await transporter.sendMail(mailOptions);
|
||||||
|
console.log('Message sent: %s', emailInfo.messageId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'Form submitted successfully and saved to the database!',
|
||||||
|
dataReceived: req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error occurred during form submission:', err.stack || err);
|
||||||
|
res.status(500).json({
|
||||||
|
message: 'An error occurred while submitting the form.',
|
||||||
|
error: err.message || 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
submitForm,
|
||||||
|
};
|
||||||
|
};
|
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
|
46
middleware/securityMw.js
Normal file
46
middleware/securityMw.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// securityMw.js
|
||||||
|
require('dotenv').config();
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
formSecurityCheck: async (req, res, next) => {
|
||||||
|
// 1. Honeypot check
|
||||||
|
if (req.body.url) {
|
||||||
|
console.warn('Bot detected! Honeypot field was filled.');
|
||||||
|
return res.status(200).json({ success: true, message: 'Thank you for your submission.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. hCaptcha verification
|
||||||
|
const hCaptchaResponse = req.body.hCaptchaResponse;
|
||||||
|
if (!hCaptchaResponse) {
|
||||||
|
return res.status(400).json({ success: false, message: 'CAPTCHA token missing.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
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.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('An error occurred during hCaptcha verification:', error);
|
||||||
|
return res.status(500).json({ success: false, message: 'Internal server error during CAPTCHA verification.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
40
routes/contactRoutes.js
Normal file
40
routes/contactRoutes.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
module.exports = (contactController, securityMw) => {
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const { body, validationResult } = require('express-validator');
|
||||||
|
|
||||||
|
// Rate limiter to prevent spam
|
||||||
|
const apiLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 5,
|
||||||
|
message: "Too many requests from this IP, please try again after 15 minutes."
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/submit-form',
|
||||||
|
apiLimiter,
|
||||||
|
securityMw.formSecurityCheck,
|
||||||
|
[
|
||||||
|
body('firstName').trim().escape(),
|
||||||
|
body('lastName').trim().escape(),
|
||||||
|
body('email').isEmail().normalizeEmail(),
|
||||||
|
body('organization').trim().escape(),
|
||||||
|
body('phone').trim(),
|
||||||
|
body('message').trim().escape(),
|
||||||
|
body('contactMethod')
|
||||||
|
.notEmpty().withMessage('Contact method is required.')
|
||||||
|
.isIn(['email', 'phone']).withMessage('Contact method must be email or phone.'),
|
||||||
|
],
|
||||||
|
(req, res, next) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
console.error('Validation failed:', errors.array());
|
||||||
|
return res.status(400).json({ success: false, message: 'Invalid form data.', errors: errors.array() });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
contactController.submitForm
|
||||||
|
);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
150
server.js
150
server.js
@@ -1,22 +1,25 @@
|
|||||||
|
//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');
|
||||||
const { Pool } = require('pg');
|
const { Pool } = require('pg');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const { body, validationResult } = require('express-validator');
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const 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'
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
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,
|
||||||
@@ -25,141 +28,24 @@ 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: 'smtp-relay.brevo.com',
|
host: process.env.EMAIL_HOST,
|
||||||
port: 2525,
|
port: 2525,
|
||||||
secure: false,
|
secure: false,
|
||||||
requireTLS: true,
|
requireTLS: true,
|
||||||
auth: {
|
auth: {
|
||||||
user: process.env.EMAIL_USER,
|
user: process.env.EMAIL_USER,
|
||||||
pass: process.env.EMAIL_PASS,
|
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 contactController = require('./controllers/contactController')(pool, transporter);
|
||||||
const formLimiter = rateLimit({
|
const securityMw = require('./middleware/securityMw');
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
const contactRoutes = require('./routes/contactRoutes')(contactController, securityMw);
|
||||||
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.use('/api', contactRoutes);
|
||||||
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, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Server listening at http://localhost:${port}`);
|
console.log(`Server listening at http://localhost:${port}`);
|
||||||
});
|
});
|
Reference in New Issue
Block a user