// $Id: UserRole.js 8636 2025-09-09 14:21:04Z amr.abdou $
// @ts-check
"use strict";
import { RestServices } from "../tools/RestServices.js";
import { cnContext } from "./cnContext.js";
import { cacheStore } from "./cache.internal.js";
import { ApiResponse } from "../tools/ApiResponse.js";
/**
* @fileoverview
* - User role domain classes and services.
* - Types and services to manage roles and entitlements.
*/
/**
* @classdesc Role aggregate (info, avatar, permissions, users).
* @category Usermanagement
*/
export class UserRole {
constructor(userRole = /** @type {UserRole} */ ({})) {
this.info = userRole.info || {};
this.avatar = userRole.avatar || ""; // Base64
this.permissions = userRole.permissions || {}; // RolePermissions enum
this.users = userRole.users || []; // string[]
}
/**
* Convert the role to a JSON object.
* @returns {object}
*/
toJSON() {
return Object.fromEntries(
Object.entries(this).filter(([_, v]) => typeof v !== "function")
);
}
/**
* Set the user role from a JSON object.
* @param {object} userRole - The JSON object
* @returns {void}
*/
setFromJSON(userRole = /** @type {object} */ ({})) {
this.info = userRole.info || {};
this.avatar = userRole.avatar || ""; // Base64
this.permissions = userRole.permissions || {}; // RolePermissions enum
this.users = userRole.users || []; // string[]
}
/**
* Create a user role from a JSON object.
* @param {object} userRole - The JSON object
* @returns {UserRole}
*/
static createFromJSON(userRole = /** @type {object} */ ({})) {
const obj = new UserRole();
obj.setFromJSON(userRole);
return obj;
}
/**
* Set the avatar of a role.
* @param {string} avatar - The avatar base64 string
* @returns {void}
*/
setAvatar(avatar) {
this.avatar = avatar;
}
/**
* Get the avatar of a role.
* @returns {string} - The avatar base64 string
*/
getAvatar() {
return this.avatar;
}
}
/**
* @classdesc Service layer for CRUD operations on roles.
* @ingroup user_roles
* @category Usermanagement
*/
export class UserRoleServices {
/**
* @since 09.2025, aab
* @param {cnContext} ctx - Context instance
*/
#cache = cacheStore;
constructor(ctx = /** @type {cnContext} */ ({})) {
this._ctx = ctx;
}
/**
* Create a new role.
* @param {object} [data] - Role payload
* @param {string} [data.name] - The name
* @param {string} [data.description] - The description
* @param {object} [data.permissions] - The permissions
* @returns {Promise<ApiResponse>}
*/
async createRole(data) {
// validate the input
if (!data?.name) {
return new ApiResponse(400, null);
}
let url = "";
try {
// create the role via api
url = `${this._ctx.getServiceUrl(
cnContext.SERVICE.USR
)}/${this._ctx.getScope()}/userroles`;
const res = await RestServices.makeApiCall(
this._ctx.getToken(),
RestServices.METHODS.POST,
url,
data
);
if (res.status < 300) {
const scope = this._ctx.getScope?.() || "";
this.#cache?.deleteWhere?.(
(key) =>
key.startsWith(`scope:${scope}|role:`) ||
key.startsWith(`scope:${scope}|roles:`)
);
return new ApiResponse(200, res.data);
} else {
return new ApiResponse(res.status, res.data || null);
}
} catch (error) {
return new ApiResponse(500, null);
}
}
/**
* Update an existing role.
* @param {string} roleid
* @param {object} [data] - Role payload
* @param {string} [data.name] - The name
* @param {string} [data.description] - The description
* @param {object} [data.permissions] - The permissions
*
* @returns {Promise<ApiResponse>}
*/
async updateRole(roleid, data) {
let url = "";
try {
// get the role data from the api
url = `${this._ctx.getServiceUrl(
cnContext.SERVICE.USR
)}/${this._ctx.getScope()}/userroles/${roleid}`;
// make the api call
const res = await RestServices.makeApiCall(
this._ctx.getToken(),
RestServices.METHODS.PATCH,
url,
data
);
if (res.status < 300) {
const scope = this._ctx.getScope?.() || "";
this.#cache?.markNeedUpdateWhere?.(
(key) =>
key === `scope:${scope}|role:${roleid}` ||
key.startsWith(`scope:${scope}|roles:`)
);
return new ApiResponse(200, res.data);
} else {
return new ApiResponse(res.status, res.data);
}
} catch (error) {
return new ApiResponse(500, null);
}
}
/**
* Delete a role.
* @param {string} roleid
* @returns {Promise<ApiResponse>}
*/
async deleteRole(roleid) {
let url = "";
try {
// get the role data from the api
url = `${this._ctx.getServiceUrl(
cnContext.SERVICE.USR
)}/${this._ctx.getScope()}/userroles/${roleid}`;
// make the api call
const res = await RestServices.makeApiCall(
this._ctx.getToken(),
RestServices.METHODS.DELETE,
url
);
if (res.status < 300) {
const scope = this._ctx.getScope?.() || "";
this.#cache?.deleteWhere?.(
(key) =>
key === `scope:${scope}|role:${roleid}` ||
key.startsWith(`scope:${scope}|roles:`)
);
return new ApiResponse(200, res.data);
} else {
return new ApiResponse(res.status, res.data);
}
} catch (error) {
return new ApiResponse(500, null);
}
}
/**
* Add users to a role.
* @param {string} roleid
* @param {object} [data] - Payload
* @param {string} [data.parentid] - The parent ID
* @param {string[]} [data.userids] - The user IDs
* @returns {Promise<ApiResponse>}
*/
async addUsersToRole(roleid, data) {
// validate the input
if (!roleid || !data?.userids || data?.userids?.length === 0) {
return new ApiResponse(400, null);
}
let url = "";
try {
// get the role data from the api
url = `${this._ctx.getServiceUrl(
cnContext.SERVICE.USR
)}/${this._ctx.getScope()}/userroles/${roleid}/users`;
// make the api call
const res = await RestServices.makeApiCall(
this._ctx.getToken(),
RestServices.METHODS.POST,
url,
data
);
if (res.status < 300) {
const scope = this._ctx.getScope?.() || "";
this.#cache?.markNeedUpdateWhere?.(
(key) =>
key === `scope:${scope}|role:${roleid}` ||
key.startsWith(`scope:${scope}|roles:`)
);
return new ApiResponse(200, res.data);
} else {
return new ApiResponse(res.status, res.data);
}
} catch (error) {
return new ApiResponse(500, null);
}
}
/**
* Remove users from a role.
* @param {string} roleid
* @param {string[]} userids
* @returns {Promise<ApiResponse>}
*/
async removeUsersFromRole(roleid, userids) {
// validate the input
if (!roleid || !userids || userids.length === 0) {
return new ApiResponse(400, null);
}
let url = "";
try {
// get the role data from the api
url = `${this._ctx.getServiceUrl(
cnContext.SERVICE.USR
)}/${this._ctx.getScope()}/userroles/${roleid}/users`;
// make the api call
const res = await RestServices.makeApiCall(
this._ctx.getToken(),
RestServices.METHODS.DELETE,
url,
userids
);
if (res.status < 300) {
const scope = this._ctx.getScope?.() || "";
this.#cache?.markNeedUpdateWhere?.(
(key) =>
key === `scope:${scope}|role:${roleid}` ||
key.startsWith(`scope:${scope}|roles:`)
);
return new ApiResponse(200, res.data);
} else {
return new ApiResponse(res.status, res.data);
}
} catch (error) {
return new ApiResponse(500, null);
}
}
/**
* Update a role avatar (base64 string).
* @param {string} roleid
* @param {string} avatar - Base64 encoded image
* @returns {Promise<ApiResponse>}
*/
async updateRoleAvatar(roleid, avatar) {
let url = "";
try {
// get the role avatar from the api
url = `${this._ctx.getServiceUrl(
cnContext.SERVICE.USR
)}/${this._ctx.getScope()}/userroles/${roleid}/avatar`;
// make the api call
const res = await RestServices.makeApiCall(
this._ctx.getToken(),
RestServices.METHODS.PUT,
url,
avatar,
"text/plain"
);
if (res.status < 300) {
const key = `role:avatar:${roleid}`;
this.#cache?.delete?.(key);
this.#cache?.set?.(key, res.data, {
tags: { type: "role", part: "avatar", roleid },
});
return new ApiResponse(200, res.data);
} else {
return new ApiResponse(res.status, res.data);
}
} catch (error) {
return new ApiResponse(500, null);
}
}
/**
* Fetch a role avatar (base64 string).
* @param {string} roleid
* @returns {Promise<ApiResponse>} - The response object
*/
async getRoleAvatar(roleid) {
// validate the input
const key = `role:avatar:${roleid}`;
// get the avatar from the cache
const avatar = this.#cache?.get?.(key);
// if the avatar is found, return it
if (avatar && avatar !== "") {
return new ApiResponse(200, avatar);
}
let url = "";
try {
// get the role avatar from the api
url = `${this._ctx.getServiceUrl(
cnContext.SERVICE.USR
)}/${this._ctx.getScope()}/userroles/${roleid}/avatar`;
// make the api call
const res = await RestServices.makeApiCall(
this._ctx.getToken(),
RestServices.METHODS.GET,
url
);
if (res.status < 300) {
// cache the avatar
this.#cache?.set?.(key, res.data, {
tags: { type: "role", part: "avatar", roleid },
});
// return the avatar
return new ApiResponse(200, res.data);
} else {
return new ApiResponse(res.status, res.data);
}
} catch (error) {
return new ApiResponse(500, null);
}
}
/**
* Fetch a single role by ID.
* @param {string} roleid
* @param {object} [query] - The query parameters
* @param {string} [query.filter] - The filter
* @param {string} [query.members] - The members
* @returns {Promise<ApiResponse>} - The response object
*/
async getRole(roleid, query) {
let url = "";
// validate the input
if (!roleid) {
return new ApiResponse(400, null);
}
// build the query string
const queryString = this.#buildQueryString(query);
url = `${this._ctx.getServiceUrl(
cnContext.SERVICE.USR
)}/${this._ctx.getScope()}/userroles/${roleid}`;
const scope = this._ctx.getScope?.() || "";
const key = `scope:${scope}|role:${roleid}|q:${queryString}`;
const cached = this.#cache?.getEntry?.(key);
if (cached && cached.needUpdate === false) {
return new ApiResponse(200, cached.value);
}
try {
// get the role data from the api
// make the api call
const res = await RestServices.makeApiCall(
this._ctx.getToken(),
RestServices.METHODS.GET,
url + queryString
);
if (res.status < 300) {
// process the data (guard null)
const list = res.data ? [res.data] : [];
const processedData = this.#processUserRoleObject(list);
this.#cache?.set?.(key, processedData, {
tags: { type: "role", kind: "single", roleid, scope },
});
// return the data
return new ApiResponse(200, processedData[0]);
} else {
return new ApiResponse(res.status, res.data);
}
} catch (error) {
return new ApiResponse(500, null);
}
}
/**
* List all roles (optionally filtered).
* @param {object} [query] - The query parameters
* @param {string} [query.filter] - The filter
* @param {string} [query.members] - The members
* @returns {Promise<ApiResponse>} - The response object
*/
async getAllRoles(query) {
let url = "";
// build the query string
const queryString = this.#buildQueryString(query);
// build the url
url = `${this._ctx.getServiceUrl(
cnContext.SERVICE.USR
)}/${this._ctx.getScope()}/userroles`;
// build the key
const scope = this._ctx.getScope?.() || "";
const key = `scope:${scope}|roles:all|q:${queryString}`;
// get the cached entry
const cachedEntry = this.#cache?.getEntry?.(key);
// if the cached entry is not stale, return it
if (cachedEntry && cachedEntry.needUpdate === false) {
return new ApiResponse(200, cachedEntry.value);
}
try {
// make the api call
const res = await RestServices.makeApiCall(
this._ctx.getToken(),
RestServices.METHODS.GET,
url + queryString
);
if (res.status < 300) {
// process the data (guard null)
const list = Array.isArray(res.data) ? res.data : [];
const processedData = this.#processUserRoleObject(list);
// cache the data
this.#cache?.set?.(key, processedData, {
tags: { type: "role", kind: "list", scope },
});
// return the data
return new ApiResponse(200, processedData);
} else {
return new ApiResponse(res.status, res.data || []);
}
} catch (error) {
// return the error
return new ApiResponse(500, []);
}
}
/**
* List roles by IDs (or all), optionally attaching avatars.
* @param {string[]} roleList - List of role IDs
* @param {object} [query] - The query parameters
* @param {string} [query.filter] - The filter
* @param {string} [query.members] - The members
* @param {boolean} withAvatar - Whether to include the avatar of the roles
* @returns {Promise<ApiResponse>} - The response object
*/
async getRoles(roleList = [], query, withAvatar = false) {
let rolesList = /** @type {UserRole[]} */ ([]);
// get the roles from the api
try {
if (roleList.length > 0) {
// get the roles from the api
const roleRes = await Promise.all(
roleList?.map(async (roleid) => {
const res = await this.getRole(roleid, query);
return res.status < 300 && res.data ? [res.data] : [];
})
);
// flatten the roles
const flattenedRoles = roleRes.flat();
rolesList.push(...flattenedRoles);
}
// if the roles are not found, get the roles from the api
if (rolesList.length === 0) {
const rolesRes = await this.getAllRoles(query);
rolesList.push(...(rolesRes.data || []));
}
// get the avatar of the roles if withAvatar is true
if (withAvatar) {
// get the avatar of the roles
const avatarList = await Promise.all(
rolesList.map(async (role) => {
// @ts-ignore
const res = await this.getRoleAvatar(role?.info?.id);
return res.status < 300 ? res.data : null;
})
);
// add the avatar to the roles
rolesList.forEach((role, index) => {
role.avatar = avatarList[index] || "";
});
}
return new ApiResponse(200, rolesList || []);
} catch (error) {
return new ApiResponse(500, []);
}
}
/**
* query string builder
* @param {object} [query] - Query object containing filter, members, etc.
* @param {string} [query.filter] - The filter
* @param {string} [query.members] - The members
* @param {string} defaultMember - Default member value if not specified
* @returns {string} - Properly formatted query string
*/
#buildQueryString(
query = /** @type {object} */ ({}),
defaultMember = "info"
) {
let queryString = "";
let c = "?";
// Handle filter parameter
if (query["filter"]) {
queryString += c + "filter=" + encodeURIComponent(query["filter"]);
c = "&";
}
// Handle members parameter - controls what data is returned
if (query["members"]) {
let membersString = query["members"];
// Ensure defaultMember is included if specified and not already present
if (defaultMember && !membersString.includes(defaultMember)) {
membersString = membersString + "," + defaultMember;
}
queryString += c + "members=" + encodeURIComponent(membersString);
c = "&";
} else if (defaultMember) {
// Apply default member if not specified
queryString += c + "members=" + encodeURIComponent(defaultMember);
c = "&";
}
return queryString;
}
/**
* Process the user role object
* @param {Object[]} Objects - Array of user role objects
* @returns {UserRole[]} - Array of UserRole objects
*/
#processUserRoleObject(Objects = []) {
let rolesList = [];
for (const object of Objects) {
const roleObject = UserRole.createFromJSON(object);
rolesList.push(roleObject);
}
return rolesList;
}
}
// ESM file: no CommonJS export
Source