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>
This commit is contained in:
214
server.js
Normal file
214
server.js
Normal file
@@ -0,0 +1,214 @@
|
||||
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 & 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> •
|
||||
<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 • 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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user