import { eq, and, inArray } from 'drizzle-orm' import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js' import { permissions, roles, rolePermissions, userRoles } from '../db/schema/rbac.js' import { SYSTEM_PERMISSIONS, DEFAULT_ROLES } from '../db/seeds/rbac.js' import { ForbiddenError } from '../lib/errors.js' export const RbacService = { /** Seed system permissions (global, run once) */ async seedPermissions(db: PostgresJsDatabase) { const existing = await db.select({ slug: permissions.slug }).from(permissions) const existingSlugs = new Set(existing.map((p) => p.slug)) const toInsert = SYSTEM_PERMISSIONS.filter((p) => !existingSlugs.has(p.slug)) if (toInsert.length === 0) return await db.insert(permissions).values(toInsert) }, /** Seed default roles for a company */ async seedRolesForCompany(db: PostgresJsDatabase, companyId: string) { const existingRoles = await db .select({ slug: roles.slug }) .from(roles) .where(and(eq(roles.companyId, companyId), eq(roles.isSystem, true))) if (existingRoles.length > 0) return // already seeded // Get all permission records for slug → id mapping const allPerms = await db.select().from(permissions) const permMap = new Map(allPerms.map((p) => [p.slug, p.id])) for (const roleDef of DEFAULT_ROLES) { const [role] = await db .insert(roles) .values({ companyId, name: roleDef.name, slug: roleDef.slug, description: roleDef.description, isSystem: true, }) .returning() const permIds = roleDef.permissions .map((slug) => permMap.get(slug)) .filter((id): id is string => id !== undefined) if (permIds.length > 0) { await db.insert(rolePermissions).values( permIds.map((permissionId) => ({ roleId: role.id, permissionId, })), ) } } }, /** Get all permissions for a user (union of all role permissions) */ async getUserPermissions(db: PostgresJsDatabase, userId: string): Promise { const userRoleRecords = await db .select({ roleId: userRoles.roleId }) .from(userRoles) .where(eq(userRoles.userId, userId)) if (userRoleRecords.length === 0) return [] const roleIds = userRoleRecords.map((r) => r.roleId) const rpRecords = await db .select({ permissionId: rolePermissions.permissionId }) .from(rolePermissions) .where(inArray(rolePermissions.roleId, roleIds)) if (rpRecords.length === 0) return [] const permIds = [...new Set(rpRecords.map((r) => r.permissionId))] const permRecords = await db .select({ slug: permissions.slug }) .from(permissions) .where(inArray(permissions.id, permIds)) return permRecords.map((p) => p.slug) }, /** List all permissions */ async listPermissions(db: PostgresJsDatabase) { return db.select().from(permissions).orderBy(permissions.domain, permissions.action) }, /** List roles for a company */ async listRoles(db: PostgresJsDatabase, companyId: string) { return db .select() .from(roles) .where(and(eq(roles.companyId, companyId), eq(roles.isActive, true))) .orderBy(roles.name) }, /** Get role with its permissions */ async getRoleWithPermissions(db: PostgresJsDatabase, companyId: string, roleId: string) { const [role] = await db .select() .from(roles) .where(and(eq(roles.id, roleId), eq(roles.companyId, companyId))) .limit(1) if (!role) return null const rp = await db .select({ permissionId: rolePermissions.permissionId, slug: permissions.slug }) .from(rolePermissions) .innerJoin(permissions, eq(rolePermissions.permissionId, permissions.id)) .where(eq(rolePermissions.roleId, roleId)) return { ...role, permissions: rp.map((r) => r.slug) } }, /** Create a custom role */ async createRole( db: PostgresJsDatabase, companyId: string, input: { name: string; slug: string; description?: string; permissionSlugs: string[] }, ) { const [role] = await db .insert(roles) .values({ companyId, name: input.name, slug: input.slug, description: input.description, isSystem: false, }) .returning() await this.setRolePermissions(db, role.id, input.permissionSlugs) return this.getRoleWithPermissions(db, companyId, role.id) }, /** Update role permissions (replace all) */ async setRolePermissions(db: PostgresJsDatabase, roleId: string, permissionSlugs: string[]) { // Delete existing await db.delete(rolePermissions).where(eq(rolePermissions.roleId, roleId)) if (permissionSlugs.length === 0) return // Get permission IDs const perms = await db .select({ id: permissions.id, slug: permissions.slug }) .from(permissions) .where(inArray(permissions.slug, permissionSlugs)) if (perms.length > 0) { await db.insert(rolePermissions).values( perms.map((p) => ({ roleId, permissionId: p.id })), ) } }, /** Update a role */ async updateRole( db: PostgresJsDatabase, companyId: string, roleId: string, input: { name?: string; description?: string; permissionSlugs?: string[] }, ) { if (input.name || input.description) { await db .update(roles) .set({ ...(input.name ? { name: input.name } : {}), ...(input.description !== undefined ? { description: input.description } : {}), updatedAt: new Date(), }) .where(and(eq(roles.id, roleId), eq(roles.companyId, companyId))) } if (input.permissionSlugs) { await this.setRolePermissions(db, roleId, input.permissionSlugs) } return this.getRoleWithPermissions(db, companyId, roleId) }, /** Delete a custom role */ async deleteRole(db: PostgresJsDatabase, companyId: string, roleId: string) { const [role] = await db .select() .from(roles) .where(and(eq(roles.id, roleId), eq(roles.companyId, companyId))) .limit(1) if (!role) return null if (role.isSystem) throw new ForbiddenError('Cannot delete a system role') // Remove role permissions and user assignments await db.delete(rolePermissions).where(eq(rolePermissions.roleId, roleId)) await db.delete(userRoles).where(eq(userRoles.roleId, roleId)) const [deleted] = await db .delete(roles) .where(eq(roles.id, roleId)) .returning() return deleted }, /** Assign a role to a user */ async assignRole(db: PostgresJsDatabase, userId: string, roleId: string, assignedBy?: string) { const [existing] = await db .select() .from(userRoles) .where(and(eq(userRoles.userId, userId), eq(userRoles.roleId, roleId))) .limit(1) if (existing) return existing // already assigned const [assignment] = await db .insert(userRoles) .values({ userId, roleId, assignedBy }) .returning() return assignment }, /** Remove a role from a user */ async removeRole(db: PostgresJsDatabase, userId: string, roleId: string) { const [removed] = await db .delete(userRoles) .where(and(eq(userRoles.userId, userId), eq(userRoles.roleId, roleId))) .returning() return removed ?? null }, /** Get roles assigned to a user */ async getUserRoles(db: PostgresJsDatabase, userId: string) { return db .select({ id: roles.id, name: roles.name, slug: roles.slug, isSystem: roles.isSystem, }) .from(userRoles) .innerJoin(roles, eq(userRoles.roleId, roles.id)) .where(eq(userRoles.userId, userId)) }, }