commentAuth.ts
| 1 | import { db } from "../db/index.ts"; |
| 2 | import type { SessionUser } from "../middleware/session.ts"; |
| 3 | |
| 4 | /** |
| 5 | * Authorise an edit on an issue or patch comment. |
| 6 | * |
| 7 | * Returns a Response if the request must be denied, or null if it may proceed. |
| 8 | * |
| 9 | * Verifies, in one query, that: |
| 10 | * 1. the comment exists, and |
| 11 | * 2. its parent issue/patch belongs to the requested repo (prevents |
| 12 | * cross-repo bypasses where the URL repo and the comment's repo differ), |
| 13 | * 3. the user is the comment author or an admin, |
| 14 | * 4. the parent issue/patch is open (admins may edit on closed parents). |
| 15 | */ |
| 16 | export async function authorizeCommentEdit( |
| 17 | kind: "issue" | "patch", |
| 18 | commentId: number, |
| 19 | repoId: number, |
| 20 | user: SessionUser | null, |
| 21 | ): Promise<Response | null> { |
| 22 | if (!user) return new Response("Unauthorized", { status: 401 }); |
| 23 | |
| 24 | const row = |
| 25 | kind === "issue" |
| 26 | ? await db |
| 27 | .selectFrom("issue_comments") |
| 28 | .innerJoin("issues", "issues.id", "issue_comments.issue_id") |
| 29 | .select(["issue_comments.author_id", "issues.status"]) |
| 30 | .where("issue_comments.id", "=", commentId) |
| 31 | .where("issues.repo_id", "=", repoId) |
| 32 | .executeTakeFirst() |
| 33 | : await db |
| 34 | .selectFrom("patch_comments") |
| 35 | .innerJoin("patches", "patches.id", "patch_comments.patch_id") |
| 36 | .select(["patch_comments.author_id", "patches.status"]) |
| 37 | .where("patch_comments.id", "=", commentId) |
| 38 | .where("patches.repo_id", "=", repoId) |
| 39 | .executeTakeFirst(); |
| 40 | |
| 41 | if (!row) return new Response("Not found", { status: 404 }); |
| 42 | if (row.author_id !== user.id && !user.isAdmin) |
| 43 | return new Response("Forbidden", { status: 403 }); |
| 44 | if (row.status !== "open" && !user.isAdmin) |
| 45 | return new Response("Forbidden", { status: 403 }); |
| 46 | return null; |
| 47 | } |
| 48 |