35 Commits

Author SHA1 Message Date
dereklseitz
7a00cf7581 fix: update links to reflect new repo server location 2025-08-27 17:41:08 -05:00
dereklseitz
c31e6290ec fix: remove 'NOW()' command from SQL INSERT because a timestamp is inserted into the time_submitted property of the record by default through the schema 2025-08-26 12:19:29 -05:00
dereklseitz
167b06b0de fix: remove extra comma from formSubmit async block in contactController 2025-08-26 12:08:59 -05:00
dereklseitz
9e7ab1e78d fix: remove extra placeholder in SQL INSERT command to match payload array. 2025-08-26 12:00:52 -05:00
dereklseitz
efc045ede4 fix: Remove references to "privacyAccepted" and "privacy_accepted" from contactController and contactRoutes 2025-08-26 11:48:04 -05:00
dereklseitz
8c3c574ddc Revert: Bring backend application to its last known working state
This commit reverts the changes made in commit f9b4eb68f1

due to incorrect database schema changes that caused a backend error. The change

was made to prepare for a more robust solution that will be implemented in a

future commit.Revert "chore: update README.md"

This reverts commit f9b4eb68f1.
2025-08-26 11:04:15 -05:00
dereklseitz
161dd3dd46 a 2025-08-26 03:08:35 -05:00
dereklseitz
0b00a63ce6 h 2025-08-26 02:01:10 -05:00
dereklseitz
98b38278b3 . 2025-08-26 01:44:02 -05:00
dereklseitz
d1e5c63dd2 feat: Add privacyAccepted and timeSubmitted properties to DB and mailer 2025-08-25 23:34:03 -05:00
dereklseitz
f9b4eb68f1 chore: update README.md 2025-08-23 00:46:06 -05:00
dereklseitz
cae3c892be chore: add example env file, improve documentation and code comments 2025-08-23 00:27:01 -05:00
dereklseitz
5a29578c7d add .env to .gitignore in preparation for repo becoming public 2025-08-22 22:25:13 -05:00
dereklseitz
e86ca9e10c fix: change nodemailer port to 2525 2025-08-21 19:26:10 -05:00
dereklseitz
40f3f101d7 refactor: add commented titles to each file 2025-08-21 18:59:43 -05:00
dereklseitz
01d7bccfe1 add logging for debugging 2025-08-21 18:53:39 -05:00
dereklseitz
5a1aae19c0 add logging for debugging 2025-08-21 18:47:55 -05:00
dereklseitz
dfea256a61 fix: correct app.use() 2025-08-21 18:40:25 -05:00
dereklseitz
d5dea7b42a fix: correct app.use() 2025-08-21 17:34:17 -05:00
dereklseitz
0b7d7cb774 fix: correct app.use() 2025-08-21 17:29:51 -05:00
dereklseitz
d11d943615 fix: correct route for form submission 2025-08-21 16:51:52 -05:00
dereklseitz
9cf06401f0 fix: add /api prefix to contactRoutes call 2025-08-21 16:25:18 -05:00
dereklseitz
5c0b291b4a fix: Add securityMw.js import 2025-08-21 07:54:52 -05:00
dereklseitz
0dea7fcaec fix: Add corrected refernce to securityMw.js for honeypot and hCaptcha verification 2025-08-21 07:51:58 -05:00
dereklseitz
b1093f3cfc a 2025-08-20 22:41:58 -05:00
dereklseitz
3f4c21cebd fix 2025-08-20 22:01:34 -05:00
dereklseitz
ac51cd7493 fix: add dotenv requirement to securityMw.js 2025-08-20 11:05:54 -05:00
dereklseitz
83bd7b97f6 fix: syntax error in contactRoutes.js 2025-08-20 10:13:25 -05:00
dereklseitz
9f47800032 fix: move call for formSecurityCheck to before express validator array in contactRoutes.js 2025-08-20 08:32:35 -05:00
dereklseitz
5116cdc445 Revert "test"
This reverts commit 9637221240.
2025-08-19 22:58:38 -05:00
dereklseitz
e8b55caaed refactor: Remove hardcoded values from codebase, converting to environment variables 2025-08-19 15:51:47 -05:00
dereklseitz
9637221240 test 2025-08-18 15:46:24 -05:00
dereklseitz
800db12439 test file added to test Gitea webhook and server webhook application 2025-08-18 15:42:24 -05:00
dereklseitz
0b48a79bec fix: Resolve TypeError by correcting module dependency injection
This commit fixes a TypeError that occurred because the contactRoutes
module was being initialized before the contactController module.

The fix involves:
- Swapping the initialization order in server.js so the contactController is
  initialized before contactRoutes.
- Modifying contactRoutes.js to accept the initialized contactController
  as a dependency, rather than requiring it on its own.
2025-08-17 17:59:44 -05:00
dereklseitz
562d831ddf refactor: Convert monolithic app to modular architecture
This commit refactors the entire codebase from a monolithic structure to a
modular one. Key changes include:

-   Extracting core components (e.g., user authentication, data processing,
    API handlers) into their own distinct modules.
-   Implementing a new directory structure to support a modular design.
-   Updating all internal references and import paths to reflect the new
    architecture.

The new structure improves maintainability, scalability, and allows for
easier independent development of each module in the future.
2025-08-17 16:54:07 -05:00
7 changed files with 363 additions and 135 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
node_modules/
.env

146
README.md
View File

@@ -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)
---

View 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
View 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
View 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
View 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;
};

148
server.js
View File

@@ -1,22 +1,25 @@
//server.js
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;
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());
// Middleware to serve static files (like index.html, styles.css, script.js)
app.use(express.static(path.join(__dirname, 'public')));
const STATIC_DIR = process.env.STATIC_DIR || '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({
user: process.env.DB_USER,
host: process.env.DB_HOST,
@@ -25,9 +28,9 @@ const pool = new Pool({
port: process.env.DB_PORT,
});
// Nodemailer transporter setup for sending emails
// Configure Nodemailer for sending emails via Brevo relay
const transporter = nodemailer.createTransport({
host: 'smtp-relay.brevo.com',
host: process.env.EMAIL_HOST,
port: 2525,
secure: false,
requireTLS: true,
@@ -35,131 +38,14 @@ const transporter = nodemailer.createTransport({
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.'
});
const contactController = require('./controllers/contactController')(pool, transporter);
const securityMw = require('./middleware/securityMw');
const contactRoutes = require('./routes/contactRoutes')(contactController, securityMw);
// Define the POST route for form submissions with rate limiting applied
app.post('/submit-form', formLimiter,
app.use('/api', contactRoutes);
// 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}`);
});