43 lines · 1.8 KB
import { createMiddleware } from "@tanstack/react-start";
import { notFound } from "@tanstack/react-router";
import { sessionViewer } from "~/server/auth";
import { HttpError, unauthorized } from "~/server/errors";
import { clientSafeError } from "~/server/client-safe-error";
import type { Viewer } from "~/lib/types";
import { paymentErrorFor } from "~/server/metering/access";
import { QuotaExceededError } from "~/server/metering/quota";

const HTTP_NOT_FOUND = 404;

/** Resolves the session viewer for every server function and maps 404s onto router not-found handling. */
export const viewerMiddleware = createMiddleware({ type: "function" }).server(async ({ next }) => {
  const viewer = await sessionViewer();
  try {
    return await next({ context: { viewer } });
  } catch (error) {
    if (error instanceof HttpError && error.status === HTTP_NOT_FOUND) throw notFound();
    const http = error instanceof QuotaExceededError ? error.toHttpError() : error;
    if (http instanceof HttpError) throw await paymentErrorFor(http, viewer?.id ?? null);
    throw http;
  }
});

export function requireViewer(viewer: Viewer | null): Viewer {
  if (!viewer) throw unauthorized("You must be signed in");
  return viewer;
}

/**
 * Registered globally in start.ts: a quota error from a server function that skips viewerMiddleware still
 * reaches the client as a 402 (not a masked 500), with billing context for whoever is signed in, and
 * unexpected errors are masked by `clientSafeError`.
 */
export const paymentErrorMiddleware = createMiddleware({ type: "function" }).server(async ({ next }) => {
  try {
    return await next();
  } catch (error) {
    if (!(error instanceof QuotaExceededError)) throw clientSafeError(error);
    throw await paymentErrorFor(error.toHttpError(), (await sessionViewer())?.id ?? null);