Deploy on DigitalOcean App Platform (GitHub-source build) + consolidate audit-fixes
Deploy config: - .do/app.yaml: build the Dockerfile directly from GitHub (deploy_on_push) instead of a pre-built DOCR image; NEXT_PUBLIC_* set RUN_AND_BUILD_TIME with the propertymanagement.network domain so they bake into the client bundle; add custom domains block (apex + www); wire Sentry DSN (server + browser). Included pending work from the audit-fixes branch: - AI provider abstraction (OpenAI/Anthropic, admin-selectable; Anthropic default) - Per-landlord e-signature (DocuSign OAuth + Dropbox Sign) + migration 0010 - Outbound webhooks / Zapier integration - PayPal removal (Stripe-only billing) - Storage hardening (fail-loud when Spaces unconfigured), security fixes Verified: full production Docker build (same build-args as DO) passes clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
917a06ee85
commit
5495b94924
@@ -0,0 +1,268 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState, useTransition } from "react"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
PenLine,
|
||||
Link2,
|
||||
CheckCircle2,
|
||||
AlertTriangle,
|
||||
KeyRound,
|
||||
ChevronDown,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
} from "lucide-react"
|
||||
import { connectDropboxSign, disconnectEsignAction } from "@/app/actions/esign"
|
||||
|
||||
type Adapter = { id: string; label: string; kind: "oauth" | "apikey"; available: boolean }
|
||||
type Conn = { provider: string; accountName: string | null; status: string; lastError: string | null }
|
||||
|
||||
const ERR_MSG: Record<string, string> = {
|
||||
connect_failed: "Connection failed — please try again.",
|
||||
invalid_state: "The connection link expired or was invalid. Please retry.",
|
||||
owner_only: "Only the account owner can manage integrations.",
|
||||
not_configured: "E-signature isn't enabled on this server yet.",
|
||||
unknown_provider: "Unknown provider.",
|
||||
use_api_key: "That provider connects with an API key, not a redirect.",
|
||||
}
|
||||
|
||||
const LABEL: Record<string, string> = { docusign: "DocuSign", dropbox_sign: "Dropbox Sign" }
|
||||
|
||||
function StatusPill({ status }: { status: string }) {
|
||||
const error = status === "error"
|
||||
return (
|
||||
<span
|
||||
className={`flex shrink-0 items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11px] font-medium ${
|
||||
error ? "border-red-500/20 bg-red-500/10 text-red-400" : "border-emerald-500/20 bg-emerald-500/10 text-emerald-400"
|
||||
}`}
|
||||
>
|
||||
{error ? <AlertTriangle className="h-3 w-3" /> : <CheckCircle2 className="h-3 w-3" />}
|
||||
{error ? "Error" : "Connected"}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function EsignIntegrations({
|
||||
adapters,
|
||||
connections,
|
||||
isOwner,
|
||||
flash,
|
||||
webhookUrl,
|
||||
}: {
|
||||
adapters: Adapter[]
|
||||
connections: Conn[]
|
||||
isOwner: boolean
|
||||
flash: { connected?: string; error?: string }
|
||||
webhookUrl: string
|
||||
}) {
|
||||
const connByProvider: Record<string, Conn> = Object.fromEntries(connections.map((c) => [c.provider, c]))
|
||||
const [pending, start] = useTransition()
|
||||
const [busy, setBusy] = useState<string | null>(null)
|
||||
const [open, setOpen] = useState<string | null>(null) // which provider's instructions are expanded
|
||||
const [apiKey, setApiKey] = useState("")
|
||||
const [showKeyForm, setShowKeyForm] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (flash.connected) toast.success(`Connected to ${LABEL[flash.connected] ?? flash.connected}`)
|
||||
if (flash.error) toast.error(ERR_MSG[flash.error] ?? "Something went wrong")
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
function connectDbx() {
|
||||
const key = apiKey.trim()
|
||||
if (!key) return
|
||||
setBusy("dropbox_sign")
|
||||
start(async () => {
|
||||
try {
|
||||
const r = await connectDropboxSign(key)
|
||||
toast.success(`Connected ${r.accountName ?? "Dropbox Sign"}`)
|
||||
setApiKey("")
|
||||
setShowKeyForm(false)
|
||||
} catch (e) {
|
||||
toast.error((e as Error).message || "Couldn't connect")
|
||||
} finally {
|
||||
setBusy(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function remove(id: string) {
|
||||
if (!confirm(`Disconnect ${LABEL[id] ?? id}? You won't be able to send leases through it until you reconnect.`)) return
|
||||
setBusy(id)
|
||||
start(async () => {
|
||||
try {
|
||||
await disconnectEsignAction(id)
|
||||
toast.success("Disconnected")
|
||||
} catch {
|
||||
toast.error("Couldn't disconnect")
|
||||
} finally {
|
||||
setBusy(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!isOwner) {
|
||||
return (
|
||||
<div className="rounded-xl border border-white/[0.06] bg-[#16161f] p-6 text-sm text-white/50">
|
||||
Only the account owner can connect e-signature providers.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* Intro / how it works */}
|
||||
<div className="rounded-2xl border border-indigo-500/15 bg-indigo-500/[0.04] p-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<PenLine className="h-4 w-4 text-indigo-400" />
|
||||
<h3 className="text-sm font-semibold text-white">Send leases for e-signature</h3>
|
||||
</div>
|
||||
<p className="mt-1.5 text-xs leading-relaxed text-white/50">
|
||||
Connect <span className="text-white/80">your own</span> DocuSign or Dropbox Sign account so signed leases carry
|
||||
your brand and audit trail — and the signing costs stay on your provider plan, not ours. Once connected, a
|
||||
<span className="text-white/80"> “Send for signature” </span> button appears on every lease that has a document
|
||||
and a tenant email.
|
||||
</p>
|
||||
<ol className="mt-3 space-y-1.5 text-xs text-white/50">
|
||||
<li>1. Connect your provider below (one-time).</li>
|
||||
<li>2. Open a lease → upload the lease PDF.</li>
|
||||
<li>3. Click “Send via DocuSign / Dropbox Sign.” The tenant signs; the status updates here automatically and the signed copy is saved back to the lease.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{adapters.map((a) => {
|
||||
const conn = connByProvider[a.id]
|
||||
const isBusy = pending && busy === a.id
|
||||
const instructionsOpen = open === a.id
|
||||
return (
|
||||
<div key={a.id} className="rounded-2xl border border-white/[0.06] bg-[#16161f] p-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-white/[0.04] text-white/70">
|
||||
<PenLine className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">{a.label}</p>
|
||||
{conn ? (
|
||||
<p className="text-xs text-white/40">{conn.accountName ?? "Connected"}</p>
|
||||
) : (
|
||||
<p className="text-xs text-white/40">
|
||||
{a.kind === "oauth" ? "Connect with your DocuSign login" : "Connect with your API key"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{conn ? (
|
||||
<StatusPill status={conn.status} />
|
||||
) : !a.available ? (
|
||||
<span className="shrink-0 rounded-full border border-amber-500/20 bg-amber-500/10 px-2.5 py-1 text-[11px] font-medium text-amber-400">
|
||||
Not available
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{conn?.status === "error" && conn.lastError && (
|
||||
<p className="mt-3 rounded-lg border border-red-500/15 bg-red-500/[0.06] px-3 py-2 text-xs text-red-300/90">{conn.lastError}</p>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||
{conn ? (
|
||||
<button
|
||||
onClick={() => remove(a.id)}
|
||||
disabled={isBusy}
|
||||
className="rounded-lg border border-white/10 px-3 py-1.5 text-xs font-medium text-white/60 transition hover:bg-white/[0.06] hover:text-white disabled:opacity-50"
|
||||
>
|
||||
{isBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Disconnect"}
|
||||
</button>
|
||||
) : !a.available ? (
|
||||
<p className="text-xs text-white/30">
|
||||
Ask your administrator to enable {a.label} (server credentials aren't configured).
|
||||
</p>
|
||||
) : a.kind === "oauth" ? (
|
||||
<a
|
||||
href={`/api/esign/${a.id}/connect`}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-indigo-500"
|
||||
>
|
||||
<Link2 className="h-3.5 w-3.5" /> Connect {a.label}
|
||||
</a>
|
||||
) : showKeyForm ? (
|
||||
<div className="flex w-full flex-col gap-2 sm:flex-row">
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="Paste your Dropbox Sign API key"
|
||||
className="flex-1 rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-xs text-white placeholder-white/30 outline-none focus:border-indigo-500/50"
|
||||
/>
|
||||
<button
|
||||
onClick={connectDbx}
|
||||
disabled={isBusy || !apiKey.trim()}
|
||||
className="flex items-center justify-center gap-1.5 rounded-lg bg-indigo-600 px-3 py-2 text-xs font-semibold text-white transition hover:bg-indigo-500 disabled:opacity-50"
|
||||
>
|
||||
{isBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <KeyRound className="h-3.5 w-3.5" />} Connect
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setShowKeyForm(true)}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-indigo-500"
|
||||
>
|
||||
<KeyRound className="h-3.5 w-3.5" /> Connect {a.label}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{a.available && (
|
||||
<button
|
||||
onClick={() => setOpen(instructionsOpen ? null : a.id)}
|
||||
className="ml-auto flex items-center gap-1 text-[11px] text-white/40 transition hover:text-white/70"
|
||||
>
|
||||
How to connect <ChevronDown className={`h-3 w-3 transition ${instructionsOpen ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
{instructionsOpen && (
|
||||
<div className="mt-3 rounded-lg border border-white/[0.06] bg-white/[0.02] p-4 text-xs leading-relaxed text-white/55">
|
||||
{a.id === "docusign" ? (
|
||||
<ol className="space-y-1.5">
|
||||
<li>
|
||||
1. You need an active{" "}
|
||||
<a href="https://www.docusign.com/products/electronic-signature" target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-0.5 text-indigo-400 hover:text-indigo-300">
|
||||
DocuSign eSignature plan <ExternalLink className="h-3 w-3" />
|
||||
</a>.
|
||||
</li>
|
||||
<li>2. Click <span className="text-white/80">Connect DocuSign</span> above.</li>
|
||||
<li>3. Log in to <span className="text-white/80">your</span> DocuSign account and click <span className="text-white/80">Allow</span> to grant access.</li>
|
||||
<li>4. You'll return here connected — no webhook setup needed. Signed-status updates and the completed PDF flow back automatically.</li>
|
||||
</ol>
|
||||
) : (
|
||||
<ol className="space-y-1.5">
|
||||
<li>
|
||||
1. In Dropbox Sign, open{" "}
|
||||
<a href="https://app.hellosign.com/account/settings/api" target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-0.5 text-indigo-400 hover:text-indigo-300">
|
||||
Settings → API <ExternalLink className="h-3 w-3" />
|
||||
</a>{" "}
|
||||
and copy your <span className="text-white/80">API key</span>.
|
||||
</li>
|
||||
<li>2. Paste it above and click <span className="text-white/80">Connect</span>.</li>
|
||||
<li>
|
||||
3. In the same API settings, set your <span className="text-white/80">account callback URL</span> to:
|
||||
<code className="mt-1 block overflow-x-auto rounded bg-black/30 px-2 py-1 font-mono text-[11px] text-emerald-300">{webhookUrl}</code>
|
||||
This lets us receive signed-status updates.
|
||||
</li>
|
||||
</ol>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<p className="px-1 text-[11px] text-white/30">
|
||||
Your credentials are encrypted at rest and never leave the server. We only send the leases you explicitly submit.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user