feat(06-02): POST /api/documents/[id]/send + document_prepared audit log
- Add send/route.ts: creates signing token, sends branded email, logs email_sent, updates status to Sent - Auth guard returns 401 for unauthenticated requests; 422 if not prepared; 409 if already signed - Wraps sendMail in try/catch — returns 502 without DB update if email delivery fails - Add logAuditEvent(document_prepared) to prepare/route.ts after successful PDF preparation
This commit is contained in:
@@ -3,6 +3,7 @@ import { db } from '@/lib/db';
|
|||||||
import { documents } from '@/lib/db/schema';
|
import { documents } from '@/lib/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { preparePdf } from '@/lib/pdf/prepare-document';
|
import { preparePdf } from '@/lib/pdf/prepare-document';
|
||||||
|
import { logAuditEvent } from '@/lib/signing/audit';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
const UPLOADS_DIR = path.join(process.cwd(), 'uploads');
|
const UPLOADS_DIR = path.join(process.cwd(), 'uploads');
|
||||||
@@ -54,5 +55,7 @@ export async function POST(
|
|||||||
.where(eq(documents.id, id))
|
.where(eq(documents.id, id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
await logAuditEvent({ documentId: id, eventType: 'document_prepared' });
|
||||||
|
|
||||||
return Response.json(updated);
|
return Response.json(updated);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { auth } from '@/lib/auth';
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
import { documents, clients } from '@/lib/db/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { createSigningToken } from '@/lib/signing/token';
|
||||||
|
import { sendSigningRequestEmail } from '@/lib/signing/signing-mailer';
|
||||||
|
import { logAuditEvent } from '@/lib/signing/audit';
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
_req: Request,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session) return new Response('Unauthorized', { status: 401 });
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const doc = await db.query.documents.findFirst({
|
||||||
|
where: eq(documents.id, id),
|
||||||
|
});
|
||||||
|
if (!doc) return NextResponse.json({ error: 'Document not found' }, { status: 404 });
|
||||||
|
if (!doc.preparedFilePath)
|
||||||
|
return NextResponse.json({ error: 'Document not yet prepared' }, { status: 422 });
|
||||||
|
if (doc.status === 'Signed')
|
||||||
|
return NextResponse.json({ error: 'Already signed' }, { status: 409 });
|
||||||
|
|
||||||
|
// Resolve recipient: prefer assignedClientId, fall back to clientId
|
||||||
|
const clientId = doc.assignedClientId ?? doc.clientId;
|
||||||
|
const client = await db.query.clients.findFirst({ where: eq(clients.id, clientId) });
|
||||||
|
if (!client) return NextResponse.json({ error: 'Client not found' }, { status: 422 });
|
||||||
|
|
||||||
|
const { token, expiresAt } = await createSigningToken(doc.id);
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000';
|
||||||
|
const signingUrl = `${baseUrl}/sign/${token}`;
|
||||||
|
|
||||||
|
await sendSigningRequestEmail({
|
||||||
|
to: client.email,
|
||||||
|
clientName: client.name,
|
||||||
|
documentName: doc.name,
|
||||||
|
signingUrl,
|
||||||
|
expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
await logAuditEvent({ documentId: doc.id, eventType: 'email_sent' });
|
||||||
|
|
||||||
|
// Update status to Sent (skip if already Sent or Signed to avoid downgrade)
|
||||||
|
if (doc.status === 'Draft') {
|
||||||
|
await db
|
||||||
|
.update(documents)
|
||||||
|
.set({ status: 'Sent', sentAt: new Date() })
|
||||||
|
.where(eq(documents.id, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true, expiresAt: expiresAt.toISOString() });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[send] error:', err);
|
||||||
|
return NextResponse.json({ error: 'Failed to send signing email' }, { status: 502 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user