42 lines · 1.7 KB
import { env } from "cloudflare:workers";
import { first } from "./db";
import type { UserRow } from "~/lib/types";

const USER_SELECT = `SELECT a.*, u.email, u.password_hash, u.github_id, u.is_admin,
  u.email_verified_at, u.totp_enabled_at, u.sessions_revoked_at
  FROM accounts a JOIN users u ON u.account_id = a.id`;

export async function loadUserById(id: string): Promise<UserRow | null> {
  return first<UserRow>(`${USER_SELECT} WHERE a.id = ?`, id);
}

export async function loadActiveUserById(id: string): Promise<UserRow | null> {
  return first<UserRow>(`${USER_SELECT} WHERE a.id = ? AND a.disabled_at IS NULL`, id);
}

export async function loadUserByLogin(login: string): Promise<UserRow | null> {
  return first<UserRow>(`${USER_SELECT} WHERE a.name = ? OR u.email = ?`, login, login);
}

/**
 * The per-request viewer lookup, answered by the user's AccountState from a short-lived copy of the directory
 * row instead of a D1 read per page view. Writers that change the row call `userChanged`.
 */
export async function loadActiveUserCached(id: string): Promise<UserRow | null> {
  try {
    return (await env.ACCOUNT_STATE.get(env.ACCOUNT_STATE.idFromName(id)).cachedUser(id)) as UserRow | null;
  } catch (error) {
    console.error("Cached user lookup failed; reading the directory", id, error);
    return loadActiveUserById(id);
  }
}

/** Drops the cached directory row after a write to `users` / `accounts` that the viewer must see at once. */
export async function userChanged(id: string): Promise<void> {
  try {
    await env.ACCOUNT_STATE.get(env.ACCOUNT_STATE.idFromName(id)).forgetUser();
  } catch (error) {
    console.error("User cache invalidation failed", id, error);
  }