138 lines · 6.2 KB
import { first, run } from "./db";
import { hashPassword, randomToken, verifyPassword } from "./crypto";
import { consumeAuthToken, revokeUserAuthTokens, storeAuthToken } from "./auth-tokens";
import { sessionCreatedAt } from "./auth";
import { enforceRateLimit } from "./limits";
import { emailTemplates, queueEmail } from "./email";
import { publicUrl } from "./events/emit";
import { badRequest, conflict } from "./errors";
import { invalidateUserSignatures } from "./security/signatures";

const VERIFY_TTL_SECONDS = 60 * 60 * 24;
const RESET_TTL_SECONDS = 60 * 60;

type VerifyPayload = { email: string };
type ResetPayload = { passwordHash: string | null };

function parseVerify(raw: string): VerifyPayload {
  const parsed: unknown = JSON.parse(raw);
  const email = typeof parsed === "object" && parsed !== null && "email" in parsed ? parsed.email : null;
  if (typeof email !== "string") throw badRequest("This verification link is invalid");
  return { email };
}

function parseReset(raw: string): ResetPayload {
  const parsed: unknown = JSON.parse(raw);
  const hash = typeof parsed === "object" && parsed !== null && "passwordHash" in parsed ? parsed.passwordHash : null;
  return { passwordHash: typeof hash === "string" ? hash : null };
}

async function displayNameFor(userId: string): Promise<string> {
  return (await first<{ name: string }>("SELECT name FROM accounts WHERE id = ?", userId))?.name ?? "there";
}

/** Sends a single-use verification link; confirming it sets (or changes to) this address. */
export async function sendEmailVerification(userId: string, email: string): Promise<void> {
  const token = randomToken("verify");
  await revokeUserAuthTokens("email-verify", userId);
  const payload: VerifyPayload = { email };
  await storeAuthToken("email-verify", token, userId, JSON.stringify(payload), VERIFY_TTL_SECONDS);
  const url = publicUrl(`/verify-email?token=${encodeURIComponent(token)}`);