mirror of
https://github.com/hanzo-apps/dataroom.git
synced 2026-10-30 14:47:36 +00:00
`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]>
99 lines
2.7 KiB
TypeScript
99 lines
2.7 KiB
TypeScript
import { after } from "@/lib/after";
|
|
import { customAlphabet } from "nanoid";
|
|
|
|
import { kv } from "@/lib/kv";
|
|
import { sendEmail } from "@/lib/resend";
|
|
|
|
import VerificationCodeEmail from "@/components/emails/verification-link";
|
|
|
|
// Generate a 10-character uppercase alphanumeric verification code (like Linear's style)
|
|
const generateVerificationCode = customAlphabet(
|
|
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
10,
|
|
);
|
|
|
|
// KV key prefixes for login codes
|
|
const LOGIN_CODE_PREFIX = "login_code:";
|
|
const LOGIN_CODE_EMAIL_PREFIX = "login_code:email:";
|
|
// Token expiration time in seconds (15 minutes)
|
|
const TOKEN_EXPIRATION_SECONDS = 15 * 60;
|
|
|
|
export interface LoginCodeData {
|
|
email: string;
|
|
code: string;
|
|
callbackUrl: string;
|
|
createdAt: number;
|
|
}
|
|
|
|
export const sendVerificationRequestEmail = async (params: {
|
|
email: string;
|
|
url: string;
|
|
}) => {
|
|
const { url, email } = params;
|
|
|
|
// Generate verification code
|
|
const code = generateVerificationCode();
|
|
|
|
// Store the login data in KV with 15-minute TTL
|
|
const loginCodeData: LoginCodeData = {
|
|
email,
|
|
code,
|
|
callbackUrl: url,
|
|
createdAt: Date.now(),
|
|
};
|
|
|
|
// Store with email:code as key for lookup (must complete before redirecting)
|
|
await kv.set(
|
|
`${LOGIN_CODE_EMAIL_PREFIX}${email.toLowerCase()}:${code}`,
|
|
JSON.stringify(loginCodeData),
|
|
{ ex: TOKEN_EXPIRATION_SECONDS },
|
|
);
|
|
|
|
const emailTemplate = VerificationCodeEmail({
|
|
email,
|
|
code,
|
|
});
|
|
|
|
// Send in the background so the response is not held on SMTP
|
|
// This keeps the serverless function alive until the email is sent
|
|
after(
|
|
sendEmail({
|
|
to: email as string,
|
|
system: true,
|
|
subject: "Login for Hanzo Dataroom",
|
|
react: emailTemplate,
|
|
test: process.env.NODE_ENV === "development",
|
|
}).catch((e) => {
|
|
console.error("Failed to send verification email:", e);
|
|
}),
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Atomically fetch and delete login code data from KV
|
|
* Uses GETDEL to prevent TOCTOU race conditions where the same code could be used twice
|
|
* Returns null if not found or expired
|
|
*/
|
|
export const fetchAndDeleteLoginCodeData = async (
|
|
email: string,
|
|
code: string,
|
|
): Promise<LoginCodeData | null> => {
|
|
try {
|
|
const key = `${LOGIN_CODE_EMAIL_PREFIX}${email.toLowerCase()}:${code.toUpperCase()}`;
|
|
|
|
// Use GETDEL for atomic get-and-delete operation
|
|
// This prevents race conditions where two requests could use the same code
|
|
const data = await kv.getdel(key);
|
|
if (!data) return null;
|
|
|
|
// Handle both string and already-parsed object (KV client behavior)
|
|
if (typeof data === "string") {
|
|
return JSON.parse(data) as LoginCodeData;
|
|
}
|
|
return data as LoginCodeData;
|
|
} catch (error) {
|
|
console.error("Error fetching and deleting login code data:", error);
|
|
return null;
|
|
}
|
|
};
|