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
+110 -28
View File
@@ -1,20 +1,29 @@
import Link from "next/link"
import { ArrowRight, Code2, Lock, Zap, BookOpen } from "lucide-react"
import { Code2, Lock, Zap, BookOpen, Webhook } from "lucide-react"
import { WEBHOOK_EVENTS } from "@/lib/webhooks/events"
export const metadata = {
title: "API Docs — Property Management Network",
title: "API Docs",
description: "Property Management Network REST API documentation for developers.",
alternates: { canonical: "/api-docs" },
}
// The real, deployed origin. Falls back to a placeholder only when the env var
// isn't set (e.g. local docs previews).
const BASE_URL = `${process.env.NEXT_PUBLIC_APP_URL ?? "https://your-app-url"}/api/v1`
const ENDPOINTS = [
{ method: "GET", path: "/api/properties", desc: "List all properties for the authenticated landlord" },
{ method: "POST", path: "/api/properties", desc: "Create a new property" },
{ method: "GET", path: "/api/tenants", desc: "List all tenants with lease status" },
{ method: "GET", path: "/api/payments", desc: "List rent payments with filters (status, date range)" },
{ method: "POST", path: "/api/payments", desc: "Record a new payment manually" },
{ method: "GET", path: "/api/maintenance", desc: "List all maintenance requests" },
{ method: "POST", path: "/api/maintenance", desc: "Create a maintenance request" },
{ method: "PATCH", path: "/api/maintenance/:id", desc: "Update request status or assign to contractor" },
{ method: "GET", path: "/properties", desc: "List all properties for the authenticated account" },
{ method: "POST", path: "/properties", desc: "Create a new property" },
{ method: "GET", path: "/tenants", desc: "List tenants with their unit and lease status" },
{ method: "GET", path: "/payments", desc: "List rent payments (filter by status, tenant_id, from/to date range)" },
{ method: "POST", path: "/payments", desc: "Record a rent payment" },
{ method: "GET", path: "/maintenance", desc: "List maintenance requests (filter by status, priority, property_id)" },
{ method: "POST", path: "/maintenance", desc: "Create a maintenance request" },
{ method: "PATCH", path: "/maintenance/:id", desc: "Update a maintenance request's status or fields" },
{ method: "GET", path: "/webhooks", desc: "List webhook subscriptions" },
{ method: "POST", path: "/webhooks", desc: "Create a webhook subscription (Zapier REST Hook subscribe)" },
{ method: "DELETE", path: "/webhooks/:id", desc: "Delete a webhook subscription (Zapier REST Hook unsubscribe)" },
]
const METHOD_COLORS: Record<string, string> = {
@@ -41,7 +50,7 @@ export default function ApiDocsPage() {
</p>
<div className="mt-6 inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.04] px-4 py-2.5">
<span className="text-xs font-mono text-white/40">Base URL:</span>
<code className="text-xs font-mono text-indigo-300">https://api.propertymanagement.network/v1</code>
<code className="text-xs font-mono text-indigo-300">{BASE_URL}</code>
</div>
</div>
</div>
@@ -54,13 +63,15 @@ export default function ApiDocsPage() {
</h2>
<div className="rounded-2xl border border-white/[0.06] bg-[#111118] p-6">
<p className="text-sm text-white/60 mb-4 leading-relaxed">
All API requests require a Bearer token in the Authorization header. Generate your API key from
the <Link href="/login" className="text-indigo-400 hover:text-indigo-300 underline underline-offset-2">dashboard settings</Link>.
All API requests require a Bearer API key in the Authorization header. Keys look like{" "}
<code className="text-indigo-300 font-mono text-xs">pmn_live_</code> and are generated from{" "}
<Link href="/login" className="text-indigo-400 hover:text-indigo-300 underline underline-offset-2">Settings API keys</Link>{" "}
inside your dashboard. The plaintext key is shown only once at creation, so store it securely.
</p>
<div className="rounded-xl bg-[#0a0a12] border border-white/[0.06] p-4 font-mono text-xs text-emerald-300">
<p className="text-white/30 mb-1"># Example request</p>
<p>curl https://api.propertymanagement.network/v1/properties \</p>
<p className="pl-4">-H &quot;Authorization: Bearer YOUR_API_KEY&quot;</p>
<p>curl {BASE_URL}/properties \</p>
<p className="pl-4">-H &quot;Authorization: Bearer pmn_live_...&quot;</p>
</div>
</div>
</div>
@@ -91,27 +102,98 @@ export default function ApiDocsPage() {
<BookOpen className="h-5 w-5 text-blue-400" /> Response Format
</h2>
<div className="rounded-2xl border border-white/[0.06] bg-[#111118] p-6">
<p className="text-sm text-white/60 mb-4">All responses are JSON. Successful responses return a <code className="text-indigo-300 font-mono text-xs">data</code> field. Errors return an <code className="text-red-300 font-mono text-xs">error</code> field with a message and code.</p>
<p className="text-sm text-white/60 mb-4">
All responses are JSON. List endpoints return a <code className="text-indigo-300 font-mono text-xs">data</code> array
with a <code className="text-indigo-300 font-mono text-xs">count</code>. Single-record and create responses return a{" "}
<code className="text-indigo-300 font-mono text-xs">data</code> object (create returns HTTP 201). Errors return an{" "}
<code className="text-red-300 font-mono text-xs">error</code> object with a numeric <code className="text-red-300 font-mono text-xs">code</code> and a <code className="text-red-300 font-mono text-xs">message</code>.
</p>
<div className="rounded-xl bg-[#0a0a12] border border-white/[0.06] p-4 font-mono text-xs leading-relaxed">
<p className="text-white/30">// Success</p>
<p className="text-white/30">{"// Success (list)"}</p>
<p className="text-emerald-300">{"{"} &quot;data&quot;: [...], &quot;count&quot;: 12 {"}"}</p>
<br />
<p className="text-white/30">// Error</p>
<p className="text-white/30">{"// Success (single / create)"}</p>
<p className="text-emerald-300">{"{"} &quot;data&quot;: {"{"} ... {"}"} {"}"}</p>
<br />
<p className="text-white/30">{"// Error"}</p>
<p className="text-red-300">{"{"} &quot;error&quot;: {"{"} &quot;code&quot;: 401, &quot;message&quot;: &quot;Unauthorized&quot; {"}"} {"}"}</p>
</div>
</div>
</div>
{/* Coming soon banner */}
<div className="rounded-2xl border border-indigo-500/20 bg-indigo-500/5 p-6 text-center">
<p className="text-sm font-semibold text-indigo-300 mb-1">Full SDK coming soon</p>
<p className="text-xs text-white/40 mb-4">We&apos;re building official JavaScript and Python SDKs. Join the waitlist to be notified.</p>
<Link
href="/signup"
className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-5 py-2 text-xs font-semibold text-white hover:bg-indigo-500 transition"
>
Join waitlist <ArrowRight className="h-3.5 w-3.5" />
</Link>
{/* Webhooks */}
<div id="webhooks" className="scroll-mt-24">
<h2 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
<Webhook className="h-5 w-5 text-emerald-400" /> Webhooks
</h2>
<div className="space-y-4">
<div className="rounded-2xl border border-white/[0.06] bg-[#111118] p-6">
<p className="text-sm text-white/60 leading-relaxed">
Subscribe to real-time events instead of polling. Add endpoints in{" "}
<Link href="/login" className="text-indigo-400 hover:text-indigo-300 underline underline-offset-2">Settings Webhooks</Link>{" "}
(or via the <code className="text-indigo-300 font-mono text-xs">/webhooks</code> API), and we&apos;ll POST a
signed JSON payload the moment something happens. This is the same mechanism that powers our{" "}
<span className="text-white/80 font-medium">Zapier</span> integration Zapier subscribes and unsubscribes
through the <code className="text-indigo-300 font-mono text-xs">POST /webhooks</code> and{" "}
<code className="text-indigo-300 font-mono text-xs">DELETE /webhooks/:id</code> endpoints (the REST Hook pattern).
</p>
</div>
{/* Events */}
<div className="rounded-2xl border border-white/[0.06] bg-[#111118] overflow-hidden">
<div className="border-b border-white/[0.06] px-6 py-3">
<p className="text-xs font-semibold uppercase tracking-widest text-white/40">Available events</p>
</div>
{WEBHOOK_EVENTS.map((ev, i) => (
<div
key={ev.id}
className={`flex items-start gap-4 px-6 py-3 ${i !== WEBHOOK_EVENTS.length - 1 ? "border-b border-white/[0.04]" : ""}`}
>
<code className="shrink-0 rounded-md bg-emerald-500/10 px-2.5 py-1 text-[11px] font-bold font-mono text-emerald-400">
{ev.id}
</code>
<p className="text-xs text-white/50 mt-0.5">{ev.description}</p>
</div>
))}
</div>
{/* Payload + signature */}
<div className="rounded-2xl border border-white/[0.06] bg-[#111118] p-6">
<p className="text-sm font-semibold text-white mb-2">Payload &amp; signature</p>
<p className="text-sm text-white/60 mb-4 leading-relaxed">
Each request body is a JSON envelope. Every delivery carries an{" "}
<code className="text-indigo-300 font-mono text-xs">X-PMN-Signature</code> header {" "}
<code className="text-indigo-300 font-mono text-xs">t=&lt;unix&gt;,v1=&lt;hex&gt;</code> where{" "}
<code className="text-indigo-300 font-mono text-xs">v1</code> is the HMAC-SHA256 of{" "}
<code className="text-indigo-300 font-mono text-xs">{"`${t}.${rawBody}`"}</code> keyed with your endpoint&apos;s
signing secret. Recompute it and compare in constant time; reject if the timestamp is stale.
</p>
<div className="rounded-xl bg-[#0a0a12] border border-white/[0.06] p-4 font-mono text-xs leading-relaxed text-white/70">
<p className="text-white/30">{"// POST body"}</p>
<p className="text-emerald-300">{"{"}</p>
<p className="pl-4">&quot;id&quot;: &quot;evt_9f2c&quot;,</p>
<p className="pl-4">&quot;event&quot;: &quot;tenant.created&quot;,</p>
<p className="pl-4">&quot;created_at&quot;: &quot;2026-07-02T12:00:00.000Z&quot;,</p>
<p className="pl-4">&quot;data&quot;: {"{"} &quot;tenant&quot;: {"{"} {"}"} {"}"}</p>
<p className="text-emerald-300">{"}"}</p>
<p className="mt-2 text-white/30">{"# Headers"}</p>
<p>X-PMN-Event: tenant.created</p>
<p>X-PMN-Delivery: &lt;delivery id&gt;</p>
<p>X-PMN-Signature: t=1751457600,v1=1a2b3c</p>
</div>
<p className="text-xs text-white/40 mt-4">
Respond with any <span className="text-white/70">2xx</span> to acknowledge. Non-2xx or timeouts are retried
with exponential backoff (up to 5 attempts); a test event is available from the dashboard.
</p>
</div>
</div>
</div>
{/* SDK note */}
<div className="rounded-2xl border border-white/[0.06] bg-white/[0.02] p-6 text-center">
<p className="text-xs text-white/40">
Official client SDKs are not yet available. Call the endpoints directly over HTTP with any language or HTTP client.
</p>
</div>
</div>
</div>