Files
zeekayandHanzo Dev 4e7c3508ce after: background work outlives the response without a platform to ask
`waitUntil` existed because a serverless invocation is frozen the moment it
responds — anything still running is killed unless the platform is told to wait.
This app is a long-lived Node process on Kubernetes. Nothing freezes, and those
promises were never at risk.

lib/after.ts is the one call sites make. Inside the App Router it delegates to
Next's own `after()`, which schedules the work against the request so it runs
after the response is flushed instead of racing it. That needs App Router request
context and THROWS without one, and ten of these fifteen sites are Pages Router
API routes — so outside that context the promise runs detached, which on a
persistent server is all waitUntil ever bought.

Either path catches. An unhandled background rejection takes the process down
under Node's default policy, and losing a pod because an analytics call 500'd is
not a trade worth making — waitUntil swallowed those, so this is not a behaviour
we are giving up.

@vercel/functions now has no importers.

Verified with npx tsc --noEmit: 100 errors before and after, 0 introduced,
compared by file+code+message so inserted imports cannot masquerade as new ones.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 10:34:32 -07:00

756 lines
22 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { getStorageConfig } from "@/lib/storage/config";
// Import authOptions directly from the source
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { after } from "@/lib/after";
import { getServerSession } from "next-auth";
import { hashToken } from "@/lib/api/auth/token";
import { verifyPreviewSession } from "@/lib/auth/preview-auth";
import { PreviewSession } from "@/lib/auth/preview-auth";
import { sendOtpVerificationEmail } from "@/lib/emails/send-email-otp-verification";
import { getFeatureFlags } from "@/lib/featureFlags";
import { getFile } from "@/lib/files/get-file";
import { newId } from "@/lib/id-helper";
import { notifyDocumentView } from "@/lib/integrations/slack/events";
import prisma from "@/lib/prisma";
import { ratelimit } from "@/lib/kv";
import { parseSheet } from "@/lib/sheet";
import { recordLinkView } from "@/lib/tracking/record-link-view";
import { CustomUser, WatermarkConfigSchema } from "@/lib/types";
import { checkPassword, decryptEncrpytedPassword, log } from "@/lib/utils";
import { isEmailMatched } from "@/lib/utils/email-domain";
import { generateOTP } from "@/lib/utils/generate-otp";
import { LOCALHOST_IP, getClientIp } from "@/lib/utils/geo";
import { checkGlobalBlockList } from "@/lib/utils/global-block-list";
import { validateEmail } from "@/lib/utils/validate-email";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// POST /api/views
const {
linkId,
documentId,
userId,
documentVersionId,
documentName,
hasPages,
ownerId,
...data
} = body as {
linkId: string;
documentId: string;
userId: string | null;
documentVersionId: string;
documentName: string;
hasPages: boolean;
ownerId: string;
};
const { email, password, name, hasConfirmedAgreement } = data as {
email: string;
password: string;
name?: string;
hasConfirmedAgreement?: boolean;
};
// Add customFields to the data extraction
const { customFields } = data as {
customFields?: { [key: string]: string };
};
// INFO: for using the advanced excel viewer
const { useAdvancedExcelViewer } = data as {
useAdvancedExcelViewer: boolean;
};
// previewToken is used to determine if the view is a preview and therefore should not be recorded
const { previewToken } = data as {
previewToken?: string;
};
// Email Verification Data
const { code, token, verifiedEmail } = data as {
code?: string;
token?: string;
verifiedEmail?: string;
};
// Fetch the link to verify the settings
const link = await prisma.link.findUnique({
where: {
id: linkId,
},
select: {
id: true,
name: true,
documentId: true,
emailProtected: true,
enableNotification: true,
emailAuthenticated: true,
password: true,
domainSlug: true,
isArchived: true,
deletedAt: true,
slug: true,
allowList: true,
denyList: true,
enableAgreement: true,
agreementId: true,
enableWatermark: true,
watermarkConfig: true,
teamId: true,
team: {
select: {
plan: true,
globalBlockList: true,
pauseStartsAt: true,
},
},
customFields: {
select: {
identifier: true,
label: true,
},
},
visitorGroups: {
select: {
visitorGroup: {
select: {
emails: true,
},
},
},
},
},
});
// Check if link exists
if (!link) {
return NextResponse.json({ message: "Link not found." }, { status: 404 });
}
// Check if link is archived
if (link.isArchived) {
return NextResponse.json(
{ message: "Link is no longer available." },
{ status: 404 },
);
}
if (link.deletedAt) {
return NextResponse.json(
{ message: "Link has been deleted." },
{ status: 404 },
);
}
let isEmailVerified: boolean = false;
let hashedVerificationToken: string | null = null;
// Check if the user is part of the team and therefore skip verification steps
let isTeamMember: boolean = false;
let isPreview: boolean = false;
if (userId && previewToken) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json(
{ message: "You need to be logged in to preview the link." },
{ status: 401 },
);
}
const sessionUserId = (session.user as CustomUser).id;
const teamMembership = await prisma.userTeam.findUnique({
where: {
userId_teamId: {
userId: sessionUserId,
teamId: link.teamId!,
},
},
});
if (teamMembership) {
isTeamMember = true;
isPreview = true;
isEmailVerified = true;
}
}
if (!isTeamMember) {
// Check if email is required for visiting the link
if (link.emailProtected) {
if (!email || email.trim() === "") {
return NextResponse.json(
{ message: "Email is required." },
{ status: 400 },
);
}
// validate email
if (!validateEmail(email)) {
return NextResponse.json(
{ message: "Invalid email address." },
{ status: 400 },
);
}
}
// Check if password is required for visiting the link
if (link.password) {
if (!password || password.trim() === "") {
return NextResponse.json(
{ message: "Password is required." },
{ status: 400 },
);
}
let isPasswordValid: boolean = false;
const textParts: string[] = link.password.split(":");
if (!textParts || textParts.length !== 2) {
isPasswordValid = await checkPassword(password, link.password);
} else {
const decryptedPassword = decryptEncrpytedPassword(link.password);
isPasswordValid = decryptedPassword === password;
}
if (!isPasswordValid) {
return NextResponse.json(
{ message: "Invalid password." },
{ status: 403 },
);
}
}
// Check if agreement is required for visiting the link
if (link.enableAgreement && !hasConfirmedAgreement) {
return NextResponse.json(
{ message: "Agreement to NDA is required." },
{ status: 400 },
);
}
// Check global block list first - this overrides all other access controls
const globalBlockCheck = checkGlobalBlockList(
email,
link.team?.globalBlockList,
);
if (globalBlockCheck.error) {
return NextResponse.json(
{ message: globalBlockCheck.error },
{ status: 400 },
);
}
if (globalBlockCheck.isBlocked) {
return NextResponse.json({ message: "Access denied" }, { status: 403 });
}
// Build combined allow list from individual emails + visitor groups
const visitorGroupEmails =
link.visitorGroups?.flatMap((vg) => vg.visitorGroup.emails) || [];
const combinedAllowList = [
...(link.allowList || []),
...visitorGroupEmails,
];
// Check if email is allowed to visit the link
if (combinedAllowList.length > 0) {
// Determine if the email or its domain is allowed
const isAllowed = combinedAllowList.some((allowed) =>
isEmailMatched(email, allowed),
);
// Deny access if the email is not allowed
if (!isAllowed) {
return NextResponse.json(
{ message: "Unauthorized access" },
{ status: 403 },
);
}
}
// Check if email is denied to visit the link
if (link.denyList && link.denyList.length > 0) {
// Determine if the email or its domain is denied
const isDenied = link.denyList.some((denied) =>
isEmailMatched(email, denied),
);
// Deny access if the email is denied
if (isDenied) {
return NextResponse.json(
{ message: "Unauthorized access" },
{ status: 403 },
);
}
}
// Request OTP Code for email verification if
// 1) email verification is required and
// 2) code is not provided or token not provided
if (link.emailAuthenticated && !code && !token) {
const ipAddressValue = getClientIp(request.headers);
// Rate limit per email/link combination (1 per 30 seconds) to prevent OTP flooding
const { success: emailLimitSuccess } = await ratelimit(1, "30 s").limit(
`send-otp:${linkId}:${email}`,
);
if (!emailLimitSuccess) {
return NextResponse.json(
{
message:
"Please wait before requesting another code. Try again in 30 seconds.",
},
{ status: 429 },
);
}
// Additional IP-based rate limit (10 per minute) to prevent abuse across different emails
const { success } = await ratelimit(10, "1 m").limit(
`send-otp:${ipAddressValue}`,
);
if (!success) {
return NextResponse.json(
{ message: "Too many requests. Please try again later." },
{ status: 429 },
);
}
await prisma.verificationToken.deleteMany({
where: {
identifier: `otp:${linkId}:${email}`,
},
});
const otpCode = generateOTP();
const expiresAt = new Date();
expiresAt.setMinutes(expiresAt.getMinutes() + 10); // token expires at 10 minutes
await prisma.verificationToken.create({
data: {
token: otpCode,
identifier: `otp:${linkId}:${email}`,
expires: expiresAt,
},
});
after(
sendOtpVerificationEmail(email, otpCode, false, link.teamId!),
);
return NextResponse.json({
type: "email-verification",
message: "Verification email sent.",
});
}
if (link.emailAuthenticated && code) {
const ipAddressValue = getClientIp(request.headers);
const { success } = await ratelimit(10, "1 m").limit(
`verify-otp:${ipAddressValue}`,
);
if (!success) {
return NextResponse.json(
{ message: "Too many requests. Please try again later." },
{ status: 429 },
);
}
// Check if the OTP code is valid
const verification = await prisma.verificationToken.findUnique({
where: {
token: code,
identifier: `otp:${linkId}:${email}`,
},
});
if (!verification) {
return NextResponse.json(
{
message: "Unauthorized access. Request new access.",
resetVerification: true,
},
{ status: 401 },
);
}
// Check the OTP code's expiration date
if (Date.now() > verification.expires.getTime()) {
await prisma.verificationToken.delete({
where: {
token: code,
},
});
return NextResponse.json(
{
message: "Access expired. Request new access.",
resetVerification: true,
},
{ status: 401 },
);
}
// delete the OTP code after verification
await prisma.verificationToken.delete({
where: {
token: code,
},
});
// Create a email verification token for repeat access
const token = newId("email");
hashedVerificationToken = hashToken(token);
const tokenExpiresAt = new Date();
tokenExpiresAt.setHours(tokenExpiresAt.getHours() + 23); // token expires at 23 hours
await prisma.verificationToken.create({
data: {
token: hashedVerificationToken,
identifier: `link-verification:${linkId}:${link.teamId}:${email}`,
expires: tokenExpiresAt,
},
});
isEmailVerified = true;
}
if (link.emailAuthenticated && token) {
const ipAddressValue = getClientIp(request.headers);
const { success } = await ratelimit(10, "1 m").limit(
`verify-email:${ipAddressValue}`,
);
if (!success) {
return NextResponse.json(
{ message: "Too many requests. Please try again later." },
{ status: 429 },
);
}
// Check if the long-term verification token is valid
const verification = await prisma.verificationToken.findUnique({
where: {
token: token,
identifier: `link-verification:${linkId}:${link.teamId}:${email}`,
},
});
if (!verification) {
return NextResponse.json(
{
message: "Unauthorized access. Request new access.",
resetVerification: true,
},
{ status: 401 },
);
}
// Check the long-term verification token's expiration date
if (Date.now() > verification.expires.getTime()) {
// delete the long-term verification token after verification
await prisma.verificationToken.delete({
where: {
token: token,
},
});
return NextResponse.json(
{
message: "Access expired. Request new access.",
resetVerification: true,
},
{ status: 401 },
);
}
isEmailVerified = true;
}
}
// Check if there's a valid preview session
let previewSession: PreviewSession | null = null;
if (!isPreview && previewToken) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json(
{ message: "You need to be logged in to preview the link." },
{ status: 401 },
);
}
previewSession = await verifyPreviewSession(
previewToken,
(session.user as CustomUser).id,
linkId,
);
console.log("previewSession", previewSession);
if (!previewSession) {
return NextResponse.json(
{
message: "Preview session expired or invalid. Request a new one.",
resetPreview: true,
},
{ status: 401 },
);
}
isPreview = true;
}
try {
let viewer: { id: string; verified: boolean } | null = null;
if (email && !isPreview) {
// find or create a viewer
console.time("find-viewer");
viewer = await prisma.viewer.findUnique({
where: {
teamId_email: {
teamId: link.teamId!,
email: email,
},
},
select: { id: true, verified: true },
});
console.timeEnd("find-viewer");
if (!viewer) {
console.time("create-viewer");
viewer = await prisma.viewer.create({
data: {
email: email,
verified: isEmailVerified,
teamId: link.teamId!,
},
select: { id: true, verified: true },
});
console.timeEnd("create-viewer");
} else if (!viewer.verified && isEmailVerified) {
await prisma.viewer.update({
where: { id: viewer.id },
data: { verified: isEmailVerified },
});
}
}
let newView: { id: string } | null = null;
if (!isPreview) {
console.time("create-view");
newView = await prisma.view.create({
data: {
linkId: linkId,
viewerEmail: email,
viewerName: name,
documentId: documentId,
teamId: link.teamId!,
viewerId: viewer?.id ?? undefined,
verified: isEmailVerified,
...(link.enableAgreement &&
link.agreementId &&
hasConfirmedAgreement && {
agreementResponse: {
create: {
agreementId: link.agreementId,
},
},
}),
...(customFields &&
link.customFields.length > 0 && {
customFieldResponse: {
create: {
data: link.customFields.map((field) => ({
identifier: field.identifier,
label: field.label,
response: customFields[field.identifier] || "",
})),
},
},
}),
},
select: { id: true },
});
console.timeEnd("create-view");
}
// if document version has pages, then return pages
// otherwise, return file from document version
let documentPages, documentVersion;
let sheetData;
// let documentPagesPromise, documentVersionPromise;
if (hasPages) {
const featureFlags = await getFeatureFlags({
teamId: link.teamId!,
});
const inDocumentLinks =
!link.team?.plan.includes("free") || featureFlags.inDocumentLinks;
// get pages from document version
console.time("get-pages");
documentPages = await prisma.documentPage.findMany({
where: { versionId: documentVersionId },
orderBy: { pageNumber: "asc" },
select: {
file: true,
storageType: true,
pageNumber: true,
embeddedLinks: inDocumentLinks,
pageLinks: inDocumentLinks,
metadata: true,
},
});
documentPages = await Promise.all(
documentPages.map(async (page) => {
const { storageType, ...otherPage } = page;
return {
...otherPage,
file: await getFile({ data: page.file, type: storageType }),
};
}),
);
console.timeEnd("get-pages");
} else {
// get file from document version
console.time("get-file");
documentVersion = await prisma.documentVersion.findUnique({
where: { id: documentVersionId },
select: {
file: true,
storageType: true,
type: true,
},
});
if (!documentVersion) {
return NextResponse.json(
{ message: "Document version not found." },
{ status: 404 },
);
}
if (
documentVersion.type === "pdf" ||
documentVersion.type === "image" ||
documentVersion.type === "video"
) {
documentVersion.file = await getFile({
data: documentVersion.file,
type: documentVersion.storageType,
});
}
if (documentVersion.type === "sheet") {
if (useAdvancedExcelViewer) {
if (!documentVersion.file.includes("https://")) {
// Get team-specific storage config for advanced distribution host
const storageConfig = getStorageConfig();
documentVersion.file = `https://${storageConfig.advancedDistributionHost}/${documentVersion.file}`;
}
} else {
const fileUrl = await getFile({
data: documentVersion.file,
type: documentVersion.storageType,
});
const data = await parseSheet({ fileUrl });
sheetData = data;
}
}
console.timeEnd("get-file");
}
if (newView) {
// Record view in the background to avoid blocking the response
after(
// Record link view in Tinybird
recordLinkView({
req: request,
clickId: newId("linkView"),
viewId: newView.id,
linkId,
documentId,
teamId: link.teamId!,
enableNotification: link.enableNotification,
}),
);
if (!isPreview) {
after(
notifyDocumentView({
teamId: link.teamId!,
documentId,
linkId,
viewerEmail: email ?? undefined,
viewerId: viewer?.id ?? undefined,
}).catch((error) => {
console.error("Error sending Slack notification:", error);
}),
);
}
}
const returnObject = {
message: "View recorded",
viewId: !isPreview && newView ? newView.id : undefined,
viewerId: viewer?.id ?? undefined,
isPreview: isPreview ? true : undefined,
file:
(documentVersion &&
(documentVersion.type === "pdf" ||
documentVersion.type === "image" ||
documentVersion.type === "zip" ||
documentVersion.type === "video" ||
documentVersion.type === "link")) ||
(documentVersion && useAdvancedExcelViewer)
? documentVersion.file
: undefined,
pages: documentPages ? documentPages : undefined,
sheetData:
documentVersion &&
documentVersion.type === "sheet" &&
!useAdvancedExcelViewer
? sheetData
: undefined,
fileType: documentVersion
? documentVersion.type
: documentPages
? "pdf"
: undefined,
watermarkConfig: link.enableWatermark
? link.watermarkConfig
: undefined,
ipAddress:
link.enableWatermark &&
link.watermarkConfig &&
WatermarkConfigSchema.parse(link.watermarkConfig).text.includes(
"{{ipAddress}}",
)
? getClientIp(request.headers) ?? LOCALHOST_IP
: undefined,
verificationToken: hashedVerificationToken ?? undefined,
...(isTeamMember && { isTeamMember: true }),
};
return NextResponse.json(returnObject);
} catch (error) {
log({
message: `Failed to record view for ${linkId}. \n\n ${error}`,
type: "error",
mention: true,
});
return NextResponse.json(
{ message: (error as Error).message },
{ status: 500 },
);
}
} catch (error) {
log({
message: `Failed to process request. \n\n ${error}`,
type: "error",
mention: true,
});
return NextResponse.json(
{ message: (error as Error).message },
{ status: 500 },
);
}
}