Files
bayarea/server.mjs
2026-06-17 10:30:31 -05:00

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)' : ''}.`);
});