const express = require('express');
const path = require('path');
const { Pool } = require('pg');
const nodemailer = require('nodemailer');
const app = express();
const PORT = process.env.PORT || 3000;
// ── PostgreSQL Lead Capture ───────────────────────────────────────────
let pool = null;
async function initDb() {
if (!process.env.DATABASE_URL) {
console.log('[leads] DATABASE_URL not set — lead capture disabled (add Railway PostgreSQL plugin)');
return;
}
try {
pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: { rejectUnauthorized: false } });
await pool.query(`
CREATE TABLE IF NOT EXISTS leads (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
install_count INT DEFAULT 1
)
`);
console.log('[leads] PostgreSQL connected — lead capture active');
} catch (err) {
console.error('[leads] DB init failed:', err.message);
pool = null;
}
}
// ── Email Transport ───────────────────────────────────────────────────
// Set SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS in Railway env vars.
// Works with any SMTP provider (Gmail, Postmark, Resend, SendGrid, etc.)
function createMailer() {
if (!process.env.SMTP_HOST) {
console.log('[mail] SMTP_HOST not set — contact form emails disabled');
return null;
}
return nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || '587', 10),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
}
const mailer = createMailer();
// ── Middleware ────────────────────────────────────────────────────────
app.use(express.json());
// ── API Routes ────────────────────────────────────────────────────────
app.post('/api/subscribe', async (req, res) => {
const { email } = req.body || {};
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email || !emailRegex.test(email)) {
return res.status(400).json({ ok: false, error: 'Invalid email' });
}
console.log(`[LEAD] ${email} ${new Date().toISOString()}`);
if (pool) {
try {
await pool.query(
`INSERT INTO leads (email)
VALUES ($1)
ON CONFLICT (email) DO UPDATE SET install_count = leads.install_count + 1`,
[email]
);
} catch (err) {
console.error('[leads] Insert failed:', err.message);
// Don't surface DB errors to the client
}
}
// Always return ok — the reveal should never be blocked
res.json({ ok: true });
});
// ── Contact Form ──────────────────────────────────────────────────────
app.post('/api/contact', async (req, res) => {
const { name, email, subject, message } = req.body || {};
if (!name || !email || !subject || !message) {
return res.status(400).json({ ok: false, error: 'All fields are required' });
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({ ok: false, error: 'Invalid email' });
}
console.log(`[CONTACT] From: ${name} <${email}> | Subject: ${subject}`);
if (mailer) {
try {
await mailer.sendMail({
from: `"Ember MCP Website" <${process.env.SMTP_USER}>`,
to: 'hello@timolabs.dev',
replyTo: `"${name}" <${email}>`,
subject: `[Ember MCP Enquiry] ${subject}`,
text: `Name: ${name}\nEmail: ${email}\nSubject: ${subject}\n\n${message}`,
html: `
<div style="font-family:sans-serif;max-width:600px;margin:0 auto;padding:24px">
<h2 style="color:#8b5cf6;margin-bottom:4px">New Enquiry via Ember MCP Website</h2>
<hr style="border:none;border-top:1px solid #e5e7eb;margin:16px 0"/>
<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p>
<p><strong>Subject:</strong> ${subject}</p>
<hr style="border:none;border-top:1px solid #e5e7eb;margin:16px 0"/>
<p style="white-space:pre-wrap;color:#374151">${message}</p>
<hr style="border:none;border-top:1px solid #e5e7eb;margin:16px 0"/>
<p style="color:#9ca3af;font-size:12px">Sent from ember.timolabs.dev contact form</p>
</div>
`,
});
console.log(`[CONTACT] Email sent for ${email}`);
} catch (err) {
console.error('[CONTACT] Email send failed:', err.message);
return res.status(500).json({ ok: false, error: 'Failed to send email' });
}
} else {
// No mailer configured — log and return ok so form still works in dev
console.log(`[CONTACT] (no mailer) Message from ${name}: ${message}`);
}
res.json({ ok: true });
});
// ── Static Files ──────────────────────────────────────────────────────
app.use(express.static(path.join(__dirname), {
maxAge: '1h',
setHeaders: (res, filePath) => {
if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache');
}
}
}));
// Fallback to index.html for SPA-like behaviour
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
// ── Start ─────────────────────────────────────────────────────────────
initDb().then(() => {
app.listen(PORT, () => {
console.log(`Ember MCP website running on port ${PORT}`);
});
});