src/lib/auth/scope.ts
Linked repository file from this build.
import type { Actor, DemoRole } from "./actor";
export class AccessError extends Error {
constructor(
message = "You do not have access to this review.",
public status = 403,
) {
super(message);
}
}
/** Every role-only permission. Rules that also depend on review state live in commands.ts. */
const capabilities = {
import: ["program_manager", "admin", "statistics"],
report: ["program_manager", "admin", "statistics"],
applyRules: ["admin", "statistics"],
schedule: ["examiner", "supervisor"],
export: ["statistics", "supervisor", "program_manager"],
exportEpp: ["supervisor", "program_manager"],
approvalQueue: ["supervisor", "program_manager", "pec"],
uploadDocuments: ["examiner", "supervisor", "program_manager", "admin"],
addNotes: ["examiner", "supervisor", "program_manager", "pec"],
seniorReview: ["supervisor", "program_manager"],
viewProgram: ["program_manager", "pec", "admin", "statistics"],
} as const satisfies Record<string, readonly DemoRole[]>;
export type Capability = keyof typeof capabilities;
export function can(actor: { role: DemoRole }, capability: Capability): boolean {
return (capabilities[capability] as readonly DemoRole[]).includes(actor.role);
}
/**
* Which examiners' reviews an actor can open inside their program.
* canAccessReview and the SQL reviewScope both use this, so lists and pages agree.
*/
export function examinerScope(actor: Actor): "program" | string[] {
// The schema requires both, but a blank value must never widen access.
if (!actor.staffId || !actor.program) return [];
if (can(actor, "viewProgram")) return "program";
if (actor.role === "supervisor") return [actor.staffId, ...(actor.subordinateStaffIds ?? [])];
return [actor.staffId];
}
type ReviewScope = { examiner: string; program: string };
export function canAccessReview(actor: Actor, review: ReviewScope): boolean {
if (actor.program !== review.program) return false;
const scope = examinerScope(actor);
return scope === "program" || scope.includes(review.examiner);
}
export function assertReviewAccess(actor: Actor, review: ReviewScope) {
if (!canAccessReview(actor, review)) throw new AccessError();
}