feat(01-04): CASL permission definitions and ability factory
- Install @casl/ability for role-based access control
- Create src/lib/casl/types.ts with AppAbility, AppSubjects, AppActions types
- Create src/lib/casl/permissions.ts with permission matrix for all 5 roles
- Create src/lib/casl/ability.ts with defineAbilityFor() factory function
- Support multi-role users via additive union of permissions
- Super-admin bypasses all permission checks via can("manage", "all")
This commit is contained in:
108
src/lib/casl/permissions.ts
Normal file
108
src/lib/casl/permissions.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { AbilityBuilder, PureAbility } from "@casl/ability";
|
||||
import { Role } from "@prisma/client";
|
||||
import type { AppAbility, AppActions, AppSubjects } from "./types";
|
||||
|
||||
/**
|
||||
* Permission matrix for each role.
|
||||
*
|
||||
* Builds and returns CASL permission rules for a given role.
|
||||
* Rules are merged (additive union) for multi-role users.
|
||||
*
|
||||
* Zone/ownership filtering (e.g., Collector assigned zones) is enforced
|
||||
* at the data layer — CASL handles the coarse-grained capability check here.
|
||||
*/
|
||||
export function definePermissionsFor(
|
||||
role: Role,
|
||||
userId: string,
|
||||
tenantId: string
|
||||
): PureAbility<[AppActions, AppSubjects]> {
|
||||
const { can, cannot, build } = new AbilityBuilder<AppAbility>(PureAbility);
|
||||
|
||||
switch (role) {
|
||||
case Role.ADMIN: {
|
||||
// Full access within tenant
|
||||
can("manage", "all");
|
||||
break;
|
||||
}
|
||||
|
||||
case Role.OFFICE_STAFF: {
|
||||
// Create and manage users, assign roles
|
||||
can("manage", "User");
|
||||
// Full subscriber management
|
||||
can("manage", "Subscriber");
|
||||
// Billing management
|
||||
can("manage", "Invoice");
|
||||
// Record payments
|
||||
can("manage", "Payment");
|
||||
// Ticketing
|
||||
can("manage", "Ticket");
|
||||
// Job order management
|
||||
can("manage", "JobOrder");
|
||||
// View financial reports (read-only)
|
||||
can("read", "Report");
|
||||
// View accounting (read-only, cannot modify Chart of Accounts)
|
||||
can("read", "Account");
|
||||
// Explicitly block CoA modifications
|
||||
cannot("create", "Account");
|
||||
cannot("update", "Account");
|
||||
cannot("delete", "Account");
|
||||
break;
|
||||
}
|
||||
|
||||
case Role.COLLECTOR: {
|
||||
// Can view all subscribers for context (zone filtering done at data layer)
|
||||
can("read", "Subscriber");
|
||||
// Can record payments (zone filtering done at data layer)
|
||||
can("create", "Payment");
|
||||
// View payment history
|
||||
can("read", "Payment");
|
||||
// Explicitly blocked from billing management
|
||||
cannot("manage", "Invoice");
|
||||
// No user management
|
||||
cannot("manage", "User");
|
||||
// No report access
|
||||
cannot("manage", "Report");
|
||||
break;
|
||||
}
|
||||
|
||||
case Role.TECHNICIAN: {
|
||||
// Only their assigned jobs
|
||||
can("read", "JobOrder", { assignedToId: userId });
|
||||
can("update", "JobOrder", { assignedToId: userId });
|
||||
// Limited subscriber access for contact info (data layer enforces scope)
|
||||
can("read", "Subscriber");
|
||||
// Only inventory checked out to them
|
||||
can("read", "Inventory", { assignedToId: userId });
|
||||
// Explicitly blocked from billing, payments, and subscriber management
|
||||
cannot("manage", "Invoice");
|
||||
cannot("manage", "Payment");
|
||||
cannot("manage", "Subscriber");
|
||||
cannot("manage", "Report");
|
||||
break;
|
||||
}
|
||||
|
||||
case Role.CLIENT: {
|
||||
// Only their own invoices
|
||||
can("read", "Invoice", { subscriberId: userId });
|
||||
// Only their own payments
|
||||
can("read", "Payment", { subscriberId: userId });
|
||||
// Only their own profile
|
||||
can("read", "Subscriber", { id: userId });
|
||||
// Submit support tickets
|
||||
can("create", "Ticket");
|
||||
// Only their own tickets
|
||||
can("read", "Ticket", { submittedById: userId });
|
||||
// Blocked from user management and reports
|
||||
cannot("manage", "User");
|
||||
cannot("manage", "Report");
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// No permissions by default — safe fallback
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return build();
|
||||
}
|
||||
Reference in New Issue
Block a user