All files / src/repositories user.repository.ts

36.23% Statements 25/69
0% Branches 0/75
7.14% Functions 1/14
34.33% Lines 23/67

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307  1x 1x                         1x 1x 1x 1x     1x     1x                 1x         102x   102x   102x   102x   102x   102x 102x 102x   102x 102x   102x 102x   102x 102x                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import {Getter, inject} from '@loopback/core';
import {
  AnyObject,
  BelongsToAccessor,
  Count,
  HasManyRepositoryFactory,
  HasOneRepositoryFactory,
  Options,
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  repository,
  Where,
} from '@loopback/repository';
import {DataObject} from '@loopback/repository/src/common-types';
import {CompanyRepository} from '.';
import {Constants} from '../configs';
import {LogChainDataSource} from '../datasources';
import {Company, Role, Status, TradeLane, User, UserActivity, UserRelations, UserWithRelations} from '../models';
import {BaseCrudRepository} from './base-crud.repository.base';
import {RoleRepository} from './role.repository';
import {TradeLaneRepository} from './trade-lane.repository';
import {UserActivityRepository} from './user-activity.repository';
import {CompanySummaryDto} from '@logchain/dtos';
 
export class UserRepository extends BaseCrudRepository<User, typeof User.prototype.id, UserRelations> {
  public readonly creator: BelongsToAccessor<User, typeof User.prototype.id>;
  public readonly modifier: BelongsToAccessor<User, typeof User.prototype.id>;
  public readonly company: BelongsToAccessor<Company, typeof User.prototype.id>;
  public readonly activities: HasManyRepositoryFactory<UserActivity, typeof User.prototype.id>;
  public readonly tradelanes: HasManyRepositoryFactory<TradeLane, typeof User.prototype.id>;
  public readonly role: HasOneRepositoryFactory<Role, typeof Role.prototype.id>;
 
  @repository(UserActivityRepository)
  public activities_repo: UserActivityRepository;
 
  constructor(
    @inject('datasources.logchain') dataSource: LogChainDataSource,
    @repository.getter('UserActivityRepository')
    protected activities_repo_getter: Getter<UserActivityRepository>,
    @repository.getter('TradeLaneRepository')
    protected tradelane_repo_getter: Getter<TradeLaneRepository>,
    @repository.getter('CompanyRepository')
    protected comp_repo_getter: Getter<CompanyRepository>,
    @repository.getter('RoleRepository')
    protected role_repo_getter: Getter<RoleRepository>,
  ) {
    super(User, dataSource);
 
    this.creator = this.createBelongsToAccessor('creator', Getter.fromValue(this));
    this.modifier = this.createBelongsToAccessor('modifier', Getter.fromValue(this));
    this.company = this.createBelongsToAccessor('company', comp_repo_getter);
 
    this.activities = this.createHasManyRepositoryFactoryFor('activities', activities_repo_getter);
    this.registerInclusionResolver('activities', this.activities.inclusionResolver);
 
    this.tradelanes = this.createHasManyRepositoryFactoryFor('tradelanes', tradelane_repo_getter);
    this.registerInclusionResolver('tradelanes', this.tradelanes.inclusionResolver);
 
    this.role = this.createHasOneRepositoryFactoryFor('role', role_repo_getter);
    this.registerInclusionResolver('role', this.role.inclusionResolver);
  }
 
  /**
   * Find user by sub.
   * @param sub The sub of user
   */
  async findBySub(sub: string): Promise<UserWithRelations | null> {
    // eslint-disable-next-line @typescript-eslint/return-await
    return this.findOne({
      include: [
        {
          relation: 'company',
          scope: {
            fields: {
              id: true,
              identifier: true,
              name: true,
              timezone: true,
              status: true,
            },
          },
        },
      ],
      where: {
        sub,
      },
    });
  }
 
  /**
   * Find users by role rights.
   * @param feature_id The feature id.
   * @param right The right access.
   */
  async findByRoleRights(company_id: number, feature_id: string, right: string): Promise<User[]> {
    if (!Object.values(Constants.AccessScope).includes(right)) {
      throw new Error(`Right is invalid ${right}`);
    }
 
    const logchain_admins = await this.execute(
      `SELECT
        name,
        contact_email
      FROM
        role_rights AS rr
        INNER JOIN users_view AS u ON rr.role_id = u.role_id
      WHERE
        feature_id = $1
        AND rr."${right}" = $2
        AND u.status = $3
        AND u.company_id = $4;`,
      [feature_id, Constants.Access.Allowed, Status.User.Active, company_id],
    );
 
    return logchain_admins as User[];
  }
 
  /**
   * Override create to register and enroll new user
   * @param entity The entity
   * @param options The options
   */
  async create(entity: DataObject<User>, options: Options = {}): Promise<User> {
    const new_user = await super.create(entity, options);
    if (process.env.HLF_BY_PASS === 'true') {
      console.log('HLF bypass enabled - skip registerAndEnrollUser');
    } else {
      await this.ca_service.registerAndEnrollUser(new_user.identifier);
    }
    return new_user;
  }
 
  /**
   * Delete all user by where.
   * - Override deleteAll from DefaultCrudRepository to do:
   *  + Cleanup S3 after delete user.
   *  + Cleanup HLF after delete user.
   *  + Any logic related to business should be done at the Service level (ex. Send Email).
   * @param where The where object
   * @param options The options object.
   */
  async deleteAll(where?: Where<User>, options?: Options): Promise<Count> {
    let deleted_user = options?.deleted_user;
    const resl = await super.updateAll(
      {
        name: '[Unknown removed user]',
        identifier: undefined,
        avatar: '',
        contact_number: '',
        contact_email: '',
        status: Status.User.Deleted,
      },
      where,
      options,
    );
 
    if (!deleted_user && options?.user_view_repo) {
      deleted_user = await options?.user_view_repo.findOne({where});
    }
 
    if (!Array.isArray(deleted_user)) {
      deleted_user = [deleted_user];
    }
 
    setImmediate(() => {
      const images: string[] = [];
      const emails: string[] = [];
      for (const user of deleted_user) {
        if (user?.avatar) {
          images.push(user.avatar);
        }
        if (user?.contact_email) {
          emails.push(user.contact_email);
        }
 
        // Delete user_activities
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
        this.activities_repo.deleteAll({user_id: deleted_user.id}).catch(err => {
          console.error(err);
        });
 
        if (user?.identifier) {
          // cessation of operation
          // eslint-disable-next-line @typescript-eslint/no-floating-promises
          if (process.env.HLF_BY_PASS === 'true') {
            console.log('HLF bypass enabled - skip revokeUser & DeleteActor');
          } else {
            this.ca_service.revokeUser(user.identifier, Constants.RevokeReason.CessationOfOperation).catch(err => {
              console.error(err);
            });
 
            // invoke chaincode DeleteActor
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
            this.cc_service.invoke('DeleteActor', user.identifier, {is_transient: false}).catch(err => {
              console.error(err);
            });
          }
        }
      }
 
      // delete user in cognito
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
      this.cognito_service.deleteUsers(emails).catch(err => {
        console.error(err);
      });
 
      // delete user avatar
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
      this.s3_service.deleteObjects(images).catch(err => {
        console.error(err);
      });
    });
 
    return resl;
  }
 
  /**
   * Get list of user ids that are excluded from receiving notifications.
   * The condition is that the user's status is Status.User.Deleted.
   * @returns {Promise<number[]>} The list of user ids.
   */
  async getExcludedUsersBySettingsFromReceivingNotifications(
    flow_id?: number,
    flow_action_id?: number,
  ): Promise<number[]> {
    const rsUsers = await this.execute(
      `
      SELECT 
        array_agg(DISTINCT u.id) AS excluded_user_ids
      FROM users u
      LEFT JOIN flows_notification fn ON fn.created_by = u.id
      LEFT JOIN flows_action_notification fan ON fan.created_by = u.id
      WHERE (u.settings->>'received_completed_action' IS NULL OR (u.settings->>'received_completed_action')::BOOLEAN = FALSE)
      OR (fn.created_by IS NOT NULL ${flow_id ? 'AND fn.flow_id = $1)' : ')'}
      OR (fan.created_by IS NOT NULL ${flow_action_id ? 'AND fan.flow_action_id = $2)' : ')'}
      ;`,
      [flow_id, flow_action_id],
    );
 
    return rsUsers?.length && rsUsers[0]?.excluded_user_ids ? rsUsers[0]?.excluded_user_ids : [0];
  }
 
  /**
   * Get a summary of users by status and role types in a given company.
   * @param company_id The id of the company.
   * @returns A list of objects each containing the status and role type of the users and the total count of that status and role type.
   */
  async getSummaryUsersByStatusAndRoleTypes(company_id: number): Promise<CompanySummaryDto> {
    const rsUsers = await this.execute(
      `
      SELECT 
        SUM(CASE WHEN users.status = 0 THEN 1 ELSE 0 END) AS invite,
        SUM(CASE WHEN users.status = 1 THEN 1 ELSE 0 END) AS inactive,
        SUM(CASE WHEN users.status = 2 THEN 1 ELSE 0 END) AS active,
        SUM(CASE WHEN users.status = 3 THEN 1 ELSE 0 END) AS deleted,
        COUNT(users.id) AS total,
        SUM(CASE WHEN users.status = 1 AND roles.collar_type = 1 THEN 1 ELSE 0 END) AS blue_inactived,
        SUM(CASE WHEN users.status = 2 AND roles.collar_type = 1 THEN 1 ELSE 0 END) AS blue_actived,
        SUM(CASE WHEN users.status IN (1,2) AND roles.collar_type = 1 THEN 1 ELSE 0 END) AS blue_total,
        SUM(CASE WHEN users.status = 1 AND roles.collar_type = 2 THEN 1 ELSE 0 END) AS white_inactived,
        SUM(CASE WHEN users.status = 2 AND roles.collar_type = 2 THEN 1 ELSE 0 END) AS white_actived,
        SUM(CASE WHEN users.status IN (1,2) AND roles.collar_type = 2 THEN 1 ELSE 0 END) AS white_total
      FROM users
      INNER JOIN roles ON roles.id = users.role_id
      WHERE users.company_id = $1
      GROUP BY users.company_id;`,
      [company_id],
    );
 
    return rsUsers?.length
      ? rsUsers[0]
      : ({
          invite: 0,
          inactive: 0,
          active: 0,
          deleted: 0,
          total: 0,
          blue_inactived: 0,
          blue_actived: 0,
          blue_total: 0,
          white_inactived: 0,
          white_actived: 0,
          white_total: 0,
        } as CompanySummaryDto);
  }
 
  async getUsersForCompany(company_id: number, options?: Options): Promise<AnyObject[]> {
    const rsUsers = await this.execute(
      `
      SELECT users_view.id,
        users_view.name,
        roles_view.collar_type,
        roles_view.collar_type_name,
        users_view.status,
        users_view.status_name
      FROM users_view
      INNER JOIN roles_view ON roles_view.id = users_view.role_id
      WHERE users_view.company_id = $1 AND users_view.status != 3
      ORDER BY name ASC;`,
      [company_id],
      options,
    );
 
    return rsUsers?.length ? (rsUsers as AnyObject[]) : [];
  }
}