/** * TASK-0133 — the shared ORG-ADMIN gate. * * `OrganizationMember.role` reuses the `WorkspaceRole` enum (OWNER/ADMIN/ * MEMBER/GUEST); the gate is `role ∈ {OWNER, ADMIN}`. Throws * `403 { reason: "requires_org_admin" }` otherwise (the org exists — a * non-member is indistinguishable from a MEMBER to avoid enumeration). * * A module-AGNOSTIC free function (takes a Prisma client, no Nest DI) so it * can be used from any service WITHOUT a module cycle — the same pattern as * `sso/enforcement.ts` and `lib/authorize.ts::assertRole` (which it mirrors * for org membership instead of workspace membership). * * API-key callers are rejected by the controller (`requireUser`) — org * admin is a USER role, never an API-key scope. */ import type { PrismaClient } from "@prisma/client"; import { ApiError } from "../../common/errors.js"; export type OrgAdminRole = "OWNER" | "ADMIN"; export async function assertOrgAdmin( prisma: Pick, orgId: string, userId: string ): Promise { const org = await prisma.organization.findUnique({ where: { id: orgId } }); if (!org) throw ApiError.notFound("Organization not found."); const m = await prisma.organizationMember.findUnique({ where: { organizationId_userId: { organizationId: orgId, userId } } }); if (!m || (m.role !== "OWNER" && m.role !== "ADMIN")) { throw ApiError.forbidden("Organization admin required.", [{ reason: "requires_org_admin" }]); } return m.role; }