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, userIds: string[], ): Promise> { 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, users: Array<{ id: string; name: string }>, ): Promise { 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( `Only collaborators and organization members can be assigned or requested: ${rejected.join(", ")}`, ); } }