import { AbilityBuilder, createMongoAbility, MongoQuery } from "@casl/ability"; import { Role } from "@prisma/client"; import type { AppAbility, AppActions, AppSubjects } from "./types"; /** * Permission matrix for each role. * * Builds and returns a CASL MongoAbility for a given role. * Uses createMongoAbility which includes built-in conditions matching, * supporting condition-based rules like { assignedToId: userId }. * * Rules are merged (additive union) for multi-role users via defineAbilityFor(). * * Zone/ownership filtering (e.g., Collector assigned zones) is enforced * at the data layer — CASL handles the coarse-grained capability check here. * * Note on conditions: Subjects are currently string literals (no Prisma models yet). * Conditions are cast via `as MongoQuery` to bypass CASL's strict field inference. * Once Prisma models are defined (Phase 2+), subjects can be replaced with class types * for full type-safe condition checking. */ export function definePermissionsFor( role: Role, userId: string, tenantId: string ): AppAbility { const { can, cannot, build } = new AbilityBuilder( createMongoAbility ); // Helper to cast condition objects — required because subjects are string literals // (no Prisma model types yet). CASL infers MongoQuery for string subjects, // so we cast through unknown. Conditions are enforced at runtime by CASL's MongoDB // query matcher. Type-safe conditions will be added when Prisma models are defined. // eslint-disable-next-line @typescript-eslint/no-explicit-any const cond = (obj: Record) => obj as unknown as any; 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"); // Zone management (assign subscribers and collectors to zones) can("manage", "Zone"); // Ticketing can("manage", "Ticket"); // Job order management can("manage", "JobOrder"); // Technician profile management (read/update, not create — admin only for compensation config) can("read", "TechnicianProfile"); can("update", "TechnicianProfile"); // Inventory management can("manage", "Inventory"); // Expense management (record, list, view) can("manage", "Expense"); // Vendor management (CRUD) can("manage", "Vendor"); // Collection and remittance management can("manage", "Collection"); can("manage", "Remittance"); // Job type rates (read-only for office staff — admin configures rates) can("read", "JobTypeRate"); // 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 read zones they are assigned to (data layer enforces which zones) can("read", "Zone"); // Can record payments (zone filtering done at data layer) can("create", "Payment"); // View payment history can("read", "Payment"); // Can create and view collections can("create", "Collection"); can("read", "Collection"); // Can create and view remittances can("create", "Remittance"); can("read", "Remittance"); // NOTE: No explicit cannot() needed — Collector simply has no rules for // Invoice, User management, or Reports. Absence of a rule = no access. break; } case Role.TECHNICIAN: { // Only their assigned jobs (conditions enforced at data layer too) can("read", "JobOrder", cond({ assignedToId: userId })); can("update", "JobOrder", cond({ assignedToId: userId })); // Limited subscriber access for contact info (data layer enforces scope) can("read", "Subscriber"); // Only inventory checked out to them can("read", "Inventory", cond({ assignedToId: userId })); // NOTE: No cannot() needed — Technician simply has no billing/payment rules. // Absence of a rule = no access for Invoice, Payment, Report, User management. break; } case Role.CLIENT: { // Only their own invoices (scoped by conditions) can("read", "Invoice", cond({ subscriberId: userId })); // Only their own payments can("read", "Payment", cond({ subscriberId: userId })); // Only their own profile can("read", "Subscriber", cond({ id: userId })); // Submit support tickets can("create", "Ticket"); // Only their own tickets can("read", "Ticket", cond({ submittedById: userId })); // NOTE: No cannot() needed — Client simply has no rules for User management or Reports. break; } default: { // No permissions by default — safe fallback break; } } return build(); }