189 lines
4.9 KiB
JavaScript
189 lines
4.9 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import express from 'express';
|
|
import nodemailer from 'nodemailer';
|
|
|
|
const DEFAULT_APP_PORT = 8080;
|
|
const DEFAULT_API_ONLY_PORT = 3013;
|
|
const DEFAULT_CONTACT_EMAIL = 'support@bayarea-cc.com';
|
|
const SMTP_HOST = 'email-smtp.us-east-2.amazonaws.com';
|
|
const SMTP_PORT = 587;
|
|
const REQUIRED_FIELDS = ['name', 'email', 'message'];
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const distDir = path.join(__dirname, 'dist');
|
|
|
|
function getFlag(name) {
|
|
return process.argv.includes(name);
|
|
}
|
|
|
|
function getOption(name) {
|
|
const entry = process.argv.find((arg) => arg.startsWith(`${name}=`));
|
|
return entry ? entry.slice(name.length + 1) : undefined;
|
|
}
|
|
|
|
function getPort(apiOnly) {
|
|
const portValue = getOption('--port') || process.env.PORT || String(apiOnly ? DEFAULT_API_ONLY_PORT : DEFAULT_APP_PORT);
|
|
const port = Number.parseInt(portValue, 10);
|
|
|
|
if (Number.isNaN(port)) {
|
|
throw new Error(`Invalid port value: ${portValue}`);
|
|
}
|
|
|
|
return port;
|
|
}
|
|
|
|
function trimField(value) {
|
|
return typeof value === 'string' ? value.trim() : '';
|
|
}
|
|
|
|
function sanitizeHeaderValue(value) {
|
|
return value.replace(/[\r\n]+/g, ' ').trim();
|
|
}
|
|
|
|
function validatePayload(payload) {
|
|
if (!payload || typeof payload !== 'object') {
|
|
return { ok: false, error: 'Invalid request payload.' };
|
|
}
|
|
|
|
const data = {
|
|
name: trimField(payload.name).slice(0, 120),
|
|
email: trimField(payload.email).slice(0, 160),
|
|
phone: trimField(payload.phone).slice(0, 80),
|
|
company: trimField(payload.company).slice(0, 120),
|
|
message: trimField(payload.message).slice(0, 4000),
|
|
website: trimField(payload.website).slice(0, 160),
|
|
};
|
|
|
|
for (const field of REQUIRED_FIELDS) {
|
|
if (!data[field]) {
|
|
return { ok: false, error: 'Please fill out all required fields.' };
|
|
}
|
|
}
|
|
|
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
|
|
return { ok: false, error: 'Please provide a valid email address.' };
|
|
}
|
|
|
|
return { ok: true, data };
|
|
}
|
|
|
|
function createTransporter() {
|
|
const user = process.env.AMAZON_USER;
|
|
const pass = process.env.AMAZON_PASSWORD;
|
|
|
|
if (!user || !pass) {
|
|
return null;
|
|
}
|
|
|
|
return nodemailer.createTransport({
|
|
host: SMTP_HOST,
|
|
secure: false,
|
|
port: SMTP_PORT,
|
|
auth: {
|
|
user,
|
|
pass,
|
|
},
|
|
});
|
|
}
|
|
|
|
function buildMessageBody(data) {
|
|
return [
|
|
'New inquiry',
|
|
'',
|
|
`Name: ${data.name}`,
|
|
`Email: ${data.email}`,
|
|
`Phone: ${data.phone || 'Not provided'}`,
|
|
`Company: ${data.company || 'Not provided'}`,
|
|
'',
|
|
'Message:',
|
|
data.message,
|
|
].join('\n');
|
|
}
|
|
|
|
function resolveRouteIndex(requestPath) {
|
|
const relativePath = requestPath.replace(/^\/+/, '');
|
|
|
|
if (!relativePath) {
|
|
return path.join(distDir, 'index.html');
|
|
}
|
|
|
|
return path.join(distDir, relativePath, 'index.html');
|
|
}
|
|
|
|
const apiOnly = getFlag('--api-only');
|
|
const port = getPort(apiOnly);
|
|
const contactToEmail = process.env.CONTACT_TO_EMAIL || DEFAULT_CONTACT_EMAIL;
|
|
const contactFromEmail = process.env.CONTACT_FROM_EMAIL || contactToEmail;
|
|
const app = express();
|
|
|
|
app.disable('x-powered-by');
|
|
app.use(express.json({ limit: '32kb' }));
|
|
|
|
app.post('/api/contact', async (req, res) => {
|
|
const validation = validatePayload(req.body);
|
|
|
|
if (!validation.ok) {
|
|
return res.status(400).json({ ok: false, error: validation.error });
|
|
}
|
|
|
|
const { data } = validation;
|
|
|
|
if (data.website) {
|
|
return res.status(400).json({ ok: false, error: 'Invalid submission.' });
|
|
}
|
|
|
|
const transporter = createTransporter();
|
|
|
|
if (!transporter) {
|
|
return res.status(503).json({
|
|
ok: false,
|
|
error: 'Email service is not configured right now. Please email us directly.',
|
|
});
|
|
}
|
|
|
|
try {
|
|
await transporter.sendMail({
|
|
from: contactFromEmail,
|
|
to: contactToEmail,
|
|
replyTo: data.email,
|
|
subject: sanitizeHeaderValue(`Website inquiry from ${data.name}`),
|
|
text: buildMessageBody(data),
|
|
});
|
|
|
|
return res.json({ ok: true });
|
|
} catch (error) {
|
|
console.error('Contact form delivery failed.', error);
|
|
|
|
return res.status(500).json({
|
|
ok: false,
|
|
error: 'Unable to send your message right now. Please call or email us directly.',
|
|
});
|
|
}
|
|
});
|
|
|
|
if (!apiOnly) {
|
|
app.use(express.static(distDir, { index: 'index.html', redirect: false }));
|
|
|
|
app.get(/^(?!\/api\/).*/, (req, res) => {
|
|
const routeIndex = resolveRouteIndex(req.path);
|
|
const fallbackIndex = path.join(distDir, 'index.html');
|
|
|
|
if (fs.existsSync(routeIndex)) {
|
|
return res.sendFile(routeIndex);
|
|
}
|
|
|
|
return res.sendFile(fallbackIndex);
|
|
});
|
|
}
|
|
|
|
app.listen(port, () => {
|
|
if (!process.env.AMAZON_USER || !process.env.AMAZON_PASSWORD) {
|
|
console.warn('Amazon SES SMTP credentials are missing. /api/contact will return 503 until configured.');
|
|
}
|
|
|
|
console.log(`Bay Area Affiliates server listening on port ${port}${apiOnly ? ' (API only)' : ''}.`);
|
|
});
|