Files
zooqueenandHanzo Dev 70be050994 sqlite: drop the two query arguments it rejects
Same failure as skipDuplicates, confirmed the same way against a database
built from this schema: Prisma does not ignore these on sqlite, it rejects
the call.

  mode: "insensitive" — broke document search and listing, dataroom search,
    and the two download-by-email lookups. Plain `contains` goes through LIKE,
    which sqlite already treats case-insensitively for ascii, so dropping the
    argument keeps the behaviour. `equals` does NOT: sqlite compares with `=`
    case-sensitively, so the two lookups that matched a visitor's address that
    way now narrow with `contains` and settle it with an exact comparison —
    verify.ts was already doing that comparison, by-email.ts was not.

    One of these was invisible to tsc: the documents where clause is assembled
    from spreads into a variable, so nothing checked it against Prisma's type
    and only the runtime rejected it.

  array_contains — every webhook delivery was throwing instead of fanning out.
    Three copies of the same query, now one `subscribers()` that matches the
    trigger against the list. `Webhook.triggers` is another postgres String[]
    that became Json, so it is narrowed like the rest.

99 errors -> 11, no file worse than it was.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 11:56:11 -07:00

60 lines
2.0 KiB
TypeScript

import { NextApiRequest, NextApiResponse } from "next";
import { getDataroomSessionByLinkIdInPagesRouter } from "@/lib/auth/dataroom-auth";
import prisma from "@/lib/prisma";
import { ratelimit } from "@/lib/kv";
import { getIpAddress } from "@/lib/utils/ip";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "POST") {
res.setHeader("Allow", ["POST"]);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
// 1. IP-based rate limiting (same pattern as OTP endpoints in verify.ts)
const ipAddress = getIpAddress(req.headers) ?? "unknown";
const { success } = await ratelimit(10, "1 m").limit(
`download-by-email-ip:${ipAddress}`,
);
if (!success) {
return res
.status(429)
.json({ error: "Too many requests. Try again later." });
}
const { linkId, email } = req.body as { linkId?: string; email?: string };
if (!linkId || !email) {
return res.status(400).json({ error: "linkId and email are required" });
}
// 2. Require a valid dataroom session
const session = await getDataroomSessionByLinkIdInPagesRouter(req, linkId);
if (!session) {
return res.status(401).json({ error: "Session required" });
}
// 3. Prevent email enumeration — always return 200 with uniform body
// `contains` rather than `equals` because sqlite compares strings with `=`
// case-sensitively, and this has to match the address however the visitor
// typed it. `contains` goes through LIKE, which sqlite treats
// case-insensitively for ascii. It is a substring match, so one address can
// be found inside another, and the exact comparison below settles it.
const views = await prisma.view.findMany({
where: {
linkId,
viewType: "DATAROOM_VIEW",
viewerEmail: { contains: email },
},
select: { id: true, viewerEmail: true },
orderBy: { viewedAt: "desc" },
});
const view = views.find(
(v) => v.viewerEmail?.toLowerCase() === email.toLowerCase(),
);
return res.status(200).json({ viewId: view?.id ?? null });
}