// Load environment variables from .env file for development if (process.env.NODE_ENV !== 'production') { require('dotenv').config(); } const express = require('express'); const bodyParser = require('body-parser'); const fs = require('fs'); const path = require('path'); const cors = require('cors'); const multer = require('multer'); const sharp = require('sharp'); const nodemailer = require('nodemailer'); const app = express(); const port = 3050; // Admin password (shared secret). Defaults to "balloons" for development if not provided. const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'balloons'; // --- Production Security Check --- if (process.env.NODE_ENV === 'production' && (!ADMIN_PASSWORD || ADMIN_PASSWORD === "balloons")) { console.warn(` **************************************************************** ** WARNING: ADMIN_PASSWORD is not set or is insecure in production. ** ** If the admin UI is already behind auth, this may be acceptable. ** ** Otherwise, set a strong ADMIN_PASSWORD environment variable. ** **************************************************************** `); } // --- Middleware Setup --- // More explicit CORS configuration to allow all origins app.use(cors({ origin: '*' })); app.use(bodyParser.json()); // --- Contact form rate limiter (in-memory, 5 submissions per IP per 15 min) --- const contactRateLimit = new Map(); setInterval(() => { const now = Date.now(); for (const [ip, r] of contactRateLimit) { if (now > r.resetAt) contactRateLimit.delete(ip); } }, 3_600_000); // --- Email transporter --- const transporter = 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, }, }); // --- File upload (memory storage, max 3 files) --- const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 }, fileFilter: (req, file, cb) => { if (!file.mimetype.startsWith('image/')) { return cb(new Error('Only image files are allowed')); } cb(null, true); }, }); // --- API Routes --- const apiRouter = express.Router(); apiRouter.post('/update-status', (req, res) => { console.log(`[${new Date().toISOString()}] Received request for /api/update-status`); const { data } = req.body; if (!data) { return res.status(400).json({ success: false, message: 'Bad Request: No data provided.' }); } const jsonString = JSON.stringify(data, null, 4); const filePath = path.join(__dirname, 'update.json'); fs.writeFile(filePath, jsonString, (err) => { if (err) { console.error(`[${new Date().toISOString()}] Error writing to update.json:`, err); return res.status(500).json({ success: false, message: 'Internal Server Error: Could not write to file.' }); } console.log(`[${new Date().toISOString()}] update.json was successfully updated.`); res.json({ success: true, message: 'Status updated successfully.' }); }); }); apiRouter.post('/contact', upload.array('photos', 3), async (req, res) => { // Rate limiting const ip = req.ip || req.socket?.remoteAddress || 'unknown'; const now = Date.now(); const rl = contactRateLimit.get(ip) || { count: 0, resetAt: now + 15 * 60 * 1000 }; if (now > rl.resetAt) { rl.count = 0; rl.resetAt = now + 15 * 60 * 1000; } rl.count++; contactRateLimit.set(ip, rl); if (rl.count > 5) { return res.status(429).json({ success: false, message: 'Too many submissions. Please wait a few minutes and try again.' }); } // Honeypot ā silently succeed so bots think they won if (req.body.website) { return res.json({ success: true }); } const { name, email, phone, message, eventType, eventDate } = req.body; if (!name || !email || !phone || !message) { return res.status(400).json({ success: false, message: 'Please fill in all required fields.' }); } const attachments = []; for (const file of (req.files || [])) { const webpBuffer = await sharp(file.buffer).webp({ quality: 85 }).toBuffer(); const baseName = path.parse(file.originalname).name; attachments.push({ filename: `${baseName}.webp`, content: webpBuffer, contentType: 'image/webp' }); } function formatDate(str) { if (!str) return null; const [y, m, d] = str.split('-'); const months = ['January','February','March','April','May','June','July','August','September','October','November','December']; return `${months[parseInt(m, 10) - 1]} ${parseInt(d, 10)}, ${y}`; } const eventDateFormatted = formatDate(eventDate); const eventLines = [ eventType ? `
Event Type: ${eventType}
` : '', eventDateFormatted ? `Event Date: ${eventDateFormatted}
` : '', ].join(''); const eventText = [ eventType ? `Event Type: ${eventType}` : '', eventDateFormatted ? `Event Date: ${eventDateFormatted}` : '', ].filter(Boolean).join('\n'); const notifyMail = { from: `"Beach Party Balloons" <${process.env.SMTP_USER}>`, replyTo: `"${name}" <${email}>`, to: process.env.CONTACT_TO, subject: \`š New inquiry from \${name}\${eventDateFormatted ? \` ā \${eventDateFormatted}\` : ''}\`, text: `Name: ${name}\nEmail: ${email}\nPhone: ${phone}\n${eventText}\n\nMessage:\n${message}`, html: `Name: ${name}
Email: ${email}
Phone: ${phone}
${eventLines}Message:
${message.replace(/\n/g, '
')}
Hi ${name},
Thanks for reaching out to Beach Party Balloons! We've received your message and will get back to you as soon as possible.
Here's a copy of what you sent:
${eventLines}Message:
${message.replace(/\n/g, '
')}
Beach Party Balloons
554 Boston Post Road, Milford, CT 06460
203.283.5626