Source: app/util/azure/AzureAppSettingsUtil.js

/**
 * This tool is only used for LOCAL DEVELOPMENT purposes. With this approach we don't the separate config
 * files per environment anymore. Just change your CONFIG_ENV to get the correct app settings
 * variables
 *
 * The possible CONFIG_ENV: dev, pen, tea, teb, teg, tst
 */
import path from 'path';
import fs from 'fs';
import merge from 'lodash/merge';
import debugLib from 'debug';
import { AppConfigurationClient } from '@azure/app-configuration';
import { SecretClient } from '@azure/keyvault-secrets';
import {
  DefaultAzureCredential,
  ChainedTokenCredential,
  ManagedIdentityCredential,
} from '@azure/identity';
import environmentConfig from './config/configuration.json';
import { labelFilterObject } from './AzureOptions';

const MATCH_UPPERCASE_UNDERSCORE_VALUES = /\b[A-Z0-9]+(?:_[A-Z0-9]+)+\b/g;
const KEYVAULT_URI_REGEX = /(vault.azure.net\/secrets)|([^/]+)\/?$/g;

// For the app settings
const client = new AppConfigurationClient(
  process.env.APPCONFIG_ENDPOINT,
  new DefaultAzureCredential(),
);

// For the keyVault settings
const credential = new ChainedTokenCredential(
  new DefaultAzureCredential(),
  new ManagedIdentityCredential(),
);

const stringifyConfig = JSON.stringify(environmentConfig);

const debug = debugLib('SlimmingWorld:getEnvironmentConfig');

/**
 *
 * @returns {Promise<boolean>}
 */
export const getEnvironmentConfig = async callback => {
  /**
   * This is for BE to have the ability to override the configuration settings for feature development
   * @type {null}
   */
  let localBeConfig = null;
  const fullPath = path.join(__dirname, '../wwwroot/appsettings.local.json');
  const hasLocalBeConfig = fs.existsSync(fullPath);

  debug('-------------------------------fetching configSettings-------------------------------');
  // Add all the variables and remove duplicates
  const environmentVariables = [
    ...new Set(stringifyConfig.match(MATCH_UPPERCASE_UNDERSCORE_VALUES)),
  ];

  // Loop through the app settings per label
  const environmentSettingsArray = [];
  const labelsFilterArray = Object.values(labelFilterObject);
  for (let i = 0; i < labelsFilterArray.length; i++) {
    const settings = await getAppConfigSettings(environmentVariables, {
      labelFilter: labelsFilterArray[i],
    });

    environmentSettingsArray.push(...settings);
  }

  const regexSettings = new RegExp(
    // eslint-disable-next-line prefer-template
    '\\b(?:' + environmentSettingsArray.map(setting => setting.key).join('|') + ')\\b',
    'gi',
  );

  // Replace all the custom variables with real value
  if (environmentSettingsArray.length > 0) {
    const configSettings = await JSON.parse(
      stringifyConfig.replace(regexSettings, matched => {
        const setting = environmentSettingsArray.find(item => item.key === matched);
        return setting.value;
      }),
    );

    if (hasLocalBeConfig) {
      debug('------------appsettings.local.json found------------');
      localBeConfig = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
    }

    const config = hasLocalBeConfig ? merge(configSettings, localBeConfig) : configSettings;

    debug(config);
    debug(
      '-------------------------------setting configSettings done-------------------------------',
    );

    callback(config);
  }

  return true;
};

/**
 * Create connection to retrieves the settings from a label or key
 *
 * @param environmentVariables
 * @param labelFilter
 * @param keyFilter
 * @returns {Promise<*[]|void>}
 */
const getAppConfigSettings = async (environmentVariables, { labelFilter, keyFilter }) => {
  try {
    const settingsIterator = await client.listConfigurationSettings({
      labelFilter, // For example: find every label that starts with uk-dev
      keyFilter, // For example: ACCOUNT_URL, MEMBER_URL, OIDC_AC_NODE_LIVE_SECRET etc etc
    });

    return getTransformedSettingsArray(environmentVariables, settingsIterator);
  } catch (error) {
    debug(error);
  }

  return Promise.resolve();
};

/**
 * Iterate through the Symbol.asyncIterator and returns an new array with only the data we want
 *
 * @param environmentVariables
 * @param settingsIterator
 * @returns {Promise<[]>}
 */
const getTransformedSettingsArray = async (environmentVariables, settingsIterator) => {
  const environmentSettingsArray = [];
  try {
    for await (const setting of settingsIterator) {
      const { key, label, value } = setting;

      if (environmentVariables.includes(key)) {
        environmentSettingsArray.push({
          key,
          label,
          value: await getKeyVaultValue(value),
        });
      }
    }
  } catch (error) {
    debug(error);
  }

  return environmentSettingsArray;
};

/**
 * Retrieves the value from the Azure keyVault secret
 *
 * for example:
 * '{"uri":"https://uk-shared-slimmingworld.vault.azure.net/secrets/uk-shared-instagram-accesstoken"}'
 *
 * - parse string into an object
 * - check for the vaultName and secretName
 * - fetch the keyVaultSecret object
 *
 * @param appSettingValue
 * @returns {Promise<string|*>}
 */
const getKeyVaultValue = async appSettingValue => {
  try {
    const keyVaultUriObject = JSON.parse(appSettingValue);

    if (typeof keyVaultUriObject === 'object') {
      const uri = keyVaultUriObject.uri;
      const match = uri.match(KEYVAULT_URI_REGEX);

      if (match.length > 0) {
        const keyVaultUri = JSON.parse(appSettingValue).uri;
        const secretUri = keyVaultUri.replace(/https?:\/\//i, '');
        const vaultName = secretUri.substring(0, secretUri.indexOf('.'));
        const secretName = match[1];

        const url = `https://${vaultName}.vault.azure.net`;
        const keyVaultClient = new SecretClient(url, credential);
        const latestSecretObject = await keyVaultClient.getSecret(secretName);

        return latestSecretObject.value;
      }
    }
  } catch (error) {
    // debug(error);
  }

  return appSettingValue;
};

export default getEnvironmentConfig;