1. HIGH — /api/v1/admin/ports/[id] PATCH+GET let any port-admin (manage_settings) mutate any other tenant's port row by passing the foreign id in the path. Now non-super-admins must target their own ctx.portId; listPorts and createPort are super-admin only. 2. HIGH — Invoice create/update accepted arbitrary expenseIds and linked them into invoice_expenses with no port check; the GET response then re-emitted those foreign expense rows via the linkedExpenses join. assertExpensesInPort now validates each id belongs to the caller's portId before insert; getInvoiceById's join filters by expenses.portId as defense-in-depth. 3. HIGH — Document creation paths (createDocument, createFromWizard, createFromUpload) persisted user-supplied clientId/interestId/ companyId/yachtId/reservationId without verifying those FKs were in-port. sendForSigning then loaded the foreign client/interest by id alone and pushed their PII into the Documenso payload. New assertSubjectFksInPort helper rejects out-of-port FKs at create time; sendForSigning's interest+client lookups now also filter by portId. 4. MEDIUM — calculateInterestScore read its redis cache before verifying portId, and the cache key was interestId-only — a foreign-port caller could observe a cached score breakdown. Cache key now includes portId, and the port-scope DB lookup runs before any cache.get. 5. MEDIUM — AI email-draft job results were retrievable by anyone who could guess the BullMQ jobId (default sequential integers). Job ids are now random UUIDs, requestEmailDraft validates interestId/ clientId belong to ctx.portId before enqueueing, the worker's client lookup is port-scoped, and getEmailDraftResult requires the caller to match the original requester's userId+portId before returning the drafted subject/body. The interest-scoring unit test that asserted "DB is bypassed on cache hit" is updated to reflect the new (security-correct) ordering. Two new regression test files cover the email-draft binding (5 tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
28 lines
751 B
TypeScript
28 lines
751 B
TypeScript
import { NextResponse } from 'next/server';
|
|
|
|
import { withAuth } from '@/lib/api/helpers';
|
|
import { getEmailDraftResult } from '@/lib/services/email-draft.service';
|
|
import { errorResponse } from '@/lib/errors';
|
|
|
|
export const GET = withAuth(async (_req, ctx, params) => {
|
|
try {
|
|
const { jobId } = params;
|
|
if (!jobId) {
|
|
return NextResponse.json({ error: 'jobId is required' }, { status: 400 });
|
|
}
|
|
|
|
const result = await getEmailDraftResult(jobId, {
|
|
userId: ctx.userId,
|
|
portId: ctx.portId,
|
|
});
|
|
|
|
if (result === null) {
|
|
return NextResponse.json({ status: 'processing' });
|
|
}
|
|
|
|
return NextResponse.json({ status: 'complete', data: result });
|
|
} catch (error) {
|
|
return errorResponse(error);
|
|
}
|
|
});
|