Source: management-system/src/backend/shared-electron-server/network/5thIndustry/5thIndustry.js

import https from 'https';
import fse from 'fs-extra';
import path from 'path';
import store from '../../data/store.js';
import { getAppDataPath } from '../../data/fileHandling.js';
import logger from '../../logging.js';
import { getBackendConfig } from '../../data/config.js';

import {
  minimalPlansQuery,
  minimalPlanQuery,
  planCreationTemplateQuery,
  inspectionPlanCreationMutation,
  atomicInspectionPlanMutation,
  userQuery,
} from './queries.js';

let clientId;
let clientSecret;
let clientEndpoint;
let clientToken;

function asyncHTTPSRequest(url, options, query) {
  return new Promise((resolve, reject) => {
    const req = https.request(url, options, (res) => {
      let data = '';
      res.on('data', (d) => {
        data = `${data}${d}`;
      });
      res.on('end', () => {
        let response;
        try {
          response = JSON.parse(data);
        } catch (err) {
          reject(
            `Unable to parse response from server. Server responded with status code ${res.statusCode}`
          );
        }

        resolve(response);
      });
    });

    req.on('error', (err) => reject(err));
    req.write(query);
    req.end();
  });
}

async function authorizeWith5i() {
  // get service account data
  let serviceData;
  try {
    serviceData = JSON.parse(
      fse.readFileSync(
        path.join(getAppDataPath(), 'Config', '5thIndustry_service_account.json'),
        'utf-8'
      )
    );

    clientId = serviceData.clientId;
    clientSecret = serviceData.clientSecret;
    clientEndpoint = serviceData.endpoint;

    // base64 encode the account data
    const authorization = Buffer.from(
      `${serviceData.clientId}:${serviceData.clientSecret}`
    ).toString('base64');

    const query = {
      grant_type: 'client_credentials',
      client_id: serviceData.clientId,
      scope: 'profit-gateway/inspectionPlans.all',
    };

    const encodedQuery = new URLSearchParams(query).toString();

    const response = await asyncHTTPSRequest(
      serviceData.endpoint,
      {
        method: 'POST',
        headers: {
          ['Content-Type']: 'application/x-www-form-urlencoded',
          ['Content-Length']: Buffer.byteLength(encodedQuery),
          Authorization: `Basic ${authorization}`,
        },
      },
      encodedQuery
    );

    if (response.error) {
      throw response.error;
    }

    if (response.access_token) {
      clientToken = `${response.token_type} ${response.access_token}`;

      // make sure to keep the access token up to date
      setTimeout(authorizeWith5i, (response.expires_in - 60) * 1000);

      logger.info(`Successfully requested an access token from 5thIndustry!`);
    }
  } catch (err) {
    // Log failure only as debug log
    logger.debug(`Failed to get 5thIndustry Service Account Authorization. Reason: ${err}`);
    return;
  }
}

authorizeWith5i();

export function get5thIndustryServiceAccountData() {
  // Only return something when there was at least one token successfully requested with the service account data
  return clientToken ? { id: clientId, secret: clientSecret, endpoint: clientEndpoint } : null;
}

export function get5thIndustryAuthorization() {
  let authorization = clientToken;

  if (!authorization) {
    authorization = store.get('userPreferences').config.user5thIndustryAuthorization;
  }

  if (!authorization.startsWith('Bearer ')) {
    authorization = `Bearer ${authorization}`;
  }

  return authorization;
}

async function requestData(body) {
  const authorization = get5thIndustryAuthorization();

  const options = {
    method: 'POST',
    headers: {
      authorization,
      ['Content-Type']: 'application/json',
      ['Content-Length']: Buffer.byteLength(body),
    },
  };

  const response = await asyncHTTPSRequest(getBackendConfig()._5thIndustryAPIURL, options, body);

  if (response.errors) {
    throw response.errors[0].message;
  } else {
    return response.data;
  }
}

async function getUserId() {
  const body = JSON.stringify({
    query: userQuery,
    variables: {
      a: true,
    },
  });

  try {
    const result = await requestData(body);
    return result.getUsers[0]._id;
  } catch (err) {
    throw new Error(err);
  }
}

export async function getInspectionPlans(type = 'entity') {
  const body = JSON.stringify({
    query: minimalPlansQuery,
    variables: {
      type,
      sort: '{"createdBy.createdAt": -1}',
      limit: 100,
    },
  });

  try {
    const result = await requestData(body);
    return result.getInspectionPlans.inspectionPlans;
  } catch (err) {
    throw new Error(err);
  }
}

export async function getInspectionPlanData(
  inspectionPlanId,
  type = 'entity',
  query = minimalPlanQuery
) {
  const body = JSON.stringify({
    query,
    variables: {
      id: inspectionPlanId,
      type,
    },
  });

  try {
    const result = await requestData(body);
    return result.getInspectionPlan.inspectionPlan;
  } catch (err) {
    throw new Error(err);
  }
}

async function getNewPlanData(planInformation, templateId) {
  let userId;

  // use the user id of the account that is used for communication with the 5thIndustry App
  if (clientToken) {
    userId = clientId;
  } else {
    userId = await getUserId();
  }

  if (!userId) {
    throw new Error('Could not get user id to create 5thIndustry Plan with.');
  }

  let planData;
  const time = new Date().toISOString();

  if (templateId) {
    planData = await getInspectionPlanData(templateId, 'template', planCreationTemplateQuery);

    planData.templateId = templateId;
    delete planData._id;
  } else {
    planData = {
      legalOwnerLogo: {},
      numberOfConflicts: 0,
      assemblyGroup: [],
    };
  }

  planData.title = [{ language: 'EN', value: planInformation.title }];
  planData.customer = planInformation.customer;
  planData.customerOrderNo = planInformation.customerOrderNo;
  planData.jobName = planInformation.jobName;

  planData.documentType = 'IuT-Plan';

  planData.revisionHistory = [
    {
      editor: {
        accountID: userId,
      },
      revisionNumber: 'AA',
      revisionDate: time,
    },
  ];
  planData.createdBy = {
    createdAt: time,
    userID: userId,
  };

  planData.workStatus = 'open';
  planData.status = 'draft';

  return planData;
}

export async function createPlan(planInformation, templateId) {
  const planData = await getNewPlanData(planInformation, templateId);

  const body = JSON.stringify({
    operationName: 'createInspectionPlan',
    query: inspectionPlanCreationMutation,
    variables: {
      type: 'entity',
      plan: planData,
    },
  });

  try {
    const result = await requestData(body);
    return result.createInspectionPlan.inspectionPlan;
  } catch (err) {
    throw new Error(err);
  }
}

/**
 * Sends a request to the 5thIndustry backend to change attributes of an existing plan
 *
 * @param {String} inspectionPlanId
 * @param {Object} values the entries to change as key value pairs
 * @param {String} type what kind of plan is supposed to be changed (either "template" or "entity")
 */
async function updateInspectionPlan(inspectionPlanId, values, type) {
  const body = JSON.stringify({
    operationName: 'atomicInspectionPlan',
    query: atomicInspectionPlanMutation,
    variables: {
      type,
      atomics: [
        {
          inspectionPlanId,
          operation: 'update',
          values,
        },
      ],
    },
  });

  await requestData(body);
}

/**
 * Will set a plan into a in progress state to signal that it is being executed
 *
 * @param {String} inspectionPlanId
 */
export async function start5thIndustryPlan(inspectionPlanId) {
  try {
    await updateInspectionPlan(inspectionPlanId, {
      status: 'released',
      workStatus: 'inWork',
    });
  } catch (err) {
    throw new Error(`Could not update the 5thIndustry inspection plan. 5thIndustryError: ${err}.`);
  }
}

/**
 * Will set the plan back to an open state to signal that is currently not being executed (might be necessary when something in the linked process fails)
 *
 * @param {String} inspectionPlanId
 */
export async function stop5thIndustryPlan(inspectionPlanId) {
  await updateInspectionPlan(inspectionPlanId, {
    workStatus: 'open',
  });
}