mirror of
https://github.com/hanzo-apps/dataroom.git
synced 2026-10-30 14:47:36 +00:00
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]>
60 lines
2.0 KiB
TypeScript
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 });
|
|
}
|