import { headers } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; import { verifySigningToken } from '@/lib/signing/token'; import { logAuditEvent } from '@/lib/signing/audit'; import { db } from '@/lib/db'; import { signingTokens, documents } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; // Public route — no auth session required export async function GET( _req: NextRequest, { params }: { params: Promise<{ token: string }> } ) { const { token } = await params; // Extract IP and user-agent for audit logging const hdrs = await headers(); const ip = hdrs.get('x-forwarded-for')?.split(',')[0]?.trim() ?? hdrs.get('x-real-ip') ?? 'unknown'; const ua = hdrs.get('user-agent') ?? 'unknown'; // 1. Verify JWT let payload: { documentId: string; jti: string; exp: number }; try { payload = await verifySigningToken(token); } catch (err: unknown) { // Check if it's a JWT expiry error const errName = err instanceof Error ? err.constructor.name : ''; const errMessage = err instanceof Error ? err.message : ''; if (errName === 'JWTExpired' || errMessage.includes('expired')) { return NextResponse.json({ status: 'expired' }, { status: 200 }); } return NextResponse.json({ status: 'invalid' }, { status: 200 }); } // 2. Look up jti in signingTokens table const tokenRow = await db.query.signingTokens.findFirst({ where: eq(signingTokens.jti, payload.jti), }); if (!tokenRow) { return NextResponse.json({ status: 'invalid' }, { status: 200 }); } // 3. Check one-time use if (tokenRow.usedAt !== null) { return NextResponse.json( { status: 'used', signedAt: tokenRow.usedAt.toISOString() }, { status: 200 } ); } // 4. Fetch document const doc = await db.query.documents.findFirst({ where: eq(documents.id, payload.documentId), columns: { id: true, name: true, signatureFields: true, preparedFilePath: true, }, }); if (!doc || !doc.preparedFilePath) { return NextResponse.json({ status: 'invalid' }, { status: 200 }); } // 5 & 6. Log audit events: link_opened + document_viewed await logAuditEvent({ documentId: payload.documentId, eventType: 'link_opened', ipAddress: ip, userAgent: ua, }); await logAuditEvent({ documentId: payload.documentId, eventType: 'document_viewed', ipAddress: ip, userAgent: ua, }); // 7. Return pending state with document data return NextResponse.json({ status: 'pending', document: { id: doc.id, name: doc.name, signatureFields: doc.signatureFields ?? [], preparedFilePath: doc.preparedFilePath, }, expiresAt: new Date(payload.exp * 1000).toISOString(), }); }