import { TRPCError } from "@trpc/server"; import type { dbClient } from "@kan/db/client"; import * as memberRepo from "@kan/db/repository/member.repo"; import * as permissionRepo from "@kan/db/repository/permission.repo"; import type { Permission, Role } from "@kan/shared"; import { canManageRole, getDefaultPermissions } from "@kan/shared"; /** * Get effective permissions for a member by combining role permissions with overrides */ export async function getMemberEffectivePermissions( db: dbClient, workspaceMemberId: number, roleId: number | null, roleName: string, ): Promise { let roleDefaults: Set; // Get role permissions from database or fallback to code defaults if (roleId) { const dbPermissions = await permissionRepo.getPermissionsByRoleId( db, roleId, ); roleDefaults = new Set(dbPermissions); } else { const codeDefaults = getDefaultPermissions(roleName as Role); roleDefaults = new Set([...codeDefaults]); } // Get and apply custom overrides const overrides = await permissionRepo.getMemberPermissionOverrides( db, workspaceMemberId, ); for (const override of overrides) { if (override.granted) { roleDefaults.add(override.permission as Permission); } else { roleDefaults.delete(override.permission as Permission); } } return Array.from(roleDefaults); } /** * Check if a member has a specific permission */ export async function memberHasPermission( db: dbClient, workspaceMemberId: number, roleId: number | null, roleName: string, permission: Permission, ): Promise { let hasRoleDefault: boolean; // Check role permission from database or fallback to code defaults if (roleId) { const dbPermissions = await permissionRepo.getPermissionsByRoleId( db, roleId, ); hasRoleDefault = dbPermissions.includes(permission); } else { const codeDefaults = getDefaultPermissions(roleName as Role); hasRoleDefault = codeDefaults.includes(permission); } // Check for override const override = await permissionRepo.getMemberPermissionOverride( db, workspaceMemberId, permission, ); // Override takes precedence if (override) { return override.granted; } return hasRoleDefault; } /** * Check if user has a specific permission in a workspace */ export async function hasPermission( db: dbClient, userId: string, workspaceId: number, permission: Permission, ): Promise { const member = await permissionRepo.getMemberWithRole(db, userId, workspaceId); if (!member) { return false; } return memberHasPermission( db, member.id, member.roleId, member.role, permission, ); } /** * Get all permissions for a user in a workspace */ export async function getUserPermissions( db: dbClient, userId: string, workspaceId: number, ): Promise<{ permissions: Permission[]; role: string; roleId: number | null; } | null> { const member = await permissionRepo.getMemberWithRole(db, userId, workspaceId); if (!member) { return null; } const permissions = await getMemberEffectivePermissions( db, member.id, member.roleId, member.role, ); return { permissions, role: member.role, roleId: member.roleId, }; } /** * Assert user has permission - throws FORBIDDEN if not */ export async function assertPermission( db: dbClient, userId: string, workspaceId: number, permission: Permission, ): Promise { const hasIt = await hasPermission(db, userId, workspaceId, permission); if (!hasIt) { throw new TRPCError({ message: `You do not have permission to perform this action (${permission})`, code: "FORBIDDEN", }); } } /** * Assert user can assign a specific role (based on hierarchy) */ export async function assertCanManageRole( db: dbClient, managerUserId: string, workspaceId: number, targetRoleName: string, ): Promise { const managerMember = await permissionRepo.getMemberWithRole( db, managerUserId, workspaceId, ); if (!managerMember) { throw new TRPCError({ message: "You are not a member of this workspace", code: "FORBIDDEN", }); } const managerRole = managerMember.role; if (!canManageRole(managerRole, targetRoleName as Role)) { throw new TRPCError({ message: `You cannot assign the "${targetRoleName}" role`, code: "FORBIDDEN", }); } } /** * Assert user can manage another member based on role hierarchy */ export async function assertCanManageMember( db: dbClient, managerUserId: string, workspaceId: number, targetMemberId: number, ): Promise { const managerMember = await permissionRepo.getMemberWithRole( db, managerUserId, workspaceId, ); if (!managerMember) { throw new TRPCError({ message: "You are not a member of this workspace", code: "FORBIDDEN", }); } const targetMember = await memberRepo.getById(db, targetMemberId); if (!targetMember) { throw new TRPCError({ message: "Target member not found", code: "NOT_FOUND", }); } const managerRole = managerMember.role; const targetRole = targetMember.role; if (!canManageRole(managerRole, targetRole)) { throw new TRPCError({ message: "You cannot manage this member due to role hierarchy", code: "FORBIDDEN", }); } } /** * Assert user can delete an entity - either has the delete permission OR is the creator */ export async function assertCanDelete( db: dbClient, userId: string, workspaceId: number, permission: Permission, createdBy: string | null, ): Promise { // Check if user has the general delete permission const hasDeletePermission = await hasPermission(db, userId, workspaceId, permission); // If user has permission, allow deletion if (hasDeletePermission) { return; } // If user doesn't have permission, check if they are the creator if (createdBy && createdBy === userId) { return; } // Neither condition met - deny deletion throw new TRPCError({ message: `You do not have permission to delete this entity (${permission})`, code: "FORBIDDEN", }); } /** * Assert user can edit an entity - either has the edit permission OR is the creator */ export async function assertCanEdit( db: dbClient, userId: string, workspaceId: number, permission: Permission, createdBy: string | null, ): Promise { // Check if user has the general edit permission const hasEditPermission = await hasPermission(db, userId, workspaceId, permission); // If user has permission, allow editing if (hasEditPermission) { return; } // If user doesn't have permission, check if they are the creator if (createdBy && createdBy === userId) { return; } // Neither condition met - deny editing throw new TRPCError({ message: `You do not have permission to edit this entity (${permission})`, code: "FORBIDDEN", }); }