feat(15-02): rewrite send route with multi-signer token loop and legacy fallback
- Loop over doc.signers when populated: one createSigningToken per signer with signerEmail - Dispatch all signing emails in parallel via Promise.all - Preserve legacy single-signer path unchanged when signers is null/empty - Replace NEXT_PUBLIC_BASE_URL with APP_BASE_URL for signing URLs - Add audit event with metadata.signerEmail for each signer in multi-signer path - Import DocumentSigner from schema for type casting
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { auth } from '@/lib/auth';
|
import { auth } from '@/lib/auth';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { documents, clients } from '@/lib/db/schema';
|
import { documents, clients, DocumentSigner } from '@/lib/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { createSigningToken } from '@/lib/signing/token';
|
import { createSigningToken } from '@/lib/signing/token';
|
||||||
import { sendSigningRequestEmail } from '@/lib/signing/signing-mailer';
|
import { sendSigningRequestEmail } from '@/lib/signing/signing-mailer';
|
||||||
@@ -26,15 +26,36 @@ export async function POST(
|
|||||||
if (doc.status === 'Signed')
|
if (doc.status === 'Signed')
|
||||||
return NextResponse.json({ error: 'Already signed' }, { status: 409 });
|
return NextResponse.json({ error: 'Already signed' }, { status: 409 });
|
||||||
|
|
||||||
// Resolve recipient: prefer assignedClientId, fall back to clientId
|
const baseUrl = process.env.APP_BASE_URL ?? 'http://localhost:3000';
|
||||||
|
|
||||||
|
if (doc.signers && (doc.signers as DocumentSigner[]).length > 0) {
|
||||||
|
// ── Multi-signer path (D-02) ──
|
||||||
|
const signers = doc.signers as DocumentSigner[];
|
||||||
|
await Promise.all(
|
||||||
|
signers.map(async (signer) => {
|
||||||
|
const { token, expiresAt } = await createSigningToken(doc.id, signer.email);
|
||||||
|
const signingUrl = `${baseUrl}/sign/${token}`;
|
||||||
|
await sendSigningRequestEmail({
|
||||||
|
to: signer.email,
|
||||||
|
documentName: doc.name,
|
||||||
|
signingUrl,
|
||||||
|
expiresAt,
|
||||||
|
});
|
||||||
|
await logAuditEvent({
|
||||||
|
documentId: doc.id,
|
||||||
|
eventType: 'email_sent',
|
||||||
|
metadata: { signerEmail: signer.email },
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// ── Legacy single-signer path (D-01) ──
|
||||||
const clientId = doc.assignedClientId ?? doc.clientId;
|
const clientId = doc.assignedClientId ?? doc.clientId;
|
||||||
const client = await db.query.clients.findFirst({ where: eq(clients.id, clientId) });
|
const client = await db.query.clients.findFirst({ where: eq(clients.id, clientId) });
|
||||||
if (!client) return NextResponse.json({ error: 'Client not found' }, { status: 422 });
|
if (!client) return NextResponse.json({ error: 'Client not found' }, { status: 422 });
|
||||||
|
|
||||||
const { token, expiresAt } = await createSigningToken(doc.id);
|
const { token, expiresAt } = await createSigningToken(doc.id);
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000';
|
|
||||||
const signingUrl = `${baseUrl}/sign/${token}`;
|
const signingUrl = `${baseUrl}/sign/${token}`;
|
||||||
|
|
||||||
await sendSigningRequestEmail({
|
await sendSigningRequestEmail({
|
||||||
to: client.email,
|
to: client.email,
|
||||||
clientName: client.name,
|
clientName: client.name,
|
||||||
@@ -42,8 +63,8 @@ export async function POST(
|
|||||||
signingUrl,
|
signingUrl,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
await logAuditEvent({ documentId: doc.id, eventType: 'email_sent' });
|
await logAuditEvent({ documentId: doc.id, eventType: 'email_sent' });
|
||||||
|
}
|
||||||
|
|
||||||
// Update status to Sent (skip if already Sent or Signed to avoid downgrade)
|
// Update status to Sent (skip if already Sent or Signed to avoid downgrade)
|
||||||
if (doc.status === 'Draft') {
|
if (doc.status === 'Draft') {
|
||||||
@@ -53,7 +74,7 @@ export async function POST(
|
|||||||
.where(eq(documents.id, id));
|
.where(eq(documents.id, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ ok: true, expiresAt: expiresAt.toISOString() });
|
return NextResponse.json({ ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[send] error:', err);
|
console.error('[send] error:', err);
|
||||||
return NextResponse.json({ error: 'Failed to send signing email' }, { status: 502 });
|
return NextResponse.json({ error: 'Failed to send signing email' }, { status: 502 });
|
||||||
|
|||||||
Reference in New Issue
Block a user