45 lines · 1.8 KB
import { all, placeholders } from "./db";
import { badRequest } from "./errors";
import type { RepoRow } from "~/lib/types";

/**
 * Who may be assigned to issues or asked for review: the owning user, collaborators and org members who can
 * read the repository. Anyone else would be spammed with notifications and shown on content they have no
 * relation to, so the filter runs server-side regardless of what the picker offered.
 */
export async function assignableUserIds(
  repo: Pick<RepoRow, "id" | "owner_id">,
  userIds: string[],
): Promise<Set<string>> {
  if (userIds.length === 0) return new Set();
  const rows = await all<{ id: string }>(
    `SELECT a.id FROM accounts a JOIN users u ON u.account_id = a.id
     WHERE a.id IN (${placeholders(userIds.length)}) AND (
       a.id = ?${userIds.length + 1}
       OR EXISTS (SELECT 1 FROM collaborators c WHERE c.repo_id = ?${userIds.length + 2} AND c.user_id = a.id)
       OR EXISTS (SELECT 1 FROM org_members m JOIN accounts o ON o.id = m.org_id
         WHERE m.org_id = ?${userIds.length + 1} AND m.user_id = a.id AND (m.role = 'owner' OR o.org_default_permission != 'none')))`,
    ...userIds,
    repo.owner_id,
    repo.id,
  );
  return new Set(rows.map((row) => row.id));
}

/** Rejects the request when any of `userIds` is not assignable, naming the offending logins. */
export async function assertAssignable(
  repo: Pick<RepoRow, "id" | "owner_id">,
  users: Array<{ id: string; name: string }>,
): Promise<void> {
  const allowed = await assignableUserIds(
    repo,
    users.map((user) => user.id),
  );
  const rejected = users.filter((user) => !allowed.has(user.id)).map((user) => user.name);
  if (rejected.length > 0) {
    throw badRequest(