mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
183fe0a93c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
140 lines
5.1 KiB
TypeScript
140 lines
5.1 KiB
TypeScript
// Avria Sign — public endpoints: lookup envelope by token, submit signature
|
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0";
|
|
|
|
const corsHeaders = {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
|
|
};
|
|
|
|
Deno.serve(async (req) => {
|
|
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
|
|
|
|
const url = new URL(req.url);
|
|
const action = url.searchParams.get("action") || "lookup";
|
|
const admin = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!);
|
|
|
|
try {
|
|
if (action === "lookup") {
|
|
const tokenStr = url.searchParams.get("token");
|
|
if (!tokenStr) return json({ error: "missing token" }, 400);
|
|
|
|
const { data: recip, error: rErr } = await admin
|
|
.from("signature_recipients")
|
|
.select("id, envelope_id, name, email, status, signed_at")
|
|
.eq("signing_token", tokenStr)
|
|
.maybeSingle();
|
|
if (rErr || !recip) return json({ error: "invalid token" }, 404);
|
|
|
|
const { data: env } = await admin
|
|
.from("signature_envelopes")
|
|
.select("id, document_name, document_url, signed_document_url, status, email_body, association_id, voided_at, voided_reason")
|
|
.eq("id", recip.envelope_id)
|
|
.maybeSingle();
|
|
|
|
// If voided, return early
|
|
if (env?.status === "voided" || env?.voided_at) {
|
|
return json({ recipient: recip, envelope: env, voided: true });
|
|
}
|
|
|
|
// Fetch fields for this recipient
|
|
const { data: fields } = await admin
|
|
.from("signature_fields")
|
|
.select("id, field_type, page_number, x_ratio, y_ratio, width_ratio, height_ratio, required")
|
|
.eq("recipient_id", recip.id)
|
|
.order("page_number");
|
|
|
|
// log view event (only first time)
|
|
if (recip.status === "pending") {
|
|
await admin.from("signature_events").insert({
|
|
envelope_id: recip.envelope_id,
|
|
recipient_id: recip.id,
|
|
event_type: "viewed",
|
|
ip_address: req.headers.get("x-forwarded-for") || null,
|
|
user_agent: req.headers.get("user-agent") || null,
|
|
});
|
|
await admin.from("signature_recipients").update({ status: "viewed" }).eq("id", recip.id).eq("status", "pending");
|
|
}
|
|
|
|
return json({ recipient: recip, envelope: env, fields: fields || [] });
|
|
}
|
|
|
|
if (action === "sign" && req.method === "POST") {
|
|
const body = await req.json();
|
|
const { token, signature_data_url, signature_method } = body;
|
|
if (!token || !signature_data_url) return json({ error: "missing fields" }, 400);
|
|
|
|
const { data: recip } = await admin
|
|
.from("signature_recipients")
|
|
.select("id, envelope_id, status")
|
|
.eq("signing_token", token)
|
|
.maybeSingle();
|
|
if (!recip) return json({ error: "invalid token" }, 404);
|
|
if (recip.status === "signed") return json({ error: "already signed" }, 400);
|
|
|
|
// Refuse if envelope voided
|
|
const { data: envCheck } = await admin
|
|
.from("signature_envelopes").select("status, voided_at").eq("id", recip.envelope_id).maybeSingle();
|
|
if (envCheck?.status === "voided" || envCheck?.voided_at) {
|
|
return json({ error: "This envelope has been voided and can no longer be signed." }, 400);
|
|
}
|
|
|
|
const ip = req.headers.get("x-forwarded-for") || null;
|
|
const ua = req.headers.get("user-agent") || null;
|
|
|
|
await admin.from("signature_recipients").update({
|
|
signature_data_url,
|
|
signature_method: signature_method || "draw",
|
|
status: "signed",
|
|
signed_at: new Date().toISOString(),
|
|
signed_ip: ip,
|
|
signed_user_agent: ua,
|
|
}).eq("id", recip.id);
|
|
|
|
await admin.from("signature_events").insert({
|
|
envelope_id: recip.envelope_id,
|
|
recipient_id: recip.id,
|
|
event_type: "signed",
|
|
ip_address: ip,
|
|
user_agent: ua,
|
|
details: { method: signature_method || "draw" },
|
|
});
|
|
|
|
const { data: allRecips } = await admin
|
|
.from("signature_recipients")
|
|
.select("status")
|
|
.eq("envelope_id", recip.envelope_id);
|
|
|
|
const allSigned = (allRecips || []).every(r => r.status === "signed");
|
|
if (allSigned) {
|
|
await admin.from("signature_envelopes").update({
|
|
status: "completed",
|
|
completed_at: new Date().toISOString(),
|
|
}).eq("id", recip.envelope_id);
|
|
|
|
await admin.from("signature_events").insert({
|
|
envelope_id: recip.envelope_id,
|
|
event_type: "completed",
|
|
});
|
|
|
|
admin.functions.invoke("avria-sign-stamp", {
|
|
body: { envelope_id: recip.envelope_id },
|
|
}).catch(e => console.warn("stamp invoke failed:", e));
|
|
}
|
|
|
|
return json({ ok: true, all_signed: allSigned });
|
|
}
|
|
|
|
return json({ error: "unknown action" }, 400);
|
|
} catch (err: any) {
|
|
console.error("avria-sign-public error:", err);
|
|
return json({ error: err.message || String(err) }, 500);
|
|
}
|
|
});
|
|
|
|
function json(payload: unknown, status = 200) {
|
|
return new Response(JSON.stringify(payload), {
|
|
status,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|