Source

infohub/UserRole.js

// $Id: UserRole.js 8754 2025-09-23 08:38:57Z amr.abdou $
// @ts-check

'use strict';

import { RestServices } from '../tools/RestServices.js';
import { Context } from './Context.js';
// import { cacheStore } from './cache.internal.js';
import { ApiResponse } from '../tools/ApiResponse.js';

/**
 * @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 {Context} ctx - Context instance
   */
  constructor(ctx = /** @type {Context} */ ({})) {
    this._ctx = ctx;
  }
  // #cache = cacheStore;

  /**
   * 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(Context.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(Context.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(Context.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(Context.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(Context.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(Context.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(Context.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(Context.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(Context.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