/**
* Role type - based on the backend's role system.
* These roles are returned from the API as strings.
*/
export type Role =
| "customer"
| "creator"
| "staff"
| "localprovider"
| "verifiedshaman"
| "master";
export type Resource =
| "event"
| "place"
| "ticket"
| "staff"
| "localprovider"
| "category";
export type Action =
| "create"
| "read"
| "edit"
| "request"
| "delete"
| "purchase"
| "redeem"
| "accept/decline"
| "update";
type PartialRecord<K extends string | number | symbol, T> = { [P in K]?: T };
type ResourceActions = PartialRecord<Resource, Action[]>;
export type RoleControl = {
[key in Role]: ResourceActions;
};
export const RBAC_ROLES: RoleControl = {
customer: {
event: ["read", "create"],
ticket: ["purchase"],
},
creator: {
event: ["read", "create"],
ticket: ["purchase"],
},
staff: {
event: ["read", "create"],
ticket: ["redeem"],
},
verifiedshaman: {
event: ["read", "create"],
ticket: ["purchase"],
place: ["request"],
category: ["create"],
},
localprovider: {
event: ["read", "create", "edit", "accept/decline"],
ticket: ["redeem"],
place: ["create", "accept/decline", "edit"],
staff: ["create", "delete"],
category: ["update"],
},
master: {
localprovider: ["create", "delete"],
event: ["read", "create", "edit", "accept/decline"],
ticket: ["redeem"],
place: ["create", "accept/decline", "edit"],
staff: ["create", "delete"],
category: ["update"],
},
};
export type Permissions = `${Resource}:${Action}`;
export type View =
| "manage-places"
| "manage-staff"
| "manage-localproviders"
| "manage-verifiedshamans"
| "manage-verification-stats";
export const RBAC_VIEWS: Record<Role, View[]> = {
customer: [],
creator: [],
staff: [],
verifiedshaman: [],
localprovider: ["manage-places", "manage-staff", "manage-verifiedshamans"],
master: [
"manage-places",
"manage-staff",
"manage-localproviders",
"manage-verifiedshamans",
"manage-verification-stats",
],
};
// Role priority for determining the primary role (higher = more important)
const ROLE_PRIORITY: Record<Role, number> = {
master: 100,
localprovider: 80,
verifiedshaman: 60,
staff: 40,
creator: 40,
customer: 0,
};
/**
* Get the highest priority role from a list of roles.
* Returns null if the list is empty.
*/
export function getPrimaryRole(roles: Role[] | undefined): Role | null {
if (!roles || roles.length === 0) {
return null;
}
return roles.reduce((highest, current) => {
const currentPriority = ROLE_PRIORITY[current] ?? -1;
const highestPriority = ROLE_PRIORITY[highest] ?? -1;
if (currentPriority > highestPriority) {
return current;
}
return highest;
});
}
/**
* Check if any of the user's roles grant access to the specified view.
*/
export function checkView(roles: Role[] | undefined, view: View): boolean {
if (!roles || roles.length === 0) {
return false;
}
return roles.some((role) => {
const views = RBAC_VIEWS[role as Role];
if (!views) return false;
return views.includes(view);
});
}
/**
* Check if any of the user's roles have all the specified permissions.
* Uses OR logic across roles - if ANY role has the permission, access is granted.
*/
export function checkPermission(
roles: Role[] | undefined,
...permissions: Permissions[]
) {
if (!roles || roles.length === 0) {
return false;
}
for (const permission of permissions) {
const [source, action] = permission.split(":") as [Resource, Action];
// Check if ANY role has this permission
const hasPermission = roles.some((role) => {
const rolePermissions = RBAC_ROLES[role as Role];
if (!rolePermissions) return false;
const permittedActions = rolePermissions[source];
if (!permittedActions) return false;
return permittedActions.includes(action);
});
if (!hasPermission) {
return false;
}
}
return true;
}
/**
* Check if user has a specific role.
*/
export function hasRole(roles: Role[] | undefined, role: Role): boolean {
if (!roles || roles.length === 0) {
return false;
}
return roles.includes(role);
}
/**
* Check if user has any of the specified roles.
*/
export function hasAnyRole(
roles: Role[] | undefined,
...targetRoles: Role[]
): boolean {
if (!roles || roles.length === 0) {
return false;
}
return targetRoles.some((targetRole) => roles.includes(targetRole));
}