Example of JavaScript integration: MailTrain#

This example demonstrates how to integrate the JavaScript API library into an existing project. The open source project Mailtrain was chosen as an example, in which you can see below how it has been integrated with the JavaScript API library.

Overview#

Mailtrain is a self hosted newsletter web based application built on Node.js (v14+) and MySQL (v8+) or MariaDB (v10+).

Project features:
  • Subscriber lists management
  • List segmentation
  • Custom fields
  • Email templates (including MJML-based templates)
  • Custom reports
  • Automation (triggered and RSS campaigns)
  • Multiple users with granular user permissions and flexible sharing
  • Hierarchical namespaces for enterprise-level situations
  • Builtin Zone-MTA for close-to-zero setup of mail delivery
Library integration:
  • Encrypt emails of subscribers before saving in DB
  • Decryption of email when they are rendered in the frontend
  • Encrypt/decrypt email templates
  • Decryption of email when backend send campaign email to the targeted recipients

Installation#

Using npm:

npm i rps-engine-client-js

Configure the transformation#

The example illustrated in this pages is based on the following transformation configuration which can be created by following this tutorial and using this JSON configuration file.

Details of the transformation configuration:

  • One rights context is defined: Can TRANSFORM.
  • Two processing contexts are defined: Protect and Deprotect.
  • The Subscriber.Email data instance can be protected/deprotected using the "AES deterministic decryption sequence"/"AES deterministic encryption sequence" transformer sequences which are based on "AES deterministic encryption" transformers
  • The CampaignSettings.ReplyEmail data instance can be protected/deprotected using the "AES deterministic decryption sequence"/"AES deterministic encryption sequence" transformer sequences which are based on "AES deterministic encryption" transformers
  • The CampaignSettings.FromEmail data instance can be protected/deprotected using the "AES deterministic decryption sequence"/"AES deterministic encryption sequence" transformer sequences which are based on "AES deterministic encryption" transformers
  • The Template.Text data instance can be protected/deprotected using the "AES deterministic decryption sequence"/"AES deterministic encryption sequence" transformer sequences which are based on "AES deterministic encryption" transformers
  • The Template.Name data instance can be protected/deprotected using the "AES deterministic decryption sequence"/"AES deterministic encryption sequence" transformer sequences which are based on "AES deterministic encryption" transformers
  • The Template.Description data instance can be protected/deprotected using the "AES deterministic decryption sequence"/"AES deterministic encryption sequence" transformer sequences which are based on "AES deterministic encryption" transformers

Configuration of the API library#

The API library is installed and configured in the shared directory because it is used on the client and on the server.

Configuration files#

In this section we see how to configure the RPS API Library in order to use the transformation configuration you just created. You have to replace following parameters with the you retrieved when you created the transformation configuration in RPS CoreConfiguration:

  • {API_KEY_OF_CONFIGURATION} = It is the API Key of the transformation configuration
  • {SECRET_KEY_OF_CONFIGURATION} = It is the Secret Key of the transformation configuration
  • {ENGINE_HOST_NAME} = It is the RPS transformation services hostname
  • {AUTH_HOST_NAME} = It is the RPS OAuth2 authentication services hostname
Configuration file in the shared directory:#
// importing classes from npm package
const {EngineClient, RequestBuilder, TokenProvider, RPSContext} = require('rps-engine-client-js/lib')

// importing classes
const {EngineClient, RequestBuilder, TokenProvider, RPSContext} = require('rps-engine-client-js/lib')

const ENGINE_HOST_NAME = '{ENGINE_HOST_NAME}' // Example: 'https://engine.rpsprod.ch'

// defining contexts
const rightsContext = new RPSContext([{name: 'Role', value: 'Admin'}])
const encryptProcessingContext = new RPSContext([{name: 'Action', value: 'Protect'}])
const decryptProcessingContext = new RPSContext([{name: 'Action', value: 'Deprotect'}])

// Request criteria
const Criteria = {
  TEMPLATE: 'Template',
  SUBSCRIBER: 'Subscriber',
  CAMPAIGN_SETTINGS: 'Campaign Settings'
}

const Instances = {
  [Criteria.TEMPLATE]: { // Request criteria
    text: { // Request criteria property
      className: 'Template',
      propertyName: 'Text'
    },
    name: { // Request criteria property
      className: 'Template',
      propertyName: 'Name'
    },
    description: { // Request criteria property
      className: 'Template',
      propertyName: 'Description'
    }
  },
  [Criteria.CAMPAIGN_SETTINGS]: {
    from_email_override: {
      className: 'Campaign Settings',
      propertyName: 'From email'
    },
    from_email: {
      className: 'Campaign Settings',
      propertyName: 'From email'
    },
    reply_to_override: {
      className: 'Campaign Settings',
      propertyName: 'Reply email'
    },
    reply_to: {
      className: 'Campaign Settings',
      propertyName: 'Reply email'
    }
  },
  [Criteria.SUBSCRIBER]: {
    email: {
      className: 'Subscriber',
      propertyName: 'Email'
    }
  }
}

const Actions = {
  ENCRYPT: 'ENCRYPT',
  DECRYPT: 'DECRYPT'
}

/**
 * EngineProvider class
 *
 * Helper class for initialization and using EngineClient via Request Criteria
 *
 * @param {TokenProvider} tokenProvider - instance of TokenProvider
 *
 */

class EngineProvider {
  constructor (tokenProvider) {
    // creating an instance of the class EngineClient using 'ENGINE_HOST_NAME' and 'tokenProvider'
    this.engineClient = new EngineClient({
      config: {baseURL: ENGINE_HOST_NAME},
      tokenProvider
    })
  }

  static getTransformedProperties (requestCriteria) {
    return Instances[requestCriteria]
  }

// generates a valid array of instances
  static generateInstances (instance, transformedProperties) {
    return Object.entries(transformedProperties)
      .reduce((acc, [property, {propertyName, className}]) => {
        const value = instance[property]
        return typeof value === 'undefined' ? acc : [...acc, {className, propertyName, value}]
      }, [])
  }

// generates a valid requestBody
  static generateRequestBody ({instance, transformedProperties, processingContext}) {
    const instances = EngineProvider.generateInstances(instance, transformedProperties)

    if (instances.length > 0) {
      return new RequestBuilder()
        .addRequest({instances, rightsContext, processingContext})
        .build()
    } else {
      return undefined
    }
  }

// parses response and substitutes the transformed values into the original object
  static parseResponse ({instance, response, transformedProperties}) {
    const {data: {responses = []}} = response || {}
    const {instances = []} = (Array.isArray(responses) && responses[0]) || {}

    return Object.entries(instance)
      .reduce((acc, [property, value]) => {
        const requestCriteriaProperty = transformedProperties[property]
        const processedValue = !!requestCriteriaProperty
          ? (instances.find(({propertyName}) => propertyName === requestCriteriaProperty.propertyName)).transformed
          : value

        return {...acc, [property]: processedValue}
      }, {})
  }

  async transform ({instance, requestBody, transformedProperties}) {
    try {
      const response = await this.engineClient.transform(requestBody)

      return EngineProvider.parseResponse({instance, response, transformedProperties})
    } catch (e) {
      console.error('error', e)
    }
  }

  async transformInstance (instance, requestCriteria, action = Actions.ENCRYPT) {
    const transformedProperties = EngineProvider.getTransformedProperties(requestCriteria)

    const needTransform = typeof transformedProperties !== 'undefined' && Object.keys(transformedProperties).length > 0

    if (!needTransform) {
      return instance
    } else {
      const requestBody = EngineProvider.generateRequestBody({
        instance,
        transformedProperties,
        processingContext: action === Actions.ENCRYPT ? encryptProcessingContext : decryptProcessingContext
      })
      const validRequestBody = typeof requestBody !== 'undefined'

      if (!validRequestBody) {
        return instance
      } else {
        return this.transform({instance, requestBody, transformedProperties})
      }
    }
  }

  // final method for ENCRYPT
  async encryptInstance (instance, requestCriteria) {
    return this.transformInstance(instance, requestCriteria, Actions.ENCRYPT)
  }

  // final method for DECRYPT
  async decryptInstance (instance, requestCriteria) {
    return this.transformInstance(instance, requestCriteria, Actions.DECRYPT)
  }
}


module.exports = {
  EngineProvider,
  TokenProvider,
  Criteria
}
Configuration file in the server directory:#
const {TokenProvider, EngineProvider, Criteria} = require('../../shared/engine');

const ENGINE_AUTH_HOST_NAME = '{AUTH_HOST_NAME}' // Example: 'https://identity.rpsprod.ch'

// configuration secrets
const CLIENT_ID = '{API_KEY_OF_CONFIGURATION}'; // Example: '518155d0-4774-467c-b891-6ebd3ea07f08'
const CLIENT_SECRET = '{SECRET_KEY_OF_CONFIGURATION}'; // Example: '75582a544bda425d956390844755f60369b9866026d8485aa49f7edcfe986bb1'
const secrets = {
  clientId: CLIENT_ID,
  clientSecret: CLIENT_SECRET
};

// creating an instance of the class TokenProvider using 'secrets' and 'ENGINE_AUTH_HOST_NAME'
const tokenProvider = new TokenProvider({
  identityServerHostName: ENGINE_AUTH_HOST_NAME,
  ...secrets
})

const engineProvider = new EngineProvider(tokenProvider)

module.exports = {
  engineProvider,
  tokenProvider,
  Criteria,
};
Engine authorization endpoint for getting token:#
const {tokenProvider} = require('../../lib/engine')

const passport = require('../../lib/passport')
const router = require('../../lib/router-async').create()

router.postAsync('/connect/token', passport.loggedIn, async (req, res) => {
  const tokenInfo = await tokenProvider.generateToken()

  return res.json(tokenInfo)
})

module.exports = router
Configuration file in the client directory:#
import {TokenProvider, EngineProvider, Criteria} from '../../../shared/engine'
import {getUrl} from './urls'

// ClientTokenProvider - Proxy token provider from backend
class ClientTokenProvider extends TokenProvider {
  constructor () {
    super({
      identityServerHostName: getUrl(),
      authEndpoint: 'rest/connect/token'
    })
  }

  // allows validation with empty secrets (clientId = undefined, clientSecret = undefined)
  _validateGenerateToken (secrets) {
    return true
  }
}

const clientTokenProvider = new ClientTokenProvider()
const engineProvider = new EngineProvider(clientTokenProvider)

export {
  clientTokenProvider,
  engineProvider,
  Criteria
}

Examples of usages#

Here you can see how data is encrypted in the client and how data is decrypted in the server by calling the RPS Engine transform endpoint via the RPS API library.

Client:#
import {engineProvider, Criteria} from '../../lib/engine';

...

const encryptMethod = instance => engineProvider.encryptInstance(instance, Criteria.SUBSCRIBER); // encrypt subscriber instance
Server:#
const {engineProvider, Criteria} = require('../../lib/engine');

...

// getSubscriberById endpoint
router.getAsync('/subscriptions/:listId/:subscriptionId', passport.loggedIn, async (req, res) => {
    const entity = await subscriptions.getById(req.context, castToInteger(req.params.listId), castToInteger(req.params.subscriptionId));
    entity.hash = await subscriptions.hashByList(castToInteger(req.params.listId), entity);
    const decryptedEntity = await engineProvider.decryptInstance(entity, Criteria.SUBSCRIBER);

    return res.json(decryptedEntity);
});

Examples of encryption#

The following screen-shots shows you how data is rendered by the frontend of MailTrain frontend in two scenarii:

  1. RPS is not used, there is no integration with RPS API library. This is how the data is stored in the database of MailTrain, so data is fully protected.
  2. RPS is used via the integration of RPS API library in MailTrain. Data is rendered in clear to the final user allowing to him to use it.