|
|
@ -1,9 +1,5 @@ |
|
|
|
// TODO: implement ABAC
|
|
|
|
|
|
|
|
export const actions = { |
|
|
|
ADMIN: 'ADMIN', |
|
|
|
CLIENT: 'CLIENT', |
|
|
|
} as const; |
|
|
|
import type { ClientType } from '#db/repositories/client/types'; |
|
|
|
import type { UserType } from '#db/repositories/user/types'; |
|
|
|
|
|
|
|
export type Role = number & { readonly __role: unique symbol }; |
|
|
|
|
|
|
@ -12,20 +8,105 @@ export const roles = { |
|
|
|
CLIENT: 2 as Role, |
|
|
|
} as const; |
|
|
|
|
|
|
|
export type Action = keyof typeof actions; |
|
|
|
type Roles = keyof typeof roles; |
|
|
|
|
|
|
|
/** |
|
|
|
* convert role to key |
|
|
|
* @example roleToKey(roles.ADMIN) => 'ADMIN' |
|
|
|
*/ |
|
|
|
function roleToKey(role: Role) { |
|
|
|
const roleKey = Object.keys(roles).find( |
|
|
|
(key) => roles[key as Roles] === role |
|
|
|
); |
|
|
|
|
|
|
|
if (roleKey === undefined) { |
|
|
|
throw new Error('Invalid role'); |
|
|
|
} |
|
|
|
|
|
|
|
return roleKey as Roles; |
|
|
|
} |
|
|
|
|
|
|
|
type PermissionCheck<Key extends keyof Permissions> = |
|
|
|
| boolean |
|
|
|
| ((user: UserType, data: Permissions[Key]['dataType']) => boolean); |
|
|
|
|
|
|
|
type MATRIX = { |
|
|
|
readonly [key in keyof typeof actions]: readonly (typeof roles)[keyof typeof roles][]; |
|
|
|
type RolesWithPermissions = { |
|
|
|
[R in Roles]: { |
|
|
|
[Key in keyof Permissions]: { |
|
|
|
[Action in Permissions[Key]['action']]: PermissionCheck<Key>; |
|
|
|
}; |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
export const MATRIX: MATRIX = { |
|
|
|
[actions.ADMIN]: [roles.ADMIN] as const, |
|
|
|
[actions.CLIENT]: [roles.CLIENT, roles.ADMIN] as const, |
|
|
|
} as const; |
|
|
|
export type Permissions = { |
|
|
|
clients: { |
|
|
|
dataType: ClientType; |
|
|
|
action: 'view' | 'create' | 'update' | 'delete' | 'custom'; |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
export const ROLES = { |
|
|
|
ADMIN: { |
|
|
|
clients: { |
|
|
|
view: true, |
|
|
|
create: true, |
|
|
|
update: true, |
|
|
|
delete: true, |
|
|
|
custom: true, |
|
|
|
}, |
|
|
|
}, |
|
|
|
CLIENT: { |
|
|
|
clients: { |
|
|
|
view: (user, client) => user.id === client.userId, |
|
|
|
create: false, |
|
|
|
update: (user, client) => user.id === client.userId, |
|
|
|
delete: (user, client) => user.id === client.userId, |
|
|
|
custom: true, |
|
|
|
}, |
|
|
|
}, |
|
|
|
} as const satisfies RolesWithPermissions; |
|
|
|
|
|
|
|
export const checkPermissions = (action: Action, user: { role: Role }) => { |
|
|
|
if (!MATRIX[action].includes(user.role)) { |
|
|
|
export function hasPermissions<Resource extends keyof Permissions>( |
|
|
|
user: UserType, |
|
|
|
resource: Resource, |
|
|
|
action: Permissions[Resource]['action'], |
|
|
|
data?: Permissions[Resource]['dataType'] |
|
|
|
) { |
|
|
|
const permission = ROLES[roleToKey(user.role)][resource][action]; |
|
|
|
|
|
|
|
if (typeof permission === 'boolean') { |
|
|
|
return permission; |
|
|
|
} |
|
|
|
|
|
|
|
if (data === undefined) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
return true; |
|
|
|
}; |
|
|
|
|
|
|
|
return permission(user, data); |
|
|
|
} |
|
|
|
|
|
|
|
export function hasPermissionsWithData<Resource extends keyof Permissions>( |
|
|
|
user: UserType, |
|
|
|
resource: Resource, |
|
|
|
action: Permissions[Resource]['action'] |
|
|
|
) { |
|
|
|
let checked = false; |
|
|
|
return { |
|
|
|
check(data?: Permissions[Resource]['dataType']) { |
|
|
|
checked = true; |
|
|
|
const isAllowed = hasPermissions(user, resource, action, data); |
|
|
|
|
|
|
|
if (!isAllowed) { |
|
|
|
throw new Error('Permission denied'); |
|
|
|
} |
|
|
|
|
|
|
|
return isAllowed; |
|
|
|
}, |
|
|
|
isBoolean() { |
|
|
|
return typeof ROLES[roleToKey(user.role)][resource][action] === 'boolean'; |
|
|
|
}, |
|
|
|
get checked() { |
|
|
|
return checked; |
|
|
|
}, |
|
|
|
}; |
|
|
|
} |
|
|
|