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:
co-authored by
Claude Opus 4.8
parent
969d5d4c8a
commit
c9968531e4
@@ -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"))
|
||||
Reference in New Issue
Block a user