mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
183fe0a93c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
234 lines
8.7 KiB
TypeScript
234 lines
8.7 KiB
TypeScript
// Avria Sign — send envelope (in-house e-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",
|
|
};
|
|
|
|
interface Recipient { name: string; email: string }
|
|
interface FieldPayload {
|
|
recipient_index: number;
|
|
field_type: "signature" | "date" | "name";
|
|
page_number: number;
|
|
x_ratio: number;
|
|
y_ratio: number;
|
|
width_ratio: number;
|
|
height_ratio: number;
|
|
}
|
|
|
|
function toSmtpSenderConfig(sender: any) {
|
|
const port = Number(sender?.smtp_port ?? 587);
|
|
const isImplicitSslPort = port === 465;
|
|
const isStartTlsPort = port === 587;
|
|
|
|
return {
|
|
host: sender.smtp_host,
|
|
port,
|
|
username: sender.smtp_username,
|
|
password: sender.smtp_password,
|
|
use_ssl: isImplicitSslPort ? true : sender.use_ssl ?? false,
|
|
use_tls: isImplicitSslPort ? false : sender.use_tls ?? isStartTlsPort,
|
|
from: sender.sender_name
|
|
? `${sender.sender_name} <${sender.email_address}>`
|
|
: sender.email_address,
|
|
fromEmail: sender.email_address,
|
|
fromName: sender.sender_name,
|
|
envelopeFrom: sender.smtp_username || sender.email_address,
|
|
signature_html: sender.signature_html || "",
|
|
};
|
|
}
|
|
|
|
Deno.serve(async (req) => {
|
|
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
|
|
|
|
try {
|
|
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
|
const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
|
const authHeader = req.headers.get("Authorization") || "";
|
|
const token = authHeader.replace("Bearer ", "");
|
|
|
|
const userClient = createClient(supabaseUrl, Deno.env.get("SUPABASE_ANON_KEY")!, {
|
|
global: { headers: { Authorization: authHeader } },
|
|
});
|
|
const { data: userData } = await userClient.auth.getUser(token);
|
|
if (!userData?.user) {
|
|
return new Response(JSON.stringify({ error: "unauthorized" }), { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } });
|
|
}
|
|
|
|
const admin = createClient(supabaseUrl, serviceKey);
|
|
const body = await req.json();
|
|
const {
|
|
association_id,
|
|
document_name,
|
|
document_url,
|
|
document_base64,
|
|
file_extension = "pdf",
|
|
recipients,
|
|
email_subject,
|
|
email_body,
|
|
fields = [],
|
|
}: {
|
|
association_id?: string;
|
|
document_name: string;
|
|
document_url?: string;
|
|
document_base64?: string;
|
|
file_extension?: string;
|
|
recipients: Recipient[];
|
|
email_subject?: string;
|
|
email_body?: string;
|
|
fields?: FieldPayload[];
|
|
} = body;
|
|
|
|
if (!document_name || !recipients?.length) {
|
|
return new Response(JSON.stringify({ error: "missing document_name or recipients" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } });
|
|
}
|
|
|
|
// 1) Insert envelope
|
|
const { data: env, error: envErr } = await admin.from("signature_envelopes").insert({
|
|
association_id: association_id || null,
|
|
document_name,
|
|
document_url: document_url || "",
|
|
email_subject,
|
|
email_body,
|
|
status: "sent",
|
|
created_by: userData.user.id,
|
|
sent_at: new Date().toISOString(),
|
|
}).select().single();
|
|
if (envErr) throw envErr;
|
|
|
|
// 2) If raw upload, store
|
|
let finalDocUrl = document_url || "";
|
|
if (document_base64) {
|
|
const binary = Uint8Array.from(atob(document_base64), c => c.charCodeAt(0));
|
|
const path = `signature-envelopes/${env.id}/original.${file_extension}`;
|
|
const { error: upErr } = await admin.storage.from("files").upload(path, binary, {
|
|
contentType: file_extension === "pdf" ? "application/pdf" : "application/octet-stream",
|
|
upsert: true,
|
|
});
|
|
if (upErr) throw upErr;
|
|
const { data: pub } = admin.storage.from("files").getPublicUrl(path);
|
|
finalDocUrl = pub.publicUrl;
|
|
await admin.from("signature_envelopes").update({ document_url: finalDocUrl }).eq("id", env.id);
|
|
}
|
|
|
|
// 3) Insert recipients
|
|
const recipientRows = recipients.map((r, idx) => ({
|
|
envelope_id: env.id,
|
|
name: r.name.trim(),
|
|
email: r.email.trim().toLowerCase(),
|
|
signing_order: idx + 1,
|
|
status: "pending",
|
|
}));
|
|
const { data: insertedRecips, error: rErr } = await admin.from("signature_recipients").insert(recipientRows).select();
|
|
if (rErr) throw rErr;
|
|
|
|
// 3b) Insert placed fields, mapping recipient_index -> inserted recipient id
|
|
if (fields.length > 0 && insertedRecips) {
|
|
const fieldRows = fields
|
|
.filter(f => insertedRecips[f.recipient_index])
|
|
.map(f => ({
|
|
envelope_id: env.id,
|
|
recipient_id: insertedRecips[f.recipient_index].id,
|
|
field_type: f.field_type,
|
|
page_number: f.page_number,
|
|
x_ratio: f.x_ratio,
|
|
y_ratio: f.y_ratio,
|
|
width_ratio: f.width_ratio,
|
|
height_ratio: f.height_ratio,
|
|
}));
|
|
if (fieldRows.length > 0) {
|
|
const { error: fErr } = await admin.from("signature_fields").insert(fieldRows);
|
|
if (fErr) console.warn("field insert error:", fErr);
|
|
}
|
|
}
|
|
|
|
// 4) Audit
|
|
await admin.from("signature_events").insert({
|
|
envelope_id: env.id,
|
|
event_type: "sent",
|
|
details: { recipient_count: recipients.length, document_name, field_count: fields.length },
|
|
});
|
|
|
|
// 5) Default sender
|
|
const { data: defaultSender } = await admin
|
|
.from("email_senders")
|
|
.select("smtp_host, smtp_port, smtp_username, smtp_password, use_tls, use_ssl, email_address, sender_name, signature_html")
|
|
.eq("is_active", true)
|
|
.eq("is_default", true)
|
|
.order("updated_at", { ascending: false })
|
|
.limit(1)
|
|
.maybeSingle();
|
|
|
|
const smtpSender = defaultSender ? toSmtpSenderConfig(defaultSender) : null;
|
|
|
|
// 6) Email + notifications
|
|
const appOrigin = req.headers.get("origin") || "https://avria.cloud";
|
|
const subject = email_subject || `Please sign: ${document_name}`;
|
|
|
|
for (const recip of insertedRecips || []) {
|
|
const signUrl = `${appOrigin}/sign/${recip.signing_token}`;
|
|
const html = `
|
|
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;padding:20px;">
|
|
<h2 style="color:#1e3a8a;">Signature Requested</h2>
|
|
<p>Hi ${recip.name},</p>
|
|
<p>You have been requested to electronically sign the following document:</p>
|
|
<p style="font-size:16px;font-weight:bold;color:#1e3a8a;">${document_name}</p>
|
|
${email_body ? `<p style="white-space:pre-wrap;">${email_body}</p>` : ""}
|
|
<p style="margin:30px 0;">
|
|
<a href="${signUrl}" style="background:#1e3a8a;color:#fff;padding:12px 24px;text-decoration:none;border-radius:6px;display:inline-block;">Review & Sign Document</a>
|
|
</p>
|
|
<p style="font-size:12px;color:#666;">Or copy this link into your browser:<br/>${signUrl}</p>
|
|
<hr style="margin-top:30px;border:none;border-top:1px solid #eee;"/>
|
|
<p style="font-size:11px;color:#999;">Avria Sign — Secure Electronic Signatures</p>
|
|
</div>`;
|
|
|
|
if (smtpSender) {
|
|
try {
|
|
await admin.functions.invoke("send-smtp-email", {
|
|
body: {
|
|
sender: smtpSender,
|
|
recipient: recip.email,
|
|
subject,
|
|
html,
|
|
body: html,
|
|
fallback_user_id: userData.user.id,
|
|
feature_type: "signature_request",
|
|
},
|
|
});
|
|
} catch (e) {
|
|
console.warn(`SMTP email failed for ${recip.email}:`, e);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const { data: existingUser } = await admin
|
|
.from("profiles").select("user_id").ilike("email", recip.email).maybeSingle();
|
|
const targetUserId = existingUser?.user_id || recip.user_id;
|
|
if (targetUserId) {
|
|
await admin.rpc("insert_notification", {
|
|
p_user_id: targetUserId,
|
|
p_type: "signature_request",
|
|
p_title: "Signature requested",
|
|
p_message: `Please sign: ${document_name}`,
|
|
p_related_item_id: env.id,
|
|
p_related_item_type: "signature_envelope",
|
|
p_link: `/sign/${recip.signing_token}`,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.warn(`Notification failed for ${recip.email}:`, e);
|
|
}
|
|
}
|
|
|
|
return new Response(JSON.stringify({
|
|
envelope_id: env.id,
|
|
recipient_count: insertedRecips?.length || 0,
|
|
field_count: fields.length,
|
|
}), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
|
|
} catch (err: any) {
|
|
console.error("avria-sign-send error:", err);
|
|
return new Response(JSON.stringify({ error: err.message || String(err) }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });
|
|
}
|
|
});
|