Initial commit: Port Nimara CRM (Layers 0-4)
Some checks failed
Build & Push Docker Images / build-and-push (push) Has been cancelled
Build & Push Docker Images / deploy (push) Has been cancelled
Build & Push Docker Images / lint (push) Has been cancelled

Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 11:52:51 +01:00
commit 67d7e6e3d5
572 changed files with 86496 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
import { NextRequest, NextResponse } from 'next/server';
import { createHash } from 'crypto';
import { db } from '@/lib/db';
import { documentEvents } from '@/lib/db/schema/documents';
import { verifyDocumensoSignature } from '@/lib/services/documenso-webhook';
import {
handleRecipientSigned,
handleDocumentCompleted,
handleDocumentExpired,
} from '@/lib/services/documents.service';
import { env } from '@/lib/env';
import { logger } from '@/lib/logger';
// BR-024: Dedup via signatureHash unique index on documentEvents
// Always return 200 from webhook (webhook best practice)
export async function POST(req: NextRequest): Promise<NextResponse> {
let payload: string;
try {
payload = await req.text();
} catch {
return NextResponse.json({ ok: false }, { status: 200 });
}
// Verify HMAC signature
const signature = req.headers.get('x-documenso-signature') ?? '';
if (!verifyDocumensoSignature(payload, signature, env.DOCUMENSO_WEBHOOK_SECRET)) {
logger.warn({ signature }, 'Invalid Documenso webhook signature');
return NextResponse.json({ ok: false, error: 'Invalid signature' }, { status: 200 });
}
// Compute deduplication hash
const signatureHash = createHash('sha256').update(payload).digest('hex');
let parsed: { type: string; payload: Record<string, unknown> };
try {
parsed = JSON.parse(payload) as typeof parsed;
} catch {
logger.warn('Failed to parse Documenso webhook payload');
return NextResponse.json({ ok: false }, { status: 200 });
}
// Dedup: try to insert a sentinel documentEvent with signatureHash
// We need a documentId — if dedup fails at this stage we can't easily check.
// Instead, store the hash lookup on the first real documentEvent insert in handlers.
// Here we just check if this hash was already seen in any event.
try {
const existing = await db.query.documentEvents.findFirst({
where: (de, { eq }) => eq(de.signatureHash, signatureHash),
});
if (existing) {
logger.info({ signatureHash }, 'Duplicate Documenso webhook — skipping');
return NextResponse.json({ ok: true }, { status: 200 });
}
} catch (err) {
logger.error({ err }, 'Failed to check duplicate webhook');
}
try {
switch (parsed.type) {
case 'RECIPIENT_SIGNED':
await handleRecipientSigned({
documentId: parsed.payload.documentId as string,
recipientEmail: parsed.payload.recipientEmail as string,
signatureHash,
});
break;
case 'DOCUMENT_COMPLETED':
await handleDocumentCompleted({
documentId: parsed.payload.documentId as string,
});
break;
case 'DOCUMENT_EXPIRED':
await handleDocumentExpired({
documentId: parsed.payload.documentId as string,
});
break;
default:
logger.info({ type: parsed.type }, 'Unhandled Documenso webhook event type');
}
} catch (err) {
logger.error({ err, type: parsed.type }, 'Error processing Documenso webhook');
}
return NextResponse.json({ ok: true }, { status: 200 });
}