import { and, desc, eq } from "drizzle-orm" import { db } from "@/lib/db" import { leases, signature_requests } from "@/lib/db/schema" import { readFile, saveBuffer } from "@/lib/storage" import { getAdapter } from "./registry" import { resolveEsignCreds, markEsignError } from "./credentials" import type { ESignProvider } from "./types" export type { ESignProvider } from "./types" export { getAdapter, listEsignAdapters } from "./registry" export { resolveEsignCreds, listEsignConnections, getEsignConnection, saveEsignConnection, disconnectEsign, } from "./credentials" const FILES_PREFIX = "/api/files/" /** The webhook URL a provider should call back — used for DocuSign envelope-level Connect. */ function webhookUrl(provider: ESignProvider): string { const base = (process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000").replace(/\/+$/, "") return `${base}/api/esign/${provider}/webhook` } /** Read the lease's stored document. Only /api/files keys are allowed (no SSRF). */ async function getDocumentBytes(documentUrl: string): Promise<{ bytes: Buffer; name: string }> { if (!documentUrl.startsWith(FILES_PREFIX)) { throw new Error("Lease document must be an uploaded file") } const key = documentUrl.slice(FILES_PREFIX.length) return { bytes: await readFile(key), name: key.split("/").pop() ?? "lease.pdf" } } /** * Send a lease for signature through the owner's OWN connected account. * Requires the provider to be connected (per-user OAuth / API key). */ export async function sendLeaseForSignature(ownerId: string, leaseId: string, provider: ESignProvider) { const adapter = getAdapter(provider) if (!adapter) throw new Error("Unknown provider") const creds = await resolveEsignCreds(ownerId, provider) if (!creds) throw new Error(`Connect your ${adapter.label} account in Settings → Integrations first`) const lease = await db.query.leases.findFirst({ where: and(eq(leases.id, leaseId), eq(leases.user_id, ownerId)), with: { tenant: { columns: { first_name: true, last_name: true, email: true } } }, }) if (!lease) throw new Error("Lease not found") if (!lease.document_url) throw new Error("Upload a lease document before sending it for signature") const email = lease.tenant?.email if (!email) throw new Error("The tenant has no email address on file") const signerName = `${lease.tenant?.first_name ?? ""} ${lease.tenant?.last_name ?? ""}`.trim() || "Tenant" const { bytes, name } = await getDocumentBytes(lease.document_url) try { const { externalId } = await adapter.send(creds, { document: bytes, documentName: name, signerEmail: email, signerName, subject: "Please sign your lease agreement", message: "Your landlord has sent your lease agreement for electronic signature.", webhookUrl: webhookUrl(provider), }) const [row] = await db .insert(signature_requests) .values({ user_id: ownerId, lease_id: leaseId, provider, external_id: externalId, status: "sent", signer_email: email, signer_name: signerName, document_name: name }) .returning() return row } catch (e) { const msg = (e as Error).message.slice(0, 500) await markEsignError(ownerId, provider, msg) await db .insert(signature_requests) .values({ user_id: ownerId, lease_id: leaseId, provider, status: "error", signer_email: email, signer_name: signerName, document_name: name, last_error: msg }) throw new Error(msg) } } export async function listRequestsForLease(ownerId: string, leaseId: string) { return db.query.signature_requests.findMany({ where: and(eq(signature_requests.user_id, ownerId), eq(signature_requests.lease_id, leaseId)), orderBy: desc(signature_requests.sent_at), }) } /** * Process an inbound provider webhook. The body is UNTRUSTED: we use it only to * find which signature request (and therefore which owner + credentials) it * concerns, then authenticate the event via the adapter (DocuSign pull-verify / * Dropbox HMAC) before updating status and archiving the signed document. */ export async function handleEsignWebhook(provider: string, body: string, headers: Headers) { const adapter = getAdapter(provider) if (!adapter) return const externalId = adapter.peekExternalId(body) if (!externalId) return const reqRow = await db.query.signature_requests.findFirst({ where: eq(signature_requests.external_id, externalId), columns: { id: true, user_id: true, status: true }, }) if (!reqRow) return const creds = await resolveEsignCreds(reqRow.user_id, provider as ESignProvider) if (!creds) return const status = await adapter.verifyAndGetStatus(creds, externalId, body, headers) if (!status) return await db .update(signature_requests) .set({ status, completed_at: status === "signed" ? new Date().toISOString() : null, updated_at: new Date().toISOString(), }) .where(eq(signature_requests.id, reqRow.id)) // Archive the executed document so the landlord can download the signed copy. if (status === "signed") { try { const bytes = await adapter.getSignedDocument(creds, externalId) if (bytes && bytes.length) { const { key } = await saveBuffer(bytes, { userId: reqRow.user_id, scope: "esign", ext: "pdf" }) await db .update(signature_requests) .set({ signed_document_url: `${FILES_PREFIX}${key}` }) .where(eq(signature_requests.id, reqRow.id)) } } catch { // Best-effort — status is already recorded. } } }