Files
houses-flowautomate/server.js
Claude 85e7dc8e0c Initial commit: houses.flowautomate.ai lead generation website
Static landing page with Express backend for lead capture.
Navy/Gold branding, mobile-responsive, SQLite storage,
Mailcow SMTP notifications, rate limiting, TCPA compliance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 04:46:20 +01:00

215 lines
8.8 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'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:"],
fontSrc: ["'self'"],
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:#1B3A5C;">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:#1B3A5C;">Name</td><td style="padding:6px 12px;">${escapeHtml(name)}</td></tr>
<tr style="background:#F8F9FA;"><td style="padding:6px 12px;font-weight:bold;color:#1B3A5C;">Phone</td><td style="padding:6px 12px;">${escapeHtml(phone)}</td></tr>
<tr><td style="padding:6px 12px;font-weight:bold;color:#1B3A5C;">Email</td><td style="padding:6px 12px;">${escapeHtml(email)}</td></tr>
<tr style="background:#F8F9FA;"><td style="padding:6px 12px;font-weight:bold;color:#1B3A5C;">Property</td><td style="padding:6px 12px;">${escapeHtml(address)}</td></tr>
<tr><td style="padding:6px 12px;font-weight:bold;color:#1B3A5C;">Condition</td><td style="padding:6px 12px;">${escapeHtml(condition || 'Not specified')}</td></tr>
<tr style="background:#F8F9FA;"><td style="padding:6px 12px;font-weight:bold;color:#1B3A5C;">Timeline</td><td style="padding:6px 12px;">${escapeHtml(timeline || 'Not specified')}</td></tr>
<tr><td style="padding:6px 12px;font-weight:bold;color:#1B3A5C;">TCPA Consent</td><td style="padding:6px 12px;">Yes</td></tr>
<tr style="background:#F8F9FA;"><td style="padding:6px 12px;font-weight:bold;color:#1B3A5C;">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:#1B3A5C;padding:24px;text-align:center;">
<h1 style="color:#C9A94E;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:#333;">
<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:#C9A94E;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 #C9A94E;padding:20px 24px;background:#F8F9FA;">
<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:#1B3A5C;">Jociah</p>
<p style="margin:0 0 4px;font-size:10px;font-weight:600;color:#C9A94E;text-transform:uppercase;letter-spacing:2px;">Founder &amp; CEO</p>
<p style="margin:0 0 2px;font-size:13px;color:#1B3A5C;font-weight:600;">Flowautomate LLC</p>
<p style="margin:0;font-size:11px;color:#888;">
<a href="tel:+14256107779" style="color:#1B3A5C;text-decoration:none;">(425) 610-7779</a> &bull;
<a href="mailto:offers@flowautomate.ai" style="color:#1B3A5C;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 &bull; No repairs needed &bull; 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// 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}`);
});