Replace Navy (#1B3A5C) / Gold (#C9A94E) with flowautomate.ai's red (#FF1F1F) / dark neutral (#171717, #262626) theme. Add Inter + Poppins Google Fonts, update logo, email templates, and CSP headers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
215 lines
8.9 KiB
JavaScript
215 lines
8.9 KiB
JavaScript
const express = require('express');
|
|
const path = require('path');
|
|
const Database = require('better-sqlite3');
|
|
const nodemailer = require('nodemailer');
|
|
const rateLimit = require('express-rate-limit');
|
|
const helmet = require('helmet');
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
// Database setup
|
|
const dbPath = path.join(__dirname, 'data', 'leads.db');
|
|
const db = new Database(dbPath);
|
|
db.pragma('journal_mode = WAL');
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS leads (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
email TEXT NOT NULL,
|
|
phone TEXT NOT NULL,
|
|
address TEXT NOT NULL,
|
|
property_condition TEXT,
|
|
timeline TEXT,
|
|
tcpa_consent INTEGER NOT NULL DEFAULT 0,
|
|
ip TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
)
|
|
`);
|
|
|
|
// SMTP transporter
|
|
const transporter = nodemailer.createTransport({
|
|
host: process.env.SMTP_HOST || 'mail.flowautomate.ai',
|
|
port: parseInt(process.env.SMTP_PORT || '587'),
|
|
secure: false,
|
|
auth: {
|
|
user: process.env.SMTP_USER || 'offers@flowautomate.ai',
|
|
pass: process.env.SMTP_PASS || '',
|
|
},
|
|
tls: { rejectUnauthorized: false },
|
|
});
|
|
|
|
// Middleware
|
|
app.use(helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
|
scriptSrc: ["'self'"],
|
|
imgSrc: ["'self'", "data:"],
|
|
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
|
connectSrc: ["'self'"],
|
|
},
|
|
},
|
|
}));
|
|
app.use(express.json());
|
|
app.use(express.urlencoded({ extended: false }));
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
// Rate limit for form submissions
|
|
const submitLimiter = rateLimit({
|
|
windowMs: 60 * 60 * 1000, // 1 hour
|
|
max: 5,
|
|
message: { success: false, error: 'Too many submissions. Please try again later.' },
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
keyGenerator: (req) => req.ip,
|
|
});
|
|
|
|
// Health check
|
|
app.get('/api/health', (req, res) => {
|
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
// Lead submission
|
|
app.post('/api/submit-lead', submitLimiter, async (req, res) => {
|
|
try {
|
|
const { name, email, phone, address, condition, timeline, tcpa_consent, honeypot } = req.body;
|
|
|
|
// Honeypot check
|
|
if (honeypot) {
|
|
return res.json({ success: true });
|
|
}
|
|
|
|
// Validate required fields
|
|
if (!name || !email || !phone || !address) {
|
|
return res.status(400).json({ success: false, error: 'All fields are required.' });
|
|
}
|
|
|
|
// Validate email format
|
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
return res.status(400).json({ success: false, error: 'Please enter a valid email address.' });
|
|
}
|
|
|
|
// Validate phone (digits, dashes, parens, spaces, plus)
|
|
const cleanPhone = phone.replace(/[\s\-\(\)\+]/g, '');
|
|
if (!/^\d{7,15}$/.test(cleanPhone)) {
|
|
return res.status(400).json({ success: false, error: 'Please enter a valid phone number.' });
|
|
}
|
|
|
|
// TCPA consent required
|
|
if (!tcpa_consent) {
|
|
return res.status(400).json({ success: false, error: 'Please agree to be contacted.' });
|
|
}
|
|
|
|
// Save to database
|
|
const stmt = db.prepare(`
|
|
INSERT INTO leads (name, email, phone, address, property_condition, timeline, tcpa_consent, ip)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
stmt.run(name, email, phone, address, condition || '', timeline || '', 1, req.ip);
|
|
|
|
// Send notification email to team
|
|
const notifyHtml = `
|
|
<h2 style="color:#171717;font-family:Arial,sans-serif;">New Lead from houses.flowautomate.ai</h2>
|
|
<table style="font-family:Arial,sans-serif;border-collapse:collapse;">
|
|
<tr><td style="padding:6px 12px;font-weight:bold;color:#171717;">Name</td><td style="padding:6px 12px;">${escapeHtml(name)}</td></tr>
|
|
<tr style="background:#f9fafb;"><td style="padding:6px 12px;font-weight:bold;color:#171717;">Phone</td><td style="padding:6px 12px;">${escapeHtml(phone)}</td></tr>
|
|
<tr><td style="padding:6px 12px;font-weight:bold;color:#171717;">Email</td><td style="padding:6px 12px;">${escapeHtml(email)}</td></tr>
|
|
<tr style="background:#f9fafb;"><td style="padding:6px 12px;font-weight:bold;color:#171717;">Property</td><td style="padding:6px 12px;">${escapeHtml(address)}</td></tr>
|
|
<tr><td style="padding:6px 12px;font-weight:bold;color:#171717;">Condition</td><td style="padding:6px 12px;">${escapeHtml(condition || 'Not specified')}</td></tr>
|
|
<tr style="background:#f9fafb;"><td style="padding:6px 12px;font-weight:bold;color:#171717;">Timeline</td><td style="padding:6px 12px;">${escapeHtml(timeline || 'Not specified')}</td></tr>
|
|
<tr><td style="padding:6px 12px;font-weight:bold;color:#171717;">TCPA Consent</td><td style="padding:6px 12px;">Yes</td></tr>
|
|
<tr style="background:#f9fafb;"><td style="padding:6px 12px;font-weight:bold;color:#171717;">Submitted</td><td style="padding:6px 12px;">${new Date().toLocaleString('en-US', { timeZone: 'America/Chicago' })}</td></tr>
|
|
</table>
|
|
`;
|
|
|
|
// Auto-response email to seller
|
|
const autoResponseHtml = `
|
|
<div style="font-family:Arial,Helvetica,sans-serif;max-width:600px;margin:0 auto;">
|
|
<div style="background:#171717;padding:24px;text-align:center;">
|
|
<h1 style="color:#FF1F1F;margin:0;font-size:22px;">Flowautomate LLC</h1>
|
|
<p style="color:#ffffff;margin:6px 0 0;font-size:13px;">AI-Powered Real Estate Solutions</p>
|
|
</div>
|
|
<div style="padding:28px 24px;color:#262626;">
|
|
<p>Hi ${escapeHtml(name.split(' ')[0])},</p>
|
|
<p>Thank you for reaching out about your property at <strong>${escapeHtml(address)}</strong>. We received your information and our team is reviewing it now.</p>
|
|
<p><strong>What happens next:</strong></p>
|
|
<ul style="color:#555;line-height:1.8;">
|
|
<li>We'll review your property details and local market data</li>
|
|
<li>You'll receive a fair, no-obligation cash offer within 24 hours</li>
|
|
<li>If the offer works for you, we can close on your timeline</li>
|
|
</ul>
|
|
<p>If you have any questions in the meantime, feel free to call or text us:</p>
|
|
<p style="text-align:center;margin:20px 0;">
|
|
<a href="tel:+14256107779" style="display:inline-block;background:#FF1F1F;color:#fff;padding:12px 28px;text-decoration:none;border-radius:6px;font-weight:700;font-size:16px;">(425) 610-7779</a>
|
|
</p>
|
|
</div>
|
|
<div style="border-top:2px solid #FF1F1F;padding:20px 24px;background:#f9fafb;">
|
|
<table cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;">
|
|
<tr>
|
|
<td style="vertical-align:top;">
|
|
<p style="margin:0 0 2px;font-size:16px;font-weight:700;color:#171717;">Jociah</p>
|
|
<p style="margin:0 0 4px;font-size:10px;font-weight:600;color:#FF1F1F;text-transform:uppercase;letter-spacing:2px;">Founder & CEO</p>
|
|
<p style="margin:0 0 2px;font-size:13px;color:#171717;font-weight:600;">Flowautomate LLC</p>
|
|
<p style="margin:0;font-size:11px;color:#888;">
|
|
<a href="tel:+14256107779" style="color:#171717;text-decoration:none;">(425) 610-7779</a> •
|
|
<a href="mailto:offers@flowautomate.ai" style="color:#171717;text-decoration:none;">offers@flowautomate.ai</a>
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<div style="padding:12px 24px;text-align:center;">
|
|
<p style="font-size:10px;color:#aaa;margin:0;">Fast cash offers • No repairs needed • Close on your timeline</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Send both emails (don't block response on email delivery)
|
|
if (process.env.SMTP_PASS) {
|
|
transporter.sendMail({
|
|
from: '"Flowautomate LLC" <offers@flowautomate.ai>',
|
|
to: 'offers@flowautomate.ai',
|
|
subject: `New Lead: ${address} - ${name}`,
|
|
html: notifyHtml,
|
|
}).catch(err => console.error('Notification email failed:', err.message));
|
|
|
|
transporter.sendMail({
|
|
from: '"Jociah at Flowautomate" <offers@flowautomate.ai>',
|
|
to: email,
|
|
subject: 'We received your info - Flowautomate LLC',
|
|
html: autoResponseHtml,
|
|
}).catch(err => console.error('Auto-response email failed:', err.message));
|
|
}
|
|
|
|
res.json({ success: true });
|
|
} catch (err) {
|
|
console.error('Lead submission error:', err);
|
|
res.status(500).json({ success: false, error: 'Something went wrong. Please try again.' });
|
|
}
|
|
});
|
|
|
|
function escapeHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
// SPA fallback — serve index.html for unknown routes
|
|
app.get('*', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
});
|
|
|
|
// Graceful shutdown
|
|
process.on('SIGTERM', () => {
|
|
db.close();
|
|
process.exit(0);
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`houses.flowautomate.ai running on port ${PORT}`);
|
|
});
|