forked from bai/curriculum-project-hub
fix: enforce active organization boundary
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import type { OrganizationStatus, Prisma } from "@prisma/client";
|
||||
|
||||
export class InactiveOrganizationError extends Error {
|
||||
readonly organizationId: string;
|
||||
readonly status: Exclude<OrganizationStatus, "ACTIVE">;
|
||||
|
||||
constructor(organizationId: string, status: Exclude<OrganizationStatus, "ACTIVE">) {
|
||||
super(`organization ${organizationId} is ${status}; customer operations require ACTIVE`);
|
||||
this.name = "InactiveOrganizationError";
|
||||
this.organizationId = organizationId;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
export function inactiveOrganizationReason(
|
||||
organizationId: string,
|
||||
status: OrganizationStatus,
|
||||
): string | undefined {
|
||||
if (status === "ACTIVE") return undefined;
|
||||
return `organization ${organizationId} is ${status}; customer operations require ACTIVE`;
|
||||
}
|
||||
|
||||
export function requireActiveOrganizationStatus(
|
||||
organizationId: string,
|
||||
status: OrganizationStatus,
|
||||
): void {
|
||||
if (status !== "ACTIVE") throw new InactiveOrganizationError(organizationId, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the Organization row for a customer operation and verify ACTIVE.
|
||||
* PostgreSQL UPDATE takes a conflicting row lock, so lifecycle transitions
|
||||
* serialize with the entire transaction that called this function.
|
||||
*/
|
||||
export async function lockActiveOrganization(
|
||||
prisma: Prisma.TransactionClient,
|
||||
organizationId: string,
|
||||
): Promise<void> {
|
||||
const rows = await prisma.$queryRaw<Array<{ readonly id: string; readonly status: OrganizationStatus }>>`
|
||||
SELECT "id", "status"::text AS "status"
|
||||
FROM "Organization"
|
||||
WHERE "id" = ${organizationId}
|
||||
FOR SHARE
|
||||
`;
|
||||
const organization = rows[0];
|
||||
if (organization === undefined) throw new Error(`organization not found: ${organizationId}`);
|
||||
requireActiveOrganizationStatus(organization.id, organization.status);
|
||||
}
|
||||
|
||||
/** Resolve a Project's tenant inside the transaction, then take the lifecycle lock. */
|
||||
export async function lockActiveProjectOrganization(
|
||||
prisma: Prisma.TransactionClient,
|
||||
projectId: string,
|
||||
): Promise<string> {
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { id: projectId },
|
||||
select: { organizationId: true },
|
||||
});
|
||||
if (project === null) throw new Error(`project not found: ${projectId}`);
|
||||
await lockActiveOrganization(prisma, project.organizationId);
|
||||
return project.organizationId;
|
||||
}
|
||||
Reference in New Issue
Block a user