/**
* Contact and Newsletter Routes
* Handles contact form submissions and newsletter subscriptions
*/
const express = require("express");
const nodemailer = require("nodemailer");
const { query } = require("../config/database");
const logger = require("../config/logger");
const { asyncHandler } = require("../middleware/errorHandler");
const { sendSuccess, sendError } = require("../utils/responseHelpers");
const rateLimit = require("express-rate-limit");
const router = express.Router();
// Rate limiting for contact form (5 submissions per 15 minutes)
const contactLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5,
message: {
success: false,
message: "Too many contact requests. Please try again later.",
},
});
// Rate limiting for newsletter (10 subscriptions per hour)
const newsletterLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10,
message: {
success: false,
message: "Too many subscription attempts. Please try again later.",
},
});
// Create email transporter
function createTransporter() {
if (
!process.env.SMTP_HOST ||
!process.env.SMTP_USER ||
!process.env.SMTP_PASS
) {
logger.warn("SMTP not configured - emails will be logged only");
return null;
}
return nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT) || 587,
secure: process.env.SMTP_SECURE === "true",
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
}
// ===========================
// CONTACT FORM SUBMISSION
// ===========================
router.post(
"/contact",
contactLimiter,
asyncHandler(async (req, res) => {
const { name, email, subject, message } = req.body;
// Validation
if (!name || !email || !subject || !message) {
return sendError(res, "All fields are required", 400);
}
// Basic email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return sendError(res, "Please provide a valid email address", 400);
}
// Sanitize inputs
const sanitizedName = name.trim().slice(0, 255);
const sanitizedEmail = email.trim().toLowerCase().slice(0, 255);
const sanitizedSubject = subject.trim().slice(0, 500);
const sanitizedMessage = message.trim().slice(0, 5000);
try {
// Save to database
await query(
`INSERT INTO contact_messages (name, email, subject, message)
VALUES ($1, $2, $3, $4)`,
[sanitizedName, sanitizedEmail, sanitizedSubject, sanitizedMessage],
);
logger.info(`Contact form submission from ${sanitizedEmail}`);
// Send email notification to admin
const transporter = createTransporter();
if (transporter) {
try {
// Email to shop owner
await transporter.sendMail({
from:
process.env.SMTP_FROM ||
`"Sky Art Shop" <${process.env.SMTP_USER}>`,
to: process.env.SMTP_USER, // Send to the shop's email
replyTo: sanitizedEmail,
subject: `📬 New Contact Form Message: ${sanitizedSubject}`,
html: `
📬 New Contact Message
From: ${sanitizedName}
Email: ${sanitizedEmail}
Subject: ${sanitizedSubject}
Message:
${sanitizedMessage.replace(/\n/g, "
")}
© ${new Date().getFullYear()} Sky Art Shop - Contact Form Notification
`,
});
// Auto-reply to customer
await transporter.sendMail({
from:
process.env.SMTP_FROM ||
`"Sky Art Shop" <${process.env.SMTP_USER}>`,
to: sanitizedEmail,
subject: `Thank you for contacting Sky Art Shop! 🎨`,
html: `
🎨 Thank You!
Hi ${sanitizedName},
Thank you for reaching out to Sky Art Shop! We've received your message and will get back to you within 24-48 hours.
Here's a copy of your message:
Subject: ${sanitizedSubject}
${sanitizedMessage.replace(/\n/g, "
")}
In the meantime, feel free to explore our collection at our shop!
Best regards,
The Sky Art Shop Team
© ${new Date().getFullYear()} Sky Art Shop
`,
});
logger.info(
`Contact notification and auto-reply sent for ${sanitizedEmail}`,
);
} catch (emailError) {
logger.error("Failed to send contact email:", emailError);
// Don't fail the request - message is saved to database
}
} else {
logger.info(
`Contact form (no SMTP): From: ${sanitizedName} <${sanitizedEmail}>, Subject: ${sanitizedSubject}`,
);
}
sendSuccess(res, {
message:
"Thank you for your message! We'll get back to you within 24-48 hours.",
});
} catch (error) {
logger.error("Contact form error:", error);
sendError(res, "Failed to send message. Please try again later.", 500);
}
}),
);
// ===========================
// NEWSLETTER SUBSCRIPTION
// ===========================
router.post(
"/newsletter/subscribe",
newsletterLimiter,
asyncHandler(async (req, res) => {
const { email, source = "website" } = req.body;
// Validation
if (!email) {
return sendError(res, "Email address is required", 400);
}
// Basic email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return sendError(res, "Please provide a valid email address", 400);
}
const sanitizedEmail = email.trim().toLowerCase().slice(0, 255);
const sanitizedSource = (source || "website").slice(0, 50);
try {
// Check if already subscribed (either in newsletter_subscribers or customers table)
const existingSubscriber = await query(
`SELECT id, is_active FROM newsletter_subscribers WHERE email = $1`,
[sanitizedEmail],
);
if (existingSubscriber.rows.length > 0) {
const subscriber = existingSubscriber.rows[0];
if (subscriber.is_active) {
return sendSuccess(res, {
message: "You're already subscribed to our newsletter!",
alreadySubscribed: true,
});
} else {
// Reactivate subscription
await query(
`UPDATE newsletter_subscribers
SET is_active = TRUE, subscribed_at = CURRENT_TIMESTAMP, unsubscribed_at = NULL, source = $1
WHERE email = $2`,
[sanitizedSource, sanitizedEmail],
);
logger.info(`Newsletter resubscription: ${sanitizedEmail}`);
}
} else {
// Check if they're a registered customer
const existingCustomer = await query(
`SELECT id, newsletter_subscribed FROM customers WHERE email = $1`,
[sanitizedEmail],
);
if (existingCustomer.rows.length > 0) {
if (existingCustomer.rows[0].newsletter_subscribed) {
return sendSuccess(res, {
message: "You're already subscribed to our newsletter!",
alreadySubscribed: true,
});
} else {
// Update customer's newsletter preference
await query(
`UPDATE customers SET newsletter_subscribed = TRUE WHERE email = $1`,
[sanitizedEmail],
);
logger.info(
`Customer newsletter subscription updated: ${sanitizedEmail}`,
);
}
} else {
// New subscriber - add to newsletter_subscribers table
await query(
`INSERT INTO newsletter_subscribers (email, source) VALUES ($1, $2)`,
[sanitizedEmail, sanitizedSource],
);
logger.info(
`New newsletter subscription: ${sanitizedEmail} (source: ${sanitizedSource})`,
);
}
}
// Send welcome email
const transporter = createTransporter();
if (transporter) {
try {
await transporter.sendMail({
from:
process.env.SMTP_FROM ||
`"Sky Art Shop" <${process.env.SMTP_USER}>`,
to: sanitizedEmail,
subject: `Welcome to Sky Art Shop Newsletter! 🎨✨`,
html: `
🎨 Welcome to Sky Art Shop!
Thank you for subscribing to our newsletter! You're now part of our creative community.
Here's what you can expect:
- 🛍️ Exclusive deals and discounts
- ✨ New product announcements
- 💡 Creative tips and inspiration
- 🎁 Special subscriber-only offers
© ${new Date().getFullYear()} Sky Art Shop
Unsubscribe
`,
});
logger.info(`Newsletter welcome email sent to ${sanitizedEmail}`);
} catch (emailError) {
logger.error("Failed to send newsletter welcome email:", emailError);
}
}
sendSuccess(res, {
message:
"Successfully subscribed! Check your email for a welcome message.",
});
} catch (error) {
logger.error("Newsletter subscription error:", error);
if (error.code === "23505") {
// Unique constraint violation
return sendSuccess(res, {
message: "You're already subscribed to our newsletter!",
alreadySubscribed: true,
});
}
sendError(res, "Failed to subscribe. Please try again later.", 500);
}
}),
);
// ===========================
// NEWSLETTER UNSUBSCRIBE
// ===========================
router.post(
"/newsletter/unsubscribe",
asyncHandler(async (req, res) => {
const { email } = req.body;
if (!email) {
return sendError(res, "Email address is required", 400);
}
const sanitizedEmail = email.trim().toLowerCase();
try {
// Update newsletter_subscribers table
await query(
`UPDATE newsletter_subscribers
SET is_active = FALSE, unsubscribed_at = CURRENT_TIMESTAMP
WHERE email = $1`,
[sanitizedEmail],
);
// Also update customers table if they're a customer
await query(
`UPDATE customers SET newsletter_subscribed = FALSE WHERE email = $1`,
[sanitizedEmail],
);
logger.info(`Newsletter unsubscription: ${sanitizedEmail}`);
sendSuccess(res, {
message: "You've been unsubscribed from our newsletter.",
});
} catch (error) {
logger.error("Newsletter unsubscribe error:", error);
sendError(res, "Failed to unsubscribe. Please try again later.", 500);
}
}),
);
module.exports = router;