webupdate
This commit is contained in:
394
backend/routes/contact-newsletter.js
Normal file
394
backend/routes/contact-newsletter.js
Normal file
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user