Vectrum
Sign In
80 lines · 2.8 KB · TypeScript
import "server-only";
import { cache } from "react";
import { headers } from "next/headers";
import { forbidden, notFound } from "next/navigation";
import { getViewer } from "./auth";
import { defaultBranch, isBranch, normalizeRepoName, resolveRefAndPath } from "./git";
import { canWrite, findRepo, repoAccess, type RepoRecord } from "./repos";
import { sshCloneUrl } from "./ssh-config";

/**
 * Loads a repository for a page. Repositories the viewer can't read render the
 * 404 page, so private repositories don't reveal that they exist.
 */
export const getRepo = cache(async (owner: string, repoParam: string) => {
  const record = findRepo(owner, normalizeRepoName(repoParam));
  if (!record) notFound();
  const viewer = await getViewer();
  const access = repoAccess(record, viewer?.id);
  if (!access) notFound();
  return {
    ...record,
    viewer,
    access,
    canWrite: canWrite(access),
    isAdmin: access === "admin",
    defaultBranch: await defaultBranch(record.dir),
    base: `/${record.owner}/${record.name}`,
  };
});

export type Repo = Awaited<ReturnType<typeof getRepo>>;

/** Like getRepo, but also requires push access. */
export async function getWritableRepo(owner: string, repoParam: string) {
  const repo = await getRepo(owner, repoParam);
  if (!repo.canWrite) forbidden();
  return repo;
}

/** Resolves catch-all URL segments into a ref and a path, or renders the 404 page. */
export async function getRefAndPath(repo: RepoRecord, segments: string[]) {
  const resolved = await resolveRefAndPath(repo.dir, segments);
  if (!resolved) notFound();
  return resolved;
}

/**
 * For editor routes: the first segments must name an existing branch, except in
 * an empty repository where the (future) branch name is taken as-is.
 */
export async function getBranchAndPath(repo: Repo, segments: string[]) {
  if (!repo.defaultBranch) {
    const [branch = "main", ...rest] = segments;
    return { branch, path: rest.join("/"), exists: false };
  }
  const resolved = await getRefAndPath(repo, segments);
  if (!(await isBranch(repo.dir, resolved.ref))) notFound();
  return { branch: resolved.ref, path: resolved.path, exists: true };
}

async function requestHost() {
  const h = await headers();
  return { host: h.get("x-forwarded-host") ?? h.get("host") ?? "localhost:3000", proto: h.get("x-forwarded-proto") ?? "http" };
}

export async function cloneUrls(repo: RepoRecord) {
  const { host, proto } = await requestHost();
  return {
    https: `${proto}://${host}/${repo.owner}/${repo.name}.git`,
    ssh: sshCloneUrl(host, `${repo.owner}/${repo.name}.git`),
  };
}

/** The repository's container image, e.g. vectrum.cc/alice/app. Image names are lowercase. */
export async function imageName(repo: RepoRecord) {
  const { host } = await requestHost();
  return `${host}/${repo.owner}/${repo.name}`.toLowerCase();
}

export type { RepoRecord };