Files
SkyArtShop/backend/routes/contact-newsletter.js

395 lines
15 KiB
JavaScript
Raw Permalink Normal View History

2026-01-20 20:29:33 -06:00
/**
* 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: `
<div style="font-family: 'Segoe UI', Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 30px; background: linear-gradient(135deg, #FFEBEB 0%, #FFD0D0 100%); border-radius: 20px;">
<div style="text-align: center; margin-bottom: 20px;">
<h1 style="color: #202023; margin: 0;">📬 New Contact Message</h1>
</div>
<div style="background: white; border-radius: 16px; padding: 30px;">
<p style="color: #202023; font-size: 16px; margin-bottom: 10px;">
<strong>From:</strong> ${sanitizedName}
</p>
<p style="color: #202023; font-size: 16px; margin-bottom: 10px;">
<strong>Email:</strong> <a href="mailto:${sanitizedEmail}">${sanitizedEmail}</a>
</p>
<p style="color: #202023; font-size: 16px; margin-bottom: 10px;">
<strong>Subject:</strong> ${sanitizedSubject}
</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="color: #202023; font-size: 16px; margin-bottom: 10px;">
<strong>Message:</strong>
</p>
<div style="background: #f9f9f9; padding: 20px; border-radius: 10px; color: #333; line-height: 1.6;">
${sanitizedMessage.replace(/\n/g, "<br>")}
</div>
</div>
<p style="text-align: center; color: #666; font-size: 12px; margin-top: 20px;">
© ${new Date().getFullYear()} Sky Art Shop - Contact Form Notification
</p>
</div>
`,
});
// 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: `
<div style="font-family: 'Segoe UI', Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 30px; background: linear-gradient(135deg, #FFEBEB 0%, #FFD0D0 100%); border-radius: 20px;">
<div style="text-align: center; margin-bottom: 20px;">
<h1 style="color: #202023; margin: 0;">🎨 Thank You!</h1>
</div>
<div style="background: white; border-radius: 16px; padding: 30px;">
<p style="color: #202023; font-size: 16px;">
Hi ${sanitizedName},
</p>
<p style="color: #202023; font-size: 16px; line-height: 1.6;">
Thank you for reaching out to Sky Art Shop! We've received your message and will get back to you within 24-48 hours.
</p>
<p style="color: #202023; font-size: 16px; line-height: 1.6;">
Here's a copy of your message:
</p>
<div style="background: #f9f9f9; padding: 20px; border-radius: 10px; margin: 20px 0;">
<p style="color: #666; font-size: 14px; margin: 0 0 10px 0;"><strong>Subject:</strong> ${sanitizedSubject}</p>
<p style="color: #333; font-size: 14px; margin: 0; line-height: 1.6;">${sanitizedMessage.replace(/\n/g, "<br>")}</p>
</div>
<p style="color: #202023; font-size: 16px;">
In the meantime, feel free to explore our collection at <a href="https://skyartshop.com/shop" style="color: #FCB1D8;">our shop</a>!
</p>
<p style="color: #202023; font-size: 16px;">
Best regards,<br>
The Sky Art Shop Team
</p>
</div>
<p style="text-align: center; color: #666; font-size: 12px; margin-top: 20px;">
© ${new Date().getFullYear()} Sky Art Shop
</p>
</div>
`,
});
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: `
<div style="font-family: 'Segoe UI', Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 30px; background: linear-gradient(135deg, #FFEBEB 0%, #FFD0D0 100%); border-radius: 20px;">
<div style="text-align: center; margin-bottom: 20px;">
<h1 style="color: #202023; margin: 0;">🎨 Welcome to Sky Art Shop!</h1>
</div>
<div style="background: white; border-radius: 16px; padding: 30px;">
<p style="color: #202023; font-size: 16px; line-height: 1.6;">
Thank you for subscribing to our newsletter! You're now part of our creative community.
</p>
<p style="color: #202023; font-size: 16px; line-height: 1.6;">
Here's what you can expect:
</p>
<ul style="color: #202023; font-size: 16px; line-height: 1.8;">
<li>🛍 Exclusive deals and discounts</li>
<li> New product announcements</li>
<li>💡 Creative tips and inspiration</li>
<li>🎁 Special subscriber-only offers</li>
</ul>
<div style="text-align: center; margin-top: 30px;">
<a href="https://skyartshop.com/shop" style="display: inline-block; background: linear-gradient(135deg, #FCB1D8 0%, #F6CCDE 100%); color: #202023; text-decoration: none; padding: 15px 40px; border-radius: 50px; font-weight: 600;">
Start Shopping
</a>
</div>
</div>
<p style="text-align: center; color: #666; font-size: 12px; margin-top: 20px;">
© ${new Date().getFullYear()} Sky Art Shop<br>
<a href="https://skyartshop.com/unsubscribe?email=${encodeURIComponent(sanitizedEmail)}" style="color: #666;">Unsubscribe</a>
</p>
</div>
`,
});
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;