Consolidate audit-fixes branch: webhooks, integrations, and deploy hardening

Batch commit of the pending working tree on security/audit-fixes-2026-07.
Major areas:
- Outbound webhooks / Zapier: schema + signed delivery with retries, public
  v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain.
- Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS
  (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when
  Spaces is unconfigured instead of silently using ephemeral disk.
- Integrations & features (concurrent work): accounting (QuickBooks/Xero),
  e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding,
  expanded legal pages.
- DB migrations 0006–0009.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leon Serfaty
2026-07-02 13:42:34 -04:00
co-authored by Claude Opus 4.8
parent 969d5d4c8a
commit c9968531e4
282 changed files with 41530 additions and 4013 deletions
+102
View File
@@ -0,0 +1,102 @@
// One-time PayPal setup: creates the product + the recurring billing plans and
// prints the plan IDs to paste into your environment.
//
// 1. Set PAYPAL_CLIENT_ID / PAYPAL_SECRET (and PAYPAL_ENVIRONMENT) in .env.local
// 2. node scripts/paypal-setup-plans.mjs
// 3. Copy the printed PAYPAL_*_PLAN_ID lines into .env.local / production env
//
// Amounts mirror the app's pricing (Pro $29/mo, Landlord $59/mo); yearly is
// billed at 10× monthly (~2 months free). Adjust in the PayPal dashboard if you
// want different annual pricing. Safe to re-run (it creates fresh plans).
import { config } from "dotenv"
config({ path: ".env.local", quiet: true })
const ENV = process.env.PAYPAL_ENVIRONMENT === "live" ? "live" : "sandbox"
const BASE = ENV === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
const id = process.env.PAYPAL_CLIENT_ID
const secret = process.env.PAYPAL_SECRET
if (!id || !secret) {
console.error("[paypal-setup] Set PAYPAL_CLIENT_ID and PAYPAL_SECRET in .env.local first.")
process.exit(1)
}
async function getToken() {
const r = await fetch(`${BASE}/v1/oauth2/token`, {
method: "POST",
headers: {
Authorization: `Basic ${Buffer.from(`${id}:${secret}`).toString("base64")}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: "grant_type=client_credentials",
})
if (!r.ok) throw new Error(`auth ${r.status}: ${await r.text()}`)
return (await r.json()).access_token
}
const token = await getToken()
const post = (path, body) =>
fetch(`${BASE}${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
})
console.log(`[paypal-setup] Environment: ${ENV}`)
const prodRes = await post("/v1/catalogs/products", {
name: "Property Management Network",
description: "Property Management Network subscription",
type: "SERVICE",
category: "SOFTWARE",
})
if (!prodRes.ok) {
console.error("[paypal-setup] product creation failed:", await prodRes.text())
process.exit(1)
}
const product = await prodRes.json()
console.log(`[paypal-setup] Product: ${product.id}`)
const AMOUNTS = { pro: 29, landlord: 59 }
const envLines = []
for (const plan of ["pro", "landlord"]) {
for (const interval of ["month", "year"]) {
const amount = interval === "month" ? AMOUNTS[plan] : AMOUNTS[plan] * 10
const res = await post("/v1/billing/plans", {
product_id: product.id,
name: `${plan[0].toUpperCase()}${plan.slice(1)} ${interval === "month" ? "Monthly" : "Yearly"}`,
status: "ACTIVE",
billing_cycles: [
{
frequency: { interval_unit: interval === "month" ? "MONTH" : "YEAR", interval_count: 1 },
tenure_type: "REGULAR",
sequence: 1,
total_cycles: 0,
pricing_scheme: { fixed_price: { value: amount.toFixed(2), currency_code: "USD" } },
},
],
payment_preferences: {
auto_bill_outstanding: true,
setup_fee_failure_action: "CONTINUE",
payment_failure_threshold: 2,
},
})
if (!res.ok) {
console.error(`[paypal-setup] plan ${plan}/${interval} failed:`, await res.text())
continue
}
const p = await res.json()
const key = `PAYPAL_${plan.toUpperCase()}_${interval === "month" ? "MONTHLY" : "YEARLY"}_PLAN_ID`
console.log(` ${plan}/${interval} $${amount}${p.id}`)
envLines.push(`${key}=${p.id}`)
}
}
console.log("\n[paypal-setup] Add these to your environment:\n")
console.log(envLines.join("\n"))