import { all, chunks, first, placeholders } from "./db"; import { forbidden, notFoundError } from "./errors"; import type { Permission, RepoRow, RepoSummary, Viewer } from "~/lib/types"; const PERMISSION_RANK: Record = { none: 0, read: 1, write: 2, admin: 3 }; export const REPO_SELECT = `SELECT r.*, a.name AS owner_name FROM repos r JOIN accounts a ON a.id = r.owner_id`; export async function loadRepo(owner: string, name: string): Promise { return first(`${REPO_SELECT} WHERE a.name = ? AND r.name = ?`, owner, name); } export async function loadRepoById(id: string): Promise { return first(`${REPO_SELECT} WHERE r.id = ?`, id); } export async function loadRepoByArtifactsName(artifactsName: string): Promise { return first(`${REPO_SELECT} WHERE r.artifacts_name = ?`, artifactsName); } export function hasPermission(actual: Permission, needed: Permission): boolean { return PERMISSION_RANK[actual] >= PERMISSION_RANK[needed]; } export async function repoPermission(repo: RepoRow, viewer: Viewer | null): Promise { const base: Permission = repo.visibility === "public" ? "read" : "none"; if (!viewer) return base; if (viewer.isAdmin || repo.owner_id === viewer.id) return "admin"; const [membership, collaborator] = await Promise.all([ first<{ role: "owner" | "member" }>( "SELECT role FROM org_members WHERE org_id = ? AND user_id = ?", repo.owner_id, viewer.id, ), first<{ permission: Permission }>( "SELECT permission FROM collaborators WHERE repo_id = ? AND user_id = ?", repo.id, viewer.id, ), ]); const candidates: Permission[] = [base]; if (membership) candidates.push(membership.role === "owner" ? "admin" : "write"); if (collaborator) candidates.push(collaborator.permission); return candidates.reduce((best, next) => (PERMISSION_RANK[next] > PERMISSION_RANK[best] ? next : best), "none"); } export type RepoAccess = { repo: RepoRow; permission: Permission }; export async function requireRepoAccess( owner: string, name: string, viewer: Viewer | null, needed: Permission, ): Promise { const repo = await loadRepo(owner, name); if (!repo) throw notFoundError("Repository not found"); const permission = await repoPermission(repo, viewer); if (permission === "none") throw notFoundError("Repository not found"); if (!hasPermission(permission, needed)) throw forbidden(); return { repo, permission }; } export async function canAdministerAccount(accountId: string, viewer: Viewer | null): Promise { if (!viewer) return false; if (viewer.isAdmin || viewer.id === accountId) return true; const membership = await first<{ role: string }>( "SELECT role FROM org_members WHERE org_id = ? AND user_id = ?", accountId, viewer.id, ); return membership?.role === "owner"; } export async function canCreateInAccount(accountId: string, viewer: Viewer | null): Promise { if (!viewer) return false; if (viewer.isAdmin || viewer.id === accountId) return true; const membership = await first<{ role: string }>( "SELECT role FROM org_members WHERE org_id = ? AND user_id = ?", accountId, viewer.id, ); return membership !== null; } export async function toRepoSummaries(rows: RepoRow[]): Promise { const forkIds = [...new Set(rows.map((row) => row.fork_of_id).filter((id): id is string => id !== null))]; const parents = new Map(); for (const part of chunks(forkIds)) { const parentRows = await all<{ id: string; name: string; owner_name: string }>( `SELECT r.id, r.name, a.name AS owner_name FROM repos r JOIN accounts a ON a.id = r.owner_id WHERE r.id IN (${placeholders(part.length)})`, ...part, ); for (const parent of parentRows) { parents.set(parent.id, { owner: parent.owner_name, name: parent.name }); } } return rows.map((row) => toRepoSummary(row, row.fork_of_id ? (parents.get(row.fork_of_id) ?? null) : null)); } export function toRepoSummary(row: RepoRow, forkOf: { owner: string; name: string } | null): RepoSummary { return { id: row.id, owner: row.owner_name, name: row.name, description: row.description, visibility: row.visibility, defaultBranch: row.default_branch, status: row.status, forkOf, isArchived: row.is_archived === 1, hasIssues: row.has_issues === 1, hasActions: row.has_actions === 1, starsCount: row.stars_count, forksCount: row.forks_count, openIssuesCount: row.open_issues_count, openPullsCount: row.open_pulls_count, pushedAt: row.pushed_at, createdAt: row.created_at, }; } const VISIBLE_REPOS_CLAUSE = `(r.visibility = 'public' OR r.owner_id = ?1 OR EXISTS (SELECT 1 FROM collaborators c WHERE c.repo_id = r.id AND c.user_id = ?1) OR EXISTS (SELECT 1 FROM org_members m WHERE m.org_id = r.owner_id AND m.user_id = ?1))`; export function visibleReposClause(viewer: Viewer | null): { clause: string; viewerId: string } { if (viewer?.isAdmin) return { clause: "(?1 = ?1)", viewerId: viewer.id }; return { clause: VISIBLE_REPOS_CLAUSE, viewerId: viewer?.id ?? "" }; }