Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
142
api/src/routes/subscribe.ts
Normal file
142
api/src/routes/subscribe.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { Router } from "express";
|
||||
import { pool } from "../db.js";
|
||||
import { submitLimiter } from "../middleware/rate-limit.js";
|
||||
import { createLead } from "../erpnext-client.js";
|
||||
// Listmonk subscriber push (non-blocking)
|
||||
const LISTMONK_URL = process.env.LISTMONK_URL || "http://listmonk:9000";
|
||||
const LISTMONK_USER = process.env.LISTMONK_USER || "api";
|
||||
const LISTMONK_PASS = process.env.LISTMONK_PASSWORD || "";
|
||||
|
||||
async function addToListmonk(email: string, name: string, company?: string) {
|
||||
const resp = await fetch(`${LISTMONK_URL}/api/subscribers`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Basic ${Buffer.from(`${LISTMONK_USER}:${LISTMONK_PASS}`).toString("base64")}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
name: name || email.split("@")[0],
|
||||
status: "enabled",
|
||||
lists: [3], // FCC Carriers - Direct Contacts
|
||||
preconfirm_subscriptions: true,
|
||||
attribs: { company: company || "", source: "website" },
|
||||
}),
|
||||
});
|
||||
if (!resp.ok && resp.status !== 409) {
|
||||
throw new Error(`Listmonk ${resp.status}: ${await resp.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
const router = Router();
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
const DISPOSABLE_DOMAINS = new Set([
|
||||
"mailinator.com", "guerrillamail.com", "tempmail.com", "throwaway.email",
|
||||
"yopmail.com", "sharklasers.com", "guerrillamailblock.com", "grr.la",
|
||||
"dispostable.com", "trashmail.com", "fakeinbox.com", "temp-mail.org",
|
||||
]);
|
||||
|
||||
const CONSENT_TEXT =
|
||||
"I agree to receive compliance updates and service announcements from Performance West Inc. I can unsubscribe at any time.";
|
||||
|
||||
// POST /api/v1/subscribe
|
||||
router.post("/api/v1/subscribe", submitLimiter, async (req, res) => {
|
||||
try {
|
||||
const { email, name, company, consent, _hp, _ts } = req.body ?? {};
|
||||
|
||||
// Honeypot — bots fill hidden fields
|
||||
if (_hp) {
|
||||
res.status(201).json({ success: true, message: "Subscribed." });
|
||||
return;
|
||||
}
|
||||
|
||||
// Timing check — reject if form submitted in < 2 seconds
|
||||
if (_ts && typeof _ts === "number" && Date.now() - _ts < 2_000) {
|
||||
res.status(201).json({ success: true, message: "Subscribed." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email || typeof email !== "string" || !EMAIL_RE.test(email)) {
|
||||
res.status(400).json({ error: "Valid email address is required." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!consent) {
|
||||
res.status(400).json({ error: "Consent is required to subscribe." });
|
||||
return;
|
||||
}
|
||||
|
||||
// Block disposable email domains
|
||||
const domain = email.split("@")[1]?.toLowerCase();
|
||||
if (domain && DISPOSABLE_DOMAINS.has(domain)) {
|
||||
res.status(400).json({ error: "Please use a permanent email address." });
|
||||
return;
|
||||
}
|
||||
|
||||
const ip = (req as any).clientIp || req.ip || "";
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO subscribers (email, name, company, consent_text, consent_at, ip_address, source)
|
||||
VALUES ($1, $2, $3, $4, NOW(), $5, 'website')
|
||||
ON CONFLICT (email) DO UPDATE SET
|
||||
unsubscribed = FALSE,
|
||||
consent_text = $4,
|
||||
consent_at = NOW(),
|
||||
ip_address = $5`,
|
||||
[email.toLowerCase().trim(), name || null, company || null, CONSENT_TEXT, ip],
|
||||
);
|
||||
|
||||
// Push to ERPNext (Lead) and Listmonk — non-blocking, don't fail the response
|
||||
const cleanEmail = email.toLowerCase().trim();
|
||||
const [firstName, ...lastParts] = (name || "").trim().split(/\s+/);
|
||||
const lastName = lastParts.join(" ") || "";
|
||||
|
||||
try {
|
||||
await createLead({
|
||||
name: (name || cleanEmail).trim(),
|
||||
email: cleanEmail,
|
||||
company: company || undefined,
|
||||
source: "Website",
|
||||
notes: `Subscribed via website. Consent: "${CONSENT_TEXT}"`,
|
||||
});
|
||||
} catch (erpErr) {
|
||||
console.error("[subscribe] ERPNext createLead failed (non-fatal):", erpErr);
|
||||
}
|
||||
|
||||
try {
|
||||
await addToListmonk(cleanEmail, name || cleanEmail, company || undefined);
|
||||
} catch (listmonkErr) {
|
||||
console.error("[subscribe] Listmonk addToListmonk failed (non-fatal):", listmonkErr);
|
||||
}
|
||||
|
||||
res.status(201).json({ success: true, message: "You're on the list." });
|
||||
} catch (err) {
|
||||
console.error("[subscribe] Error:", err);
|
||||
res.status(500).json({ error: "Subscription failed. Please try again." });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/v1/unsubscribe
|
||||
router.post("/api/v1/unsubscribe", async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body ?? {};
|
||||
if (!email || typeof email !== "string") {
|
||||
res.status(400).json({ error: "Email is required." });
|
||||
return;
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
"UPDATE subscribers SET unsubscribed = TRUE WHERE email = $1",
|
||||
[email.toLowerCase().trim()],
|
||||
);
|
||||
|
||||
res.status(200).json({ success: true, message: "You have been unsubscribed." });
|
||||
} catch (err) {
|
||||
console.error("[unsubscribe] Error:", err);
|
||||
res.status(500).json({ error: "Unsubscribe failed. Please try again." });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Loading…
Add table
Add a link
Reference in a new issue