Source: operations/originators.js

'use strict';
/**
 * @module lib/operations/originators
 * @summary Whiteflag API originators endpoints handler module
 * @description Module with api originators endpoint handlers
 * @tutorial modules
 * @tutorial openapi
 */
module.exports = {
    // Endpoint handler functions
    getOriginators,
    getOriginator,
    updateOriginator,
    deleteOriginator,
    getPreSharedKey,
    storePreSharedKey,
    deletePreSharedKey,
    getAuthToken,
    storeAuthToken,
    deleteAuthToken
};

// Whiteflag common functions and classes //
const response = require('../common/httpres');
const array = require('../common/arrays');
const { hash } = require('../common/crypto');
const { ProcessingError } = require('../common/errors');

// Whiteflag modules //
const wfState = require('../protocol/state');

// Module constants //
const KEYIDLENGTH = 12;

// MAIN MODULE FUNCTIONS //
/**
 * Provides current state of all originators from state module
 * @function getOriginators
 * @alias module:lib/operations/originators.getOriginators
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function getOriginators(req, res, operationId, callback) {
    wfState.getOriginators(function endpointGetOriginatorsCb(err, originatorsState = null) {
        // Create response body and preserve information before responding
        let resBody = response.createBody(req, operationId);

        // Send response using common endpoint response function
        if (!err && !originatorsState) err = new Error('Could not retrieve known originators from state');
        if (originatorsState) resBody.meta.info = array.addItem(resBody.meta.info, 'Currently known originators from state');
        return response.sendIndicative(res, err, resBody, originatorsState, callback);
    });
}

/**
 * Provides current originator state from state module
 * @function getOriginator
 * @alias module:lib/operations/originators.getOriginator
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function getOriginator(req, res, operationId, callback) {
    const originatorAddress = req.params.address;
    wfState.getOriginatorData(originatorAddress, function endpointGetOriginatorCb(err, originatorData = null) {
        // Create response body and preserve information before responding
        let resBody = response.createBody(req, operationId);

        // Send response using common endpoint response function
        if (!err && !originatorData) err = new ProcessingError(`Unknown originator: ${originatorAddress}`, null, 'WF_API_NO_RESOURCE');
        if (!err) {
            resBody.meta.originator = originatorAddress;
            resBody.meta.info = array.addItem(resBody.meta.info, 'Current originator state');
        }
        return response.sendIndicative(res, err, resBody, originatorData, callback);
    });
}

/**
 * Updates originator state data
 * @function updateOriginator
 * @alias module:lib/operations/originators.updateOriginator
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function updateOriginator(req, res, operationId, callback) {
    const originatorAddress = req.params.address;
    const originatorData = req.body;

    // Check if originator exists
    wfState.getOriginatorData(originatorAddress, function endpointUpdateOriginatorCb(err, data = null) {
        // Create response body and preserve information before responding
        let resBody = response.createBody(req, operationId);

        // Check orginator data
        if (!err) {
            if (!data) {
                err = new ProcessingError(`Unknown originator: ${originatorAddress}`, null, 'WF_API_NO_RESOURCE');
            } else {
                resBody.meta.originator = originatorAddress;
            }
            // Add and check address for correct update
            if (!originatorData.address) originatorData.address = originatorAddress;
            if (originatorData.address !== originatorAddress) {
                err = new ProcessingError(`Different originator address in request body: ${originatorData.address}`, null, 'WF_API_BAD_REQUEST');
            }
        }
        // Upsert originator data
        if (!err) {
            resBody.meta.resource = `/originators/${originatorAddress}`;
            resBody.meta.info = array.addItem(resBody.meta.info, 'Accepted request to update originator');
            wfState.upsertOriginatorData(originatorData);
        }
        return response.sendIndicative(res, err, resBody, originatorData, callback);
    });
}

/**
 * Updates originator state data
 * @function deleteOriginator
 * @alias module:lib/operations/originators.deleteOriginator
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function deleteOriginator(req, res, operationId, callback) {
    const originatorAddress = req.params.address;

    // Check if originator exists
    wfState.getOriginatorData(originatorAddress, function endpointUpdateOriginatorCb(err, data = {}) {
        // Create response body and preserve information before responding
        let resBody = response.createBody(req, operationId);

        // Check orginator data
        if (!err && !data) {
            err = new ProcessingError(`Unknown originator: ${originatorAddress}`, null, 'WF_API_NO_RESOURCE');
        } else {
            resBody.meta.originator = originatorAddress;
        }
        // Delete originator data
        if (!err) {
            resBody.meta.info = array.addItem(resBody.meta.info, 'Accepted request to delete originator');
            wfState.removeOriginatorData(originatorAddress);
        }
        return response.sendIndicative(res, err, resBody, data, callback);
    });
}

/**
 * Checks for pre-shared key for an originator
 * @function getPreSharedKey
 * @alias module:lib/operations/originators.getPreSharedKey
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function getPreSharedKey(req, res, operationId, callback) {
    const originatorAddress = req.params.address;
    const accountAddress = req.params.account;
    let resBody = response.createBody(req, operationId);

    // Get originator and check for errors
    wfState.getOriginatorData(originatorAddress, function endpointGetOriginatorCb(err, originatorData = null) {
        if (!err && !originatorData) err = new ProcessingError(`Unknown originator: ${originatorAddress}`, null, 'WF_API_NO_RESOURCE');
        if (!err) resBody.meta.originator = originatorAddress;
        if (!err && !originatorData.blockchain) err = new Error('Could not determine blockchain for originator');
        if (err) return response.sendImperative(res, err, resBody, originatorData, callback);

        if (!err) {
            // Add metadata to response
            resBody.meta.blockchain = originatorData.blockchain;
            resBody.meta.account = accountAddress;

            // Get pre-shared key
            const keyId = hash(originatorData.blockchain + originatorAddress + accountAddress, KEYIDLENGTH);
            wfState.getKey('presharedKeys', keyId, function endpointGetOriginatorKeyCb(err, key) {
                if (!err && !key) err = new ProcessingError(`No pre-shared key with this originator available for use with blockchain account ${accountAddress}`, null, 'WF_API_NO_RESOURCE');
                if (key) {
                    resBody.meta.keyId = keyId;
                    if (!err) {
                        resBody.meta.info = array.addItem(resBody.meta.info, 'Pre-shared key exists for this account and originator');
                        resBody.meta.info = array.addItem(resBody.meta.info, 'Returning originator data related to pre-shared key');
                    }
                }
                key = undefined;
                return response.sendImperative(res, err, resBody, originatorData, callback);
            });
        }
    });
}

/**
 * Stores or updates pre-shared key for an originator
 * @function storePreSharedKey
 * @alias module:lib/operations/originators.storePreSharedKey
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function storePreSharedKey(req, res, operationId, callback) {
    const originatorAddress = req.params.address;
    const accountAddress = req.params.account;
    let psk = req.body.preSharedKey || null;
    req.body.preSharedKey = undefined;
    let resBody = response.createBody(req, operationId);

    // Get originator and check for errors
    wfState.getOriginatorData(originatorAddress, function endpointGetOriginatorCb(err, originatorData = null) {
        if (!err && !originatorData) err = new ProcessingError(`Unknown originator: ${originatorAddress}`, null, 'WF_API_NO_RESOURCE');
        if (!err) resBody.meta.originator = originatorAddress;
        if (!err && !psk) err = new ProcessingError('No pre-shared key provided', null, 'WF_API_BAD_REQUEST');
        if (!err && !originatorData.blockchain) err = new Error('Could not determine blockchain for originator');
        if (err) return response.sendImperative(res, err, resBody, originatorData, callback);

        // Check blockchain account
        wfState.getBlockchainData(originatorData.blockchain, function endpointGetBlockchainCb(err, blockchainState) {
            if (!err && !blockchainState) err = new Error(`Blockchain ${originatorData.blockchain} does not exist in state`);
            if (!err) {
                const index = blockchainState.accounts.findIndex(account => account.address.toLowerCase() === accountAddress.toLowerCase());
                if (index < 0) err = new ProcessingError(`Blockchain account ${accountAddress} does not exist`, null, 'WF_API_NO_RESOURCE');
            }
            // If no errors, store the pre-shared key (async)
            if (!err) {
                // Add metadata to response
                resBody.meta.blockchain = originatorData.blockchain;
                resBody.meta.account = accountAddress;
                resBody.meta.resource = `/originators/${originatorAddress}/psk/${accountAddress}`;

                // Store pre-shared key
                const keyId = hash(originatorData.blockchain + originatorAddress + accountAddress, KEYIDLENGTH);
                resBody.meta.keyId = keyId;
                resBody.meta.info = array.addItem(resBody.meta.info, 'Accepted request to store pre-shared key');
                wfState.upsertKey('presharedKeys', keyId, psk);
                psk = undefined;
            }
            return response.sendImperative(res, err, resBody, originatorData, callback);
        });
    });
}

/**
 * Deletes pre-shared key for an originator
 * @function deletePreSharedKey
 * @alias module:lib/operations/originators.deletePreSharedKey
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function deletePreSharedKey(req, res, operationId, callback) {
    const originatorAddress = req.params.address;
    const accountAddress = req.params.account;
    let resBody = response.createBody(req, operationId);

    // Get originator and delete pre-shared key (async)
    wfState.getOriginatorData(originatorAddress, function endpointGetOriginatorCb(err, originatorData = null) {
        if (!err && !originatorData) err = new ProcessingError(`Unknown originator: ${originatorAddress}`, null, 'WF_API_NO_RESOURCE');
        if (!err) resBody.meta.originator = originatorAddress;
        if (!err && !originatorData.blockchain) err = new Error('Unknown blockchain for originator');
        if (!err) {
            // Add metadata to response
            resBody.meta.blockchain = originatorData.blockchain;
            resBody.meta.account = accountAddress;

            // Determine key id and remove key
            const keyId = hash(originatorData.blockchain + originatorAddress + accountAddress, KEYIDLENGTH);
            resBody.meta.keyId = keyId;
            resBody.meta.info = array.addItem(resBody.meta.info, 'Accepted request to delete pre-shared key');
            wfState.removeKey('presharedKeys', keyId);
        }
        return response.sendImperative(res, err, resBody, originatorData, callback);
    });
}

/**
 * Checks for an authentication token of an originator
 * @function getAuthToken
 * @alias module:lib/operations/originators.getAuthToken
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function getAuthToken(req, res, operationId, callback) {
    const authTokenId = req.params.authTokenId;
    let resBody = response.createBody(req, operationId);

    // Get authentication token
    wfState.getKey('authTokens', authTokenId, function getAuthTokenGetKeyCb(err, existingAuthToken) {
        if (!err && !existingAuthToken) {
            err = new ProcessingError(`No authentication token with token id ${authTokenId}`, null, 'WF_API_NO_RESOURCE');
            return response.sendImperative(res, err, resBody, null, callback);
        }
        existingAuthToken = undefined;
        resBody.meta.authTokenId = authTokenId;
        resBody.meta.info = array.addItem(resBody.meta.info, 'Authentication token exists');

        // Get originator data and send response
        wfState.getOriginatorAuthToken(authTokenId, function getOriginatorAuthTokenCb(err, originatorData) {
            if (!err && !originatorData) {
                err = new ProcessingError(`No originator found for token id ${authTokenId}`, null, 'WF_API_NO_DATA');
            }
            if (!err) resBody.meta.info = array.addItem(resBody.meta.info, 'Returning originator data for authentication token');
            return response.sendImperative(res, err, resBody, originatorData, callback);
        });
    });
}

/**
 * Stores an authentication token for an originator
 * @function storeAuthToken
 * @alias module:lib/operations/originators.storeAuthToken
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function storeAuthToken(req, res, operationId, callback) {
    let authToken = req.body.secret || null;
    req.body.secret = undefined;
    let originatorData = {};
    let resBody = response.createBody(req, operationId);

    // Check for errors
    let errors = [];
    if (!authToken) errors.push('No authentication token provided');
    if (!req.body.name) errors.push('No originator name provided');
    if (!req.body.blockchain) errors.push('No blockchain provided');
    if (errors.length > 0) {
        let err = new ProcessingError('Invalid authentication token data', errors, 'WF_API_BAD_REQUEST');
        return response.sendImperative(res, err, resBody, originatorData, callback);
    }
    // Check for existing authentication token
    const authTokenId = hash(originatorData.blockchain + authToken, KEYIDLENGTH);
    resBody.meta.authTokenId = authTokenId;
    resBody.meta.resource = `/originators/tokens/${authTokenId}`;
    wfState.getKey('authTokens', authTokenId, function storeAuthTokenGetKeyCb(err, existingAuthToken) {
        // Check for errors and existing authentication token
        if (err || existingAuthToken) {
            existingAuthToken = undefined;
            if (!err) err = new ProcessingError('Authentication token already exists', null, 'WF_API_RESOURCE_CONFLICT');
            return response.sendImperative(res, err, resBody, originatorData, callback);
        }
        // Store token
        resBody.meta.info = array.addItem(resBody.meta.info, 'Accepted request to store authentication token');
        wfState.upsertKey('authTokens', authTokenId, authToken);
        authToken = undefined;

        // Store originator data
        originatorData.name = req.body.name;
        originatorData.blockchain = req.body.blockchain;
        if (req.body.address) originatorData.address = req.body.address;
        originatorData.authTokenId = authTokenId;
        wfState.upsertOriginatorData(originatorData);

        // Send response
        return response.sendImperative(res, err, resBody, originatorData, callback);
    });
}

/**
 * Deletes an authentication token of an originator
 * @function deleteAuthToken
 * @alias module:lib/operations/originators.deleteAuthToken
 * @param {Object} req the http request
 * @param {Object} res the http response
 * @param {string} operationId the operation id as defined in the openapi definition
 * @param {logEndpointEventCb} callback
 */
function deleteAuthToken(req, res, operationId, callback) {
    const authTokenId = req.params.authTokenId;
    let resBody = response.createBody(req, operationId);

    // Check if key exists before deleting
    wfState.getKey('authTokens', authTokenId, function getAuthTokenGetKeyCb(err, existingAuthToken) {
        if (!err && !existingAuthToken) {
            err = new ProcessingError(`No authentication token with token id ${authTokenId}`, null, 'WF_API_NO_RESOURCE');
            return response.sendImperative(res, err, resBody, null, callback);
        }
        existingAuthToken = undefined;
        resBody.meta.authTokenId = authTokenId;

        // Get originator data before deleting
        wfState.getOriginatorAuthToken(authTokenId, function getOriginatorAuthTokenCb(err, originatorData) {
            if (!originatorData) originatorData = {};
            if (!err) {
                resBody.meta.info = array.addItem(resBody.meta.info, 'Accepted request to delete authentication token');
                wfState.removeOriginatorAuthToken(authTokenId);
            }
            return response.sendImperative(res, err, resBody, originatorData, callback);
        });
    });
}