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

135 lines
3.1 KiB
TypeScript

import { Metadata } from "next";
import { redirect } from "next/navigation";
import NotFound from "@/pages/404";
import { VerificationToken } from "@prisma/client";
import { after } from "@/lib/after";
import { hashToken } from "@/lib/api/auth/token";
import prisma from "@/lib/prisma";
import { kv } from "@/lib/kv";
import { sendEmail, subscribe, unsubscribe } from "@/lib/resend";
import { CustomUser } from "@/lib/types";
import EmailUpdated from "@/components/emails/email-updated";
import ConfirmEmailChangePageClient from "./page-client";
import { getSession } from "./utils";
export const runtime = "nodejs";
const data = {
description: "Confirm email change",
title: "Confirm email change | Hanzo Dataroom",
url: "/auth/confirm-email-change",
};
export const metadata: Metadata = {
metadataBase: new URL("https://dataroom.hanzo.ai"),
title: data.title,
description: data.description,
openGraph: {
title: data.title,
description: data.description,
url: data.url,
siteName: "Hanzo Dataroom",
images: [
{
url: "/_static/meta-image.png",
width: 800,
height: 600,
},
],
locale: "en_US",
type: "website",
},
twitter: {
card: "summary_large_image",
title: data.title,
description: data.description,
creator: "@hanzoai",
images: ["/_static/meta-image.png"],
},
};
interface PageProps {
params: Promise<{ token: string }>;
}
export default async function ConfirmEmailChangePage(props: PageProps) {
return (
<div className="flex flex-col items-center justify-center gap-6 text-center">
<VerifyEmailChange {...props} />
</div>
);
}
const VerifyEmailChange = async ({ params }: PageProps) => {
const { token } = await params;
const tokenFound = await prisma.verificationToken.findUnique({
where: {
token: hashToken(token),
},
});
if (!tokenFound || tokenFound.expires < new Date()) return <NotFound />;
const session = await getSession();
if (!session) {
redirect(`/login?next=/auth/confirm-email-change/${token}`);
}
const currentUserId = (session.user as CustomUser).id;
const data = await kv.get<{ email: string; newEmail: string }>(
`email-change-request:user:${currentUserId}`,
);
if (!data) return <NotFound />;
await unsubscribe(data.email);
await prisma.user.update({
where: {
id: currentUserId,
},
data: {
email: data.newEmail,
},
});
after(
Promise.all([
deleteRequest(tokenFound),
subscribe(data.newEmail),
sendEmail({
to: data.email,
subject: "Your email address has been changed",
system: true,
react: EmailUpdated({
oldEmail: data.email,
newEmail: data.newEmail,
}),
test: process.env.NODE_ENV === "development",
}),
]),
);
return <ConfirmEmailChangePageClient />;
};
const deleteRequest = async (tokenFound: VerificationToken) => {
await Promise.all([
prisma.verificationToken.delete({
where: {
token: tokenFound.token,
},
}),
kv.del(`email-change-request:user:${tokenFound.identifier}`),
]);
};