From 01e72d6e15c15970fb77eb51ecea4b8cdcb35aab Mon Sep 17 00:00:00 2001 From: Kenneth Mutombo Date: Tue, 12 Oct 2021 22:36:46 +0200 Subject: [PATCH 1/6] Adds refactoring for the getclients method --- src/api/metadata.js | 492 +++++++++++++++++++++----------------------- 1 file changed, 240 insertions(+), 252 deletions(-) diff --git a/src/api/metadata.js b/src/api/metadata.js index 06a1c4c3..63c66e2c 100644 --- a/src/api/metadata.js +++ b/src/api/metadata.js @@ -1,252 +1,240 @@ -'use strict' - -import logger from 'winston' - -import * as authorisation from './authorisation' -import * as utils from '../utils' -import {ChannelModelAPI} from '../model/channels' -import {ClientModelAPI} from '../model/clients' -import {ContactGroupModelAPI} from '../model/contactGroups' -import {KeystoreModelAPI} from '../model/keystore' -import {MediatorModelAPI} from '../model/mediators' -import {UserModelAPI} from '../model/users' -import * as polling from '../polling' - -// Map string parameters to collections -const collections = { - Channels: ChannelModelAPI, - Clients: ClientModelAPI, - Mediators: MediatorModelAPI, - Users: UserModelAPI, - ContactGroups: ContactGroupModelAPI, - KeystoreModelAPI -} - -// Function to remove properties from export object -function removeProperties(obj) { - const propertyID = '_id' - const propertyV = '__v' - - for (const prop in obj) { - if (prop === propertyID || prop === propertyV) { - delete obj[prop] - } else if (typeof obj[prop] === 'object' || obj[prop] instanceof Array) { - removeProperties(obj[prop]) - } - } - return obj -} - -// Function to return unique identifier key and value for a collection -function getUniqueIdentifierForCollection(collection, doc) { - let uid - let uidKey - switch (collection) { - case 'Channels': - uidKey = 'name' - uid = doc.name - break - case 'Clients': - uidKey = 'clientID' - uid = doc.clientID - break - case 'Mediators': - uidKey = 'urn' - uid = doc.urn - break - case 'Users': - uidKey = 'email' - uid = doc.email - break - case 'ContactGroups': - uidKey = 'groups' - uid = doc.groups - break - default: - logger.debug( - `Unhandeled case for ${collection} in getUniqueIdentifierForCollection` - ) - break - } - const returnObj = {} - returnObj[uidKey] = uid - return returnObj -} - -// Build response object -function buildResponseObject(model, doc, status, message, uid) { - return { - model, - record: doc, - status, - message, - uid - } -} - -// API endpoint that returns metadata for export -export async function getMetadata(ctx) { - // Test if the user is authorised - if (!authorisation.inGroup('admin', ctx.authenticated)) { - return utils.logAndSetResponse( - ctx, - 403, - `User ${ctx.authenticated.email} is not an admin, API access to getMetadata denied.`, - 'info' - ) - } - - try { - const exportObject = {} - - // Return all documents from all collections for export - for (const col in collections) { - exportObject[col] = await collections[col].find().lean().exec() - for (let doc of Array.from(exportObject[col])) { - if (doc._id) { - doc = removeProperties(doc) - } - } - } - - ctx.body = [exportObject] - ctx.status = 200 - } catch (e) { - ctx.body = e.message - utils.logAndSetResponse( - ctx, - 500, - `Could not fetch specified metadata via the API ${e}`, - 'error' - ) - } -} - -async function handleMetadataPost(ctx, action) { - // Test if the user is authorised - if (!authorisation.inGroup('admin', ctx.authenticated)) { - return utils.logAndSetResponse( - ctx, - 403, - `User ${ctx.authenticated.email} is not an admin, API access to importMetadata denied.`, - 'info' - ) - } - - try { - let status - const returnObject = [] - const insertObject = ctx.request.body - - for (const key in insertObject) { - const insertDocuments = insertObject[key] - for (let doc of Array.from(insertDocuments)) { - let error - let uid - try { - let result - if (!(key in collections)) { - throw new Error('Invalid Collection in Import Object') - } - - // Keystore model does not have a uid other than _id and may not contain more than one entry - if (key === 'Keystore') { - result = await collections[key].find().exec() - uid = '' - } else { - const uidObj = getUniqueIdentifierForCollection(key, doc) - uid = uidObj[Object.keys(uidObj)[0]] - result = await collections[key].find(uidObj).exec() - } - - if (action === 'import') { - if (result && result.length > 0 && result[0]._id) { - if (doc._id) { - delete doc._id - } - result = await collections[key].findById(result[0]._id).exec() - result.set(doc) - result.set( - 'updatedBy', - utils.selectAuditFields(ctx.authenticated) - ) - result = await result.save() - status = 'Updated' - } else { - doc = new collections[key](doc) - doc.set('updatedBy', utils.selectAuditFields(ctx.authenticated)) - result = await doc.save() - status = 'Inserted' - } - - // Ideally we should rather use our APIs to insert object rather than go directly to the DB - // Then we would have to do this sort on thing as it's already covered there. - // E.g. https://github.com/jembi/openhim-core-js/blob/cd7d1fbbe0e122101186ecba9cf1de37711580b8/src/api/channels.js#L241-L257 - if ( - key === 'Channels' && - result.type === 'polling' && - result.status === 'enabled' - ) { - polling.registerPollingChannel(result, err => { - logger.error(err) - }) - } - } - - if (action === 'validate') { - if (result && result.length > 0 && result[0]._id) { - status = 'Conflict' - } else { - doc = new collections[key](doc) - doc.set('updatedBy', utils.selectAuditFields(ctx.authenticated)) - error = doc.validateSync() - if (error) { - throw new Error(`Document Validation failed: ${error}`) - } - status = 'Valid' - } - } - - logger.info( - `User ${ctx.authenticated.email} performed ${action} action on ${key}, got ${status}` - ) - returnObject.push(buildResponseObject(key, doc, status, '', uid)) - } catch (err) { - logger.error( - `Failed to ${action} ${key} with unique identifier ${uid}. ${err.message}` - ) - returnObject.push( - buildResponseObject(key, doc, 'Error', err.message, uid) - ) - } - } - } - - ctx.body = returnObject - ctx.status = 201 - } catch (error2) { - ctx.body = error2.message - utils.logAndSetResponse( - ctx, - 500, - `Could not import metadata via the API ${error2}`, - 'error' - ) - } -} - -// API endpoint that upserts metadata -export async function importMetadata(ctx) { - return handleMetadataPost(ctx, 'import') -} - -// API endpoint that checks for conflicts between import object and database -export async function validateMetadata(ctx) { - return handleMetadataPost(ctx, 'validate') -} - -if (process.env.NODE_ENV === 'test') { - exports.buildResponseObject = buildResponseObject - exports.getUniqueIdentifierForCollection = getUniqueIdentifierForCollection - exports.removeProperties = removeProperties -} +'use strict' +import logger from 'winston' +import * as authorisation from './authorisation' +import * as utils from '../utils' +import {ChannelModelAPI} from '../model/channels' +import {ClientModelAPI} from '../model/clients' +import {ContactGroupModelAPI} from '../model/contactGroups' +import {KeystoreModelAPI} from '../model/keystore' +import {MediatorModelAPI} from '../model/mediators' +import {UserModelAPI} from '../model/users' +import {getClients} from '../api/clients' +import {getChannels} from '../api/channels' +import * as polling from '../polling' +// Map string parameters to collections +const collections = { + Channels: ChannelModelAPI, + Clients: ClientModelAPI, + Mediators: MediatorModelAPI, + Users: UserModelAPI, + ContactGroups: ContactGroupModelAPI, + KeystoreModelAPI +} +// Function to remove properties from export object +function removeProperties(obj) { + const propertyID = '_id' + const propertyV = '__v' + for (const prop in obj) { + if (prop === propertyID || prop === propertyV) { + delete obj[prop] + } else if (typeof obj[prop] === 'object' || obj[prop] instanceof Array) { + removeProperties(obj[prop]) + } + } + return obj +} +// Function to return unique identifier key and value for a collection +function getUniqueIdentifierForCollection(collection, doc) { + let uid + let uidKey + switch (collection) { + case 'Channels': + uidKey = 'name' + uid = doc.name + break + case 'Clients': + uidKey = 'clientID' + uid = doc.clientID + break + case 'Mediators': + uidKey = 'urn' + uid = doc.urn + break + case 'Users': + uidKey = 'email' + uid = doc.email + break + case 'ContactGroups': + uidKey = 'groups' + uid = doc.groups + break + default: + logger.debug( + `Unhandeled case for ${collection} in getUniqueIdentifierForCollection` + ) + break + } + const returnObj = {} + returnObj[uidKey] = uid + return returnObj +} +// Build response object +function buildResponseObject(model, doc, status, message, uid) { + return { + model, + record: doc, + status, + message, + uid + } +} +// API endpoint that returns metadata for export +export async function getMetadata(ctx) { + // Test if the user is authorised + if (!authorisation.inGroup('admin', ctx.authenticated)) { + return utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to getMetadata denied.`, + 'info' + ) + } + try { + const exportObject = {} + // Return all documents from all collections for export + for (const model in collections) { + switch(model) { + case 'Clients': + await getClients(ctx); + console.log('clientData: ' + JSON.stringify(ctx.body)); + exportObject[model] = ctx.body; + break; + default: + exportObject[model] = await collections[model].find().lean().exec(); + break; + } + for (let doc of Array.from(exportObject[model])) { + if (doc._id) { + doc = removeProperties(doc) + } + } + } + ctx.body = [exportObject] + ctx.status = 200 + } catch (e) { + ctx.body = e.message + utils.logAndSetResponse( + ctx, + 500, + `Could not fetch specified metadata via the API ${e}`, + 'error' + ) + } +} +async function handleMetadataPost(ctx, action) { + // Test if the user is authorised + if (!authorisation.inGroup('admin', ctx.authenticated)) { + return utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to importMetadata denied.`, + 'info' + ) + } + try { + let status + const returnObject = [] + const insertObject = ctx.request.body + for (const key in insertObject) { + const insertDocuments = insertObject[key] + for (let doc of Array.from(insertDocuments)) { + let error + let uid + try { + let result + if (!(key in collections)) { + throw new Error('Invalid Collection in Import Object') + } + // Keystore model does not have a uid other than _id and may not contain more than one entry + if (key === 'Keystore') { + result = await collections[key].find().exec() + uid = '' + } else { + const uidObj = getUniqueIdentifierForCollection(key, doc) + uid = uidObj[Object.keys(uidObj)[0]] + result = await collections[key].find(uidObj).exec() + } + if (action === 'import') { + if (result && result.length > 0 && result[0]._id) { + if (doc._id) { + delete doc._id + } + result = await collections[key].findById(result[0]._id).exec() + result.set(doc) + result.set( + 'updatedBy', + utils.selectAuditFields(ctx.authenticated) + ) + result = await result.save() + status = 'Updated' + } else { + doc = new collections[key](doc) + doc.set('updatedBy', utils.selectAuditFields(ctx.authenticated)) + result = await doc.save() + status = 'Inserted' + } + // Ideally we should rather use our APIs to insert object rather than go directly to the DB + // Then we would have to do this sort on thing as it's already covered there. + // E.g. https://github.com/jembi/openhim-core-js/blob/cd7d1fbbe0e122101186ecba9cf1de37711580b8/src/api/channels.js#L241-L257 + if ( + key === 'Channels' && + result.type === 'polling' && + result.status === 'enabled' + ) { + polling.registerPollingChannel(result, err => { + logger.error(err) + }) + } + } + if (action === 'validate') { + if (result && result.length > 0 && result[0]._id) { + status = 'Conflict' + } else { + doc = new collections[key](doc) + doc.set('updatedBy', utils.selectAuditFields(ctx.authenticated)) + error = doc.validateSync() + if (error) { + throw new Error(`Document Validation failed: ${error}`) + } + status = 'Valid' + } + } + logger.info( + `User ${ctx.authenticated.email} performed ${action} action on ${key}, got ${status}` + ) + returnObject.push(buildResponseObject(key, doc, status, '', uid)) + } catch (err) { + logger.error( + `Failed to ${action} ${key} with unique identifier ${uid}. ${err.message}` + ) + returnObject.push( + buildResponseObject(key, doc, 'Error', err.message, uid) + ) + } + } + } + ctx.body = returnObject + ctx.status = 201 + } catch (error2) { + ctx.body = error2.message + utils.logAndSetResponse( + ctx, + 500, + `Could not import metadata via the API ${error2}`, + 'error' + ) + } +} +// API endpoint that upserts metadata +export async function importMetadata(ctx) { + return handleMetadataPost(ctx, 'import') +} +// API endpoint that checks for conflicts between import object and database +export async function validateMetadata(ctx) { + return handleMetadataPost(ctx, 'validate') +} +if (process.env.NODE_ENV === 'test') { + exports.buildResponseObject = buildResponseObject + exports.getUniqueIdentifierForCollection = getUniqueIdentifierForCollection + exports.removeProperties = removeProperties +} \ No newline at end of file From 68d74fd045329a62d77049262479ad33af22311d Mon Sep 17 00:00:00 2001 From: Kenneth Mutombo Date: Sat, 16 Oct 2021 10:13:09 +0200 Subject: [PATCH 2/6] Adds method to Check if client exists --- src/api/metadata.js | 118 +++++++++++++++++++++++++++++--------------- 1 file changed, 78 insertions(+), 40 deletions(-) diff --git a/src/api/metadata.js b/src/api/metadata.js index 63c66e2c..b532f38a 100644 --- a/src/api/metadata.js +++ b/src/api/metadata.js @@ -8,9 +8,10 @@ import {ContactGroupModelAPI} from '../model/contactGroups' import {KeystoreModelAPI} from '../model/keystore' import {MediatorModelAPI} from '../model/mediators' import {UserModelAPI} from '../model/users' -import {getClients} from '../api/clients' +import {getClient, getClients, addClient, updateClient} from '../api/clients' import {getChannels} from '../api/channels' import * as polling from '../polling' + // Map string parameters to collections const collections = { Channels: ChannelModelAPI, @@ -20,6 +21,7 @@ const collections = { ContactGroups: ContactGroupModelAPI, KeystoreModelAPI } + // Function to remove properties from export object function removeProperties(obj) { const propertyID = '_id' @@ -33,6 +35,7 @@ function removeProperties(obj) { } return obj } + // Function to return unique identifier key and value for a collection function getUniqueIdentifierForCollection(collection, doc) { let uid @@ -95,10 +98,15 @@ export async function getMetadata(ctx) { for (const model in collections) { switch(model) { case 'Clients': - await getClients(ctx); + await getClients(ctx); console.log('clientData: ' + JSON.stringify(ctx.body)); exportObject[model] = ctx.body; break; + case 'Channels': + await getChannels(ctx); + console.log('channelData: ' + JSON.stringify(ctx.body)); + exportObject[model] = ctx.body; + break; default: exportObject[model] = await collections[model].find().lean().exec(); break; @@ -121,6 +129,17 @@ export async function getMetadata(ctx) { ) } } +function clientExists(clientResult) { + return clientResult && clientResult.length > 0 && clientResult[0]._id; +} +function validateDocument(key, doc, ctx) { + doc = new collections[key](doc) + doc.set('updatedBy', utils.selectAuditFields(ctx.authenticated)) + const error = doc.validateSync() + if (error) { + throw new Error(`Document Validation failed: ${error}`) + } +} async function handleMetadataPost(ctx, action) { // Test if the user is authorised if (!authorisation.inGroup('admin', ctx.authenticated)) { @@ -154,49 +173,68 @@ async function handleMetadataPost(ctx, action) { uid = uidObj[Object.keys(uidObj)[0]] result = await collections[key].find(uidObj).exec() } - if (action === 'import') { - if (result && result.length > 0 && result[0]._id) { - if (doc._id) { - delete doc._id + if(key === 'Clients') { + const clientResult = getClient(ctx, doc.clientID); + if(action === 'import') { + if (clientExists(clientResult)) { + await updateClient(ctx, doc.clientID); + status = 'Updated'; + } + else { + await addClient(ctx); //must modify ctx body to only include this particular client data block + status = 'Inserted'; } - result = await collections[key].findById(result[0]._id).exec() - result.set(doc) - result.set( - 'updatedBy', - utils.selectAuditFields(ctx.authenticated) - ) - result = await result.save() - status = 'Updated' - } else { - doc = new collections[key](doc) - doc.set('updatedBy', utils.selectAuditFields(ctx.authenticated)) - result = await doc.save() - status = 'Inserted' } - // Ideally we should rather use our APIs to insert object rather than go directly to the DB - // Then we would have to do this sort on thing as it's already covered there. - // E.g. https://github.com/jembi/openhim-core-js/blob/cd7d1fbbe0e122101186ecba9cf1de37711580b8/src/api/channels.js#L241-L257 - if ( - key === 'Channels' && - result.type === 'polling' && - result.status === 'enabled' - ) { - polling.registerPollingChannel(result, err => { - logger.error(err) - }) + if(action === 'validate') { + if (clientExists(clientResult)) { + status = 'Conflict'; + } + else { + validateDocument(key, doc, ctx); + status = 'Valid' + } } } - if (action === 'validate') { - if (result && result.length > 0 && result[0]._id) { - status = 'Conflict' - } else { - doc = new collections[key](doc) - doc.set('updatedBy', utils.selectAuditFields(ctx.authenticated)) - error = doc.validateSync() - if (error) { - throw new Error(`Document Validation failed: ${error}`) + else { + if (action === 'import') { + if (result && result.length > 0 && result[0]._id) { + if (doc._id) { + delete doc._id + } + result = await collections[key].findById(result[0]._id).exec() + result.set(doc) + result.set( + 'updatedBy', + utils.selectAuditFields(ctx.authenticated) + ) + result = await result.save() + status = 'Updated' + } else { + doc = new collections[key](doc) + doc.set('updatedBy', utils.selectAuditFields(ctx.authenticated)) + result = await doc.save() + status = 'Inserted' + } + // Ideally we should rather use our APIs to insert object rather than go directly to the DB + // Then we would have to do this sort on thing as it's already covered there. + // E.g. https://github.com/jembi/openhim-core-js/blob/cd7d1fbbe0e122101186ecba9cf1de37711580b8/src/api/channels.js#L241-L257 + if ( + key === 'Channels' && + result.type === 'polling' && + result.status === 'enabled' + ) { + polling.registerPollingChannel(result, err => { + logger.error(err) + }) + } + } + if (action === 'validate') { + if (result && result.length > 0 && result[0]._id) { + status = 'Conflict' + } else { + validateDocument(key, doc, ctx); + status = 'Valid' } - status = 'Valid' } } logger.info( From 31f74331041ec9b142a0fab3fab8e49c0b675f89 Mon Sep 17 00:00:00 2001 From: Kenneth Mutombo Date: Sat, 16 Oct 2021 19:45:40 +0200 Subject: [PATCH 3/6] WIP: adds AddsDoesExst method --- src/api/metadata.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/api/metadata.js b/src/api/metadata.js index b532f38a..dbe110bd 100644 --- a/src/api/metadata.js +++ b/src/api/metadata.js @@ -11,6 +11,7 @@ import {UserModelAPI} from '../model/users' import {getClient, getClients, addClient, updateClient} from '../api/clients' import {getChannels} from '../api/channels' import * as polling from '../polling' +import { compareSync } from 'bcryptjs' // Map string parameters to collections const collections = { @@ -99,18 +100,19 @@ export async function getMetadata(ctx) { switch(model) { case 'Clients': await getClients(ctx); - console.log('clientData: ' + JSON.stringify(ctx.body)); exportObject[model] = ctx.body; break; case 'Channels': await getChannels(ctx); - console.log('channelData: ' + JSON.stringify(ctx.body)); exportObject[model] = ctx.body; break; default: exportObject[model] = await collections[model].find().lean().exec(); break; } + + console.log('array from : '+ JSON.stringify(Array.from(exportObject[model]))); + for (let doc of Array.from(exportObject[model])) { if (doc._id) { doc = removeProperties(doc) @@ -129,9 +131,17 @@ export async function getMetadata(ctx) { ) } } + function clientExists(clientResult) { return clientResult && clientResult.length > 0 && clientResult[0]._id; } + +function doesExist(ctx) { + //we need to get the + const clientResult = ctx.request.body; + return clientResult && clientResult.length > 0 && clientResult[0]._id; +} + function validateDocument(key, doc, ctx) { doc = new collections[key](doc) doc.set('updatedBy', utils.selectAuditFields(ctx.authenticated)) @@ -140,6 +150,7 @@ function validateDocument(key, doc, ctx) { throw new Error(`Document Validation failed: ${error}`) } } + async function handleMetadataPost(ctx, action) { // Test if the user is authorised if (!authorisation.inGroup('admin', ctx.authenticated)) { From 2d840a43352ee757d87e9e1db6dc97801bd5b930 Mon Sep 17 00:00:00 2001 From: Kenneth Mutombo Date: Sat, 16 Oct 2021 22:37:53 +0200 Subject: [PATCH 4/6] Adds logic to insert a new client via the metadata endpoint --- src/api/channels.js | 1284 ++++++++++++++++++++++--------------------- src/api/clients.js | 574 +++++++++---------- src/api/metadata.js | 65 ++- src/koaApi.js | 386 ++++++------- 4 files changed, 1185 insertions(+), 1124 deletions(-) diff --git a/src/api/channels.js b/src/api/channels.js index 80d5d541..513fe03e 100644 --- a/src/api/channels.js +++ b/src/api/channels.js @@ -1,616 +1,668 @@ -'use strict' - -import logger from 'winston' -import axios from 'axios' - -import * as Channels from '../model/channels' -import * as authorisation from './authorisation' -import * as polling from '../polling' -import * as routerMiddleware from '../middleware/router' -import * as server from '../server' -import * as tcpAdapter from '../tcpAdapter' -import * as utils from '../utils' -import {TransactionModelAPI} from '../model/transactions' -import {config} from '../config' - -const {ChannelModel} = Channels - -const MAX_BODY_AGE_MESSAGE = `Channel property maxBodyAgeDays has to be a number that's valid and requestBody or responseBody must be true.` -const TIMEOUT_SECONDS_MESSAGE = `Channel property timeoutSeconds has to be a number greater than 1 and less than an 3600` - -config.polling = config.get('polling') - -function isPathValid(channel) { - if (channel.routes != null) { - for (const route of Array.from(channel.routes)) { - // There cannot be both path and pathTransform. pathTransform must be valid - if ( - (route.path && route.pathTransform) || - (route.pathTransform && !/s\/.*\/.*/.test(route.pathTransform)) - ) { - return false - } - } - } - return true -} - -/* - * Retrieves the list of active channels - */ -export async function getChannels(ctx) { - try { - ctx.body = await authorisation.getUserViewableChannels(ctx.authenticated) - } catch (err) { - utils.logAndSetResponse( - ctx, - 500, - `Could not fetch all channels via the API: ${err}`, - 'error' - ) - } -} - -function processPostAddTriggers(channel) { - if (channel.type && Channels.isChannelEnabled(channel)) { - if ( - (channel.type === 'tcp' || channel.type === 'tls') && - server.isTcpHttpReceiverRunning() - ) { - return tcpAdapter.notifyMasterToStartTCPServer(channel._id, err => { - if (err) { - return logger.error(err) - } - }) - } else if (channel.type === 'polling') { - return polling.registerPollingChannel(channel, err => { - if (err) { - return logger.error(err) - } - }) - } - } -} - -export function validateMethod(channel) { - const {methods = []} = channel || {} - if (methods.length === 0) { - return - } - - if (!/http/i.test(channel.type || 'http')) { - return `Channel method can't be defined if channel type is not http` - } - - const mapCount = methods.reduce((dictionary, method) => { - if (dictionary[method] == null) { - dictionary[method] = 0 - } - dictionary[method] += 1 - return dictionary - }, {}) - - const repeats = Object.keys(mapCount) - .filter(k => mapCount[k] > 1) - .sort() - if (repeats.length > 0) { - return `Channel methods can't be repeated. Repeated methods are ${repeats.join( - ', ' - )}` - } -} - -export function isTimeoutValid(channel) { - if (channel.timeout == null) { - return true - } - - return ( - typeof channel.timeout === 'number' && - channel.timeout > 0 && - channel.timeout <= 3600000 - ) -} - -export function isMaxBodyDaysValid(channel) { - if (channel.maxBodyAgeDays == null) { - return true - } - - if (!channel.requestBody && !channel.responseBody) { - return false - } - - return ( - typeof channel.maxBodyAgeDays === 'number' && - channel.maxBodyAgeDays > 0 && - channel.maxBodyAgeDays < 36500 - ) -} - -/* - * Creates a new channel - */ -export async function addChannel(ctx) { - // Test if the user is authorised - if (authorisation.inGroup('admin', ctx.authenticated) === false) { - utils.logAndSetResponse( - ctx, - 403, - `User ${ctx.authenticated.email} is not an admin, API access to addChannel denied.`, - 'info' - ) - return - } - - // Get the values to use - const channelData = ctx.request.body - - // Set the user creating the channel for auditing purposes - channelData.updatedBy = utils.selectAuditFields(ctx.authenticated) - - try { - const channel = new ChannelModel(channelData) - - if (!isPathValid(channel)) { - ctx.body = - 'Channel cannot have both path and pathTransform. pathTransform must be of the form s/from/to[/g]' - ctx.status = 400 - return - } - - if (channel.priority != null && channel.priority < 1) { - ctx.body = 'Channel priority cannot be below 1 (= Highest priority)' - ctx.status = 400 - return - } - - let methodValidation = validateMethod(channel) - - if (methodValidation != null) { - ctx.body = methodValidation - ctx.status = 400 - return - } - - if (!isTimeoutValid(channel)) { - ctx.body = TIMEOUT_SECONDS_MESSAGE - ctx.status = 400 - return - } - - const numPrimaries = routerMiddleware.numberOfPrimaryRoutes(channel.routes) - if (numPrimaries === 0) { - ctx.body = 'Channel must have a primary route' - ctx.status = 400 - return - } - if (numPrimaries > 1) { - ctx.body = 'Channel cannot have a multiple primary routes' - ctx.status = 400 - return - } - - if (!isMaxBodyDaysValid(channelData)) { - ctx.body = MAX_BODY_AGE_MESSAGE - ctx.status = 400 - return - } - - await channel.save() - - // All ok! So set the result - ctx.body = 'Channel successfully created' - ctx.status = 201 - logger.info( - `User ${ctx.authenticated.email} created channel with id ${channel.id}` - ) - - channelData._id = channel._id - processPostAddTriggers(channelData) - } catch (err) { - // Error! So inform the user - utils.logAndSetResponse( - ctx, - 400, - `Could not add channel via the API: ${err}`, - 'error' - ) - } -} - -/* - * Retrieves the details for a specific channel - */ -export async function getChannel(ctx, channelId) { - // Get the values to use - const id = unescape(channelId) - - try { - // Try to get the channel - let result = null - let accessDenied = false - // if admin allow acces to all channels otherwise restrict result set - if (authorisation.inGroup('admin', ctx.authenticated) === false) { - result = await ChannelModel.findOne({ - _id: id, - txViewAcl: {$in: ctx.authenticated.groups} - }).exec() - const adminResult = await ChannelModel.findById(id).exec() - if (adminResult != null) { - accessDenied = true - } - } else { - result = await ChannelModel.findById(id).exec() - } - - // Test if the result if valid - if (result === null) { - if (accessDenied) { - // Channel exists but this user doesn't have access - ctx.body = `Access denied to channel with Id: '${id}'.` - ctx.status = 403 - } else { - // Channel not found! So inform the user - ctx.body = `We could not find a channel with Id:'${id}'.` - ctx.status = 404 - } - } else { - // All ok! So set the result - ctx.body = result - } - } catch (err) { - // Error! So inform the user - utils.logAndSetResponse( - ctx, - 500, - `Could not fetch channel by Id '${id}' via the API: ${err}`, - 'error' - ) - } -} - -export async function getChannelAudits(ctx, channelId) { - if (!authorisation.inGroup('admin', ctx.authenticated)) { - utils.logAndSetResponse( - ctx, - 403, - `User ${ctx.authenticated.email} is not an admin, API access to addChannel denied.`, - 'info' - ) - return - } - - try { - const channel = await ChannelModel.findById(channelId).exec() - if (channel) { - ctx.body = await channel.patches - .find({ - $and: [ - {ref: channel.id}, - {ops: {$elemMatch: {path: {$ne: '/lastBodyCleared'}}}} - ] - }) - .sort({_id: -1}) - .exec() - } else { - ctx.body = [] - } - } catch (err) { - utils.logAndSetResponse( - ctx, - 500, - `Could not fetch all channels via the API: ${err}`, - 'error' - ) - } -} - -function processPostUpdateTriggers(channel) { - if (channel.type) { - if ( - (channel.type === 'tcp' || channel.type === 'tls') && - server.isTcpHttpReceiverRunning() - ) { - if (Channels.isChannelEnabled(channel)) { - return tcpAdapter.notifyMasterToStartTCPServer(channel._id, err => { - if (err) { - return logger.error(err) - } - }) - } else { - return tcpAdapter.notifyMasterToStopTCPServer(channel._id, err => { - if (err) { - return logger.error(err) - } - }) - } - } else if (channel.type === 'polling') { - if (Channels.isChannelEnabled(channel)) { - return polling.registerPollingChannel(channel, err => { - if (err) { - return logger.error(err) - } - }) - } else { - return polling.removePollingChannel(channel, err => { - if (err) { - return logger.error(err) - } - }) - } - } - } -} - -async function findChannelByIdAndUpdate(id, channelData) { - const channel = await ChannelModel.findById(id) - if ( - channelData.maxBodyAgeDays != null && - channel.maxBodyAgeDays != null && - channelData.maxBodyAgeDays !== channel.maxBodyAgeDays - ) { - channelData.lastBodyCleared = undefined - } - channel.set(channelData) - return channel.save() -} - -/* - * Updates the details for a specific channel - */ -export async function updateChannel(ctx, channelId) { - // Test if the user is authorised - if (authorisation.inGroup('admin', ctx.authenticated) === false) { - utils.logAndSetResponse( - ctx, - 403, - `User ${ctx.authenticated.email} is not an admin, API access to updateChannel denied.`, - 'info' - ) - return - } - - // Get the values to use - const id = unescape(channelId) - const channelData = ctx.request.body - - // Set the user updating the channel for auditing purposes - channelData.updatedBy = utils.selectAuditFields(ctx.authenticated) - const updatedChannel = await ChannelModel.findById(id) - // This is so you can see how the channel will look as a whole before saving - updatedChannel.set(channelData) - - if ( - !utils.isNullOrWhitespace(channelData.type) && - utils.isNullOrEmpty(channelData.methods) - ) { - // Empty the methods if the type has changed from http - if (channelData.type !== 'http') { - channelData.methods = [] - } - } else { - const {type} = updatedChannel - let {methods} = updatedChannel - let methodValidation = validateMethod({type, methods}) - - if (methodValidation != null) { - ctx.body = methodValidation - ctx.status = 400 - return - } - } - - if (!isTimeoutValid(channelData)) { - ctx.body = TIMEOUT_SECONDS_MESSAGE - ctx.status = 400 - return - } - - // Ignore _id if it exists, user cannot change the internal id - if (typeof channelData._id !== 'undefined') { - delete channelData._id - } - - if (!isPathValid(channelData)) { - utils.logAndSetResponse( - ctx, - 400, - 'Channel cannot have both path and pathTransform. pathTransform must be of the form s/from/to[/g]', - 'info' - ) - return - } - - if (channelData.priority != null && channelData.priority < 1) { - ctx.body = 'Channel priority cannot be below 1 (= Highest priority)' - ctx.status = 400 - return - } - - if (channelData.routes != null) { - const numPrimaries = routerMiddleware.numberOfPrimaryRoutes( - channelData.routes - ) - if (numPrimaries === 0) { - ctx.body = 'Channel must have a primary route' - ctx.status = 400 - return - } - if (numPrimaries > 1) { - ctx.body = 'Channel cannot have a multiple primary routes' - ctx.status = 400 - return - } - } - - if (!isTimeoutValid(updatedChannel)) { - ctx.body = TIMEOUT_SECONDS_MESSAGE - ctx.status = 400 - return - } - - if (!isMaxBodyDaysValid(updatedChannel)) { - ctx.body = MAX_BODY_AGE_MESSAGE - ctx.status = 400 - return - } - - try { - const channel = await findChannelByIdAndUpdate(id, channelData) - - // All ok! So set the result - ctx.body = 'The channel was successfully updated' - logger.info(`User ${ctx.authenticated.email} updated channel with id ${id}`) - - return processPostUpdateTriggers(channel) - } catch (err) { - // Error! So inform the user - utils.logAndSetResponse( - ctx, - 500, - `Could not update channel by id: ${id} via the API: ${err}`, - 'error' - ) - } -} - -function processPostDeleteTriggers(channel) { - if (channel.type) { - if ( - (channel.type === 'tcp' || channel.type === 'tls') && - server.isTcpHttpReceiverRunning() - ) { - return tcpAdapter.notifyMasterToStopTCPServer(channel._id, err => { - if (err) { - return logger.error(err) - } - }) - } else if (channel.type === 'polling') { - return polling.removePollingChannel(channel, err => { - if (err) { - return logger.error(err) - } - }) - } - } -} - -/* - * Deletes a specific channels details - */ -export async function removeChannel(ctx, channelId) { - // Test if the user is authorised - if (authorisation.inGroup('admin', ctx.authenticated) === false) { - utils.logAndSetResponse( - ctx, - 403, - `User ${ctx.authenticated.email} is not an admin, API access to removeChannel denied.`, - 'info' - ) - return - } - - // Get the values to use - const id = unescape(channelId) - - try { - let channel - const numExistingTransactions = await TransactionModelAPI.countDocuments({ - channelID: id - }).exec() - - // Try to get the channel (Call the function that emits a promise and Koa will wait for the function to complete) - if (numExistingTransactions === 0) { - // safe to remove - channel = await ChannelModel.findByIdAndRemove(id).exec() - } else { - // not safe to remove. just flag as deleted - channel = await findChannelByIdAndUpdate(id, { - status: 'deleted', - updatedBy: utils.selectAuditFields(ctx.authenticated) - }) - } - - // All ok! So set the result - ctx.body = 'The channel was successfully deleted' - logger.info(`User ${ctx.authenticated.email} removed channel with id ${id}`) - - return processPostDeleteTriggers(channel) - } catch (err) { - // Error! So inform the user - utils.logAndSetResponse( - ctx, - 500, - `Could not remove channel by id: ${id} via the API: ${err}`, - 'error' - ) - } -} - -/* - * Manually Triggers Polling Channel - */ -export async function triggerChannel(ctx, channelId) { - // Test if the user is authorised - if (authorisation.inGroup('admin', ctx.authenticated) === false) { - utils.logAndSetResponse( - ctx, - 403, - `User ${ctx.authenticated.email} is not an admin, API access to removeChannel denied.`, - 'info' - ) - return - } - - // Get the values to use - const id = unescape(channelId) - - // need to initialize return status otherwise will always return 404 - ctx.status = 200 - - try { - const channel = await ChannelModel.findById(id).exec() - - // Test if the result if valid - if (channel === null) { - // Channel not found! So inform the user - ctx.body = `We could not find a channel with Id:'${id}'.` - ctx.status = 404 - return - } else { - logger.info(`Manually Polling channel ${channel._id}`) - const options = { - url: `http://${config.polling.host}:${config.polling.pollingPort}/trigger`, - headers: { - 'channel-id': channel._id, - 'X-OpenHIM-LastRunAt': new Date() - }, - method: 'GET' - } - - await new Promise(resolve => { - axios(options) - .then(() => { - logger.info(`Channel Successfully polled ${channel._id}`) - // Return success status - ctx.status = 200 - resolve() - }) - .catch(err => { - logger.error(err.message) - ctx.status = 500 - resolve() - }) - }) - } - } catch (err) { - // Error! So inform the user - utils.logAndSetResponse( - ctx, - 500, - `Could not fetch channel by Id '${id}' via the API: ${err}`, - 'error' - ) - } -} +'use strict' + +import logger from 'winston' +import axios from 'axios' + +import * as Channels from '../model/channels' +import * as authorisation from './authorisation' +import * as polling from '../polling' +import * as routerMiddleware from '../middleware/router' +import * as server from '../server' +import * as tcpAdapter from '../tcpAdapter' +import * as utils from '../utils' +import {TransactionModelAPI} from '../model/transactions' +import {config} from '../config' + +const {ChannelModel} = Channels + +const MAX_BODY_AGE_MESSAGE = `Channel property maxBodyAgeDays has to be a number that's valid and requestBody or responseBody must be true.` +const TIMEOUT_SECONDS_MESSAGE = `Channel property timeoutSeconds has to be a number greater than 1 and less than an 3600` + +config.polling = config.get('polling') + +function isPathValid(channel) { + if (channel.routes != null) { + for (const route of Array.from(channel.routes)) { + // There cannot be both path and pathTransform. pathTransform must be valid + if ( + (route.path && route.pathTransform) || + (route.pathTransform && !/s\/.*\/.*/.test(route.pathTransform)) + ) { + return false + } + } + } + return true +} + +/* + * Retrieves the list of active channels + */ +export async function getChannels(ctx) { + try { + ctx.body = await authorisation.getUserViewableChannels(ctx.authenticated) + } catch (err) { + utils.logAndSetResponse( + ctx, + 500, + `Could not fetch all channels via the API: ${err}`, + 'error' + ) + } +} + +function processPostAddTriggers(channel) { + if (channel.type && Channels.isChannelEnabled(channel)) { + if ( + (channel.type === 'tcp' || channel.type === 'tls') && + server.isTcpHttpReceiverRunning() + ) { + return tcpAdapter.notifyMasterToStartTCPServer(channel._id, err => { + if (err) { + return logger.error(err) + } + }) + } else if (channel.type === 'polling') { + return polling.registerPollingChannel(channel, err => { + if (err) { + return logger.error(err) + } + }) + } + } +} + +export function validateMethod(channel) { + const {methods = []} = channel || {} + if (methods.length === 0) { + return + } + + if (!/http/i.test(channel.type || 'http')) { + return `Channel method can't be defined if channel type is not http` + } + + const mapCount = methods.reduce((dictionary, method) => { + if (dictionary[method] == null) { + dictionary[method] = 0 + } + dictionary[method] += 1 + return dictionary + }, {}) + + const repeats = Object.keys(mapCount) + .filter(k => mapCount[k] > 1) + .sort() + if (repeats.length > 0) { + return `Channel methods can't be repeated. Repeated methods are ${repeats.join( + ', ' + )}` + } +} + +export function isTimeoutValid(channel) { + if (channel.timeout == null) { + return true + } + + return ( + typeof channel.timeout === 'number' && + channel.timeout > 0 && + channel.timeout <= 3600000 + ) +} + +export function isMaxBodyDaysValid(channel) { + if (channel.maxBodyAgeDays == null) { + return true + } + + if (!channel.requestBody && !channel.responseBody) { + return false + } + + return ( + typeof channel.maxBodyAgeDays === 'number' && + channel.maxBodyAgeDays > 0 && + channel.maxBodyAgeDays < 36500 + ) +} + +/* + * Creates a new channel + */ +export async function addChannel(ctx) { + // Test if the user is authorised + if (authorisation.inGroup('admin', ctx.authenticated) === false) { + utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to addChannel denied.`, + 'info' + ) + return + } + + // Get the values to use + const channelData = ctx.request.body + + // Set the user creating the channel for auditing purposes + channelData.updatedBy = utils.selectAuditFields(ctx.authenticated) + + try { + const channel = new ChannelModel(channelData) + + if (!isPathValid(channel)) { + ctx.body = + 'Channel cannot have both path and pathTransform. pathTransform must be of the form s/from/to[/g]' + ctx.status = 400 + return + } + + if (channel.priority != null && channel.priority < 1) { + ctx.body = 'Channel priority cannot be below 1 (= Highest priority)' + ctx.status = 400 + return + } + + let methodValidation = validateMethod(channel) + + if (methodValidation != null) { + ctx.body = methodValidation + ctx.status = 400 + return + } + + if (!isTimeoutValid(channel)) { + ctx.body = TIMEOUT_SECONDS_MESSAGE + ctx.status = 400 + return + } + + const numPrimaries = routerMiddleware.numberOfPrimaryRoutes(channel.routes) + if (numPrimaries === 0) { + ctx.body = 'Channel must have a primary route' + ctx.status = 400 + return + } + if (numPrimaries > 1) { + ctx.body = 'Channel cannot have a multiple primary routes' + ctx.status = 400 + return + } + + if (!isMaxBodyDaysValid(channelData)) { + ctx.body = MAX_BODY_AGE_MESSAGE + ctx.status = 400 + return + } + + await channel.save() + + // All ok! So set the result + ctx.body = 'Channel successfully created' + ctx.status = 201 + logger.info( + `User ${ctx.authenticated.email} created channel with id ${channel.id}` + ) + + channelData._id = channel._id + processPostAddTriggers(channelData) + } catch (err) { + // Error! So inform the user + utils.logAndSetResponse( + ctx, + 400, + `Could not add channel via the API: ${err}`, + 'error' + ) + } +} + +/* + * Retrieves the details for a specific channel + */ +export async function getChannel(ctx, channelId) { + // Get the values to use + const id = unescape(channelId) + + try { + // Try to get the channel + let result = null + let accessDenied = false + // if admin allow acces to all channels otherwise restrict result set + if (authorisation.inGroup('admin', ctx.authenticated) === false) { + result = await ChannelModel.findOne({ + _id: id, + txViewAcl: {$in: ctx.authenticated.groups} + }).exec() + const adminResult = await ChannelModel.findById(id).exec() + if (adminResult != null) { + accessDenied = true + } + } else { + result = await ChannelModel.findById(id).exec() + } + + // Test if the result if valid + if (result === null) { + if (accessDenied) { + // Channel exists but this user doesn't have access + ctx.body = `Access denied to channel with Id: '${id}'.` + ctx.status = 403 + } else { + // Channel not found! So inform the user + ctx.body = `We could not find a channel with Id:'${id}'.` + ctx.status = 404 + } + } else { + // All ok! So set the result + ctx.body = result + } + } catch (err) { + // Error! So inform the user + utils.logAndSetResponse( + ctx, + 500, + `Could not fetch channel by Id '${id}' via the API: ${err}`, + 'error' + ) + } +} + +export async function getChannelByName(ctx, channelName) { + // Get the values to use + const name = unescape(channelName) + + try { + // Try to get the channel + let result = null + let accessDenied = false + // if admin allow acces to all channels otherwise restrict result set + if (authorisation.inGroup('admin', ctx.authenticated) === false) { + result = await ChannelModel.findOne({ + name: name, + txViewAcl: {$in: ctx.authenticated.groups} + }).exec() + const adminResult = await ChannelModel.findOne({ + name: name + }).exec() + if (adminResult != null) { + accessDenied = true + } + } else { + result = await ChannelModel.findOne({ + name: name + }).exec() + } + + // Test if the result if valid + if (result === null) { + if (accessDenied) { + // Channel exists but this user doesn't have access + ctx.body = `Access denied to channel with name: '${name}'.` + ctx.status = 403 + } else { + // Channel not found! So inform the user + ctx.body = `We could not find a channel with name:'${name}'.` + ctx.status = 404 + } + } else { + // All ok! So set the result + ctx.body = result + } + } catch (err) { + // Error! So inform the user + utils.logAndSetResponse( + ctx, + 500, + `Could not fetch channel by name '${name}' via the API: ${err}`, + 'error' + ) + } +} + +export async function getChannelAudits(ctx, channelId) { + if (!authorisation.inGroup('admin', ctx.authenticated)) { + utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to addChannel denied.`, + 'info' + ) + return + } + + try { + const channel = await ChannelModel.findById(channelId).exec() + if (channel) { + ctx.body = await channel.patches + .find({ + $and: [ + {ref: channel.id}, + {ops: {$elemMatch: {path: {$ne: '/lastBodyCleared'}}}} + ] + }) + .sort({_id: -1}) + .exec() + } else { + ctx.body = [] + } + } catch (err) { + utils.logAndSetResponse( + ctx, + 500, + `Could not fetch all channels via the API: ${err}`, + 'error' + ) + } +} + +function processPostUpdateTriggers(channel) { + if (channel.type) { + if ( + (channel.type === 'tcp' || channel.type === 'tls') && + server.isTcpHttpReceiverRunning() + ) { + if (Channels.isChannelEnabled(channel)) { + return tcpAdapter.notifyMasterToStartTCPServer(channel._id, err => { + if (err) { + return logger.error(err) + } + }) + } else { + return tcpAdapter.notifyMasterToStopTCPServer(channel._id, err => { + if (err) { + return logger.error(err) + } + }) + } + } else if (channel.type === 'polling') { + if (Channels.isChannelEnabled(channel)) { + return polling.registerPollingChannel(channel, err => { + if (err) { + return logger.error(err) + } + }) + } else { + return polling.removePollingChannel(channel, err => { + if (err) { + return logger.error(err) + } + }) + } + } + } +} + +async function findChannelByIdAndUpdate(id, channelData) { + const channel = await ChannelModel.findById(id) + if ( + channelData.maxBodyAgeDays != null && + channel.maxBodyAgeDays != null && + channelData.maxBodyAgeDays !== channel.maxBodyAgeDays + ) { + channelData.lastBodyCleared = undefined + } + channel.set(channelData) + return channel.save() +} + +/* + * Updates the details for a specific channel + */ +export async function updateChannel(ctx, channelId) { + // Test if the user is authorised + if (authorisation.inGroup('admin', ctx.authenticated) === false) { + utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to updateChannel denied.`, + 'info' + ) + return + } + + // Get the values to use + const id = unescape(channelId) + const channelData = ctx.request.body + + // Set the user updating the channel for auditing purposes + channelData.updatedBy = utils.selectAuditFields(ctx.authenticated) + const updatedChannel = await ChannelModel.findById(id) + // This is so you can see how the channel will look as a whole before saving + updatedChannel.set(channelData) + + if ( + !utils.isNullOrWhitespace(channelData.type) && + utils.isNullOrEmpty(channelData.methods) + ) { + // Empty the methods if the type has changed from http + if (channelData.type !== 'http') { + channelData.methods = [] + } + } else { + const {type} = updatedChannel + let {methods} = updatedChannel + let methodValidation = validateMethod({type, methods}) + + if (methodValidation != null) { + ctx.body = methodValidation + ctx.status = 400 + return + } + } + + if (!isTimeoutValid(channelData)) { + ctx.body = TIMEOUT_SECONDS_MESSAGE + ctx.status = 400 + return + } + + // Ignore _id if it exists, user cannot change the internal id + if (typeof channelData._id !== 'undefined') { + delete channelData._id + } + + if (!isPathValid(channelData)) { + utils.logAndSetResponse( + ctx, + 400, + 'Channel cannot have both path and pathTransform. pathTransform must be of the form s/from/to[/g]', + 'info' + ) + return + } + + if (channelData.priority != null && channelData.priority < 1) { + ctx.body = 'Channel priority cannot be below 1 (= Highest priority)' + ctx.status = 400 + return + } + + if (channelData.routes != null) { + const numPrimaries = routerMiddleware.numberOfPrimaryRoutes( + channelData.routes + ) + if (numPrimaries === 0) { + ctx.body = 'Channel must have a primary route' + ctx.status = 400 + return + } + if (numPrimaries > 1) { + ctx.body = 'Channel cannot have a multiple primary routes' + ctx.status = 400 + return + } + } + + if (!isTimeoutValid(updatedChannel)) { + ctx.body = TIMEOUT_SECONDS_MESSAGE + ctx.status = 400 + return + } + + if (!isMaxBodyDaysValid(updatedChannel)) { + ctx.body = MAX_BODY_AGE_MESSAGE + ctx.status = 400 + return + } + + try { + const channel = await findChannelByIdAndUpdate(id, channelData) + + // All ok! So set the result + ctx.body = 'The channel was successfully updated' + logger.info(`User ${ctx.authenticated.email} updated channel with id ${id}`) + + return processPostUpdateTriggers(channel) + } catch (err) { + // Error! So inform the user + utils.logAndSetResponse( + ctx, + 500, + `Could not update channel by id: ${id} via the API: ${err}`, + 'error' + ) + } +} + +function processPostDeleteTriggers(channel) { + if (channel.type) { + if ( + (channel.type === 'tcp' || channel.type === 'tls') && + server.isTcpHttpReceiverRunning() + ) { + return tcpAdapter.notifyMasterToStopTCPServer(channel._id, err => { + if (err) { + return logger.error(err) + } + }) + } else if (channel.type === 'polling') { + return polling.removePollingChannel(channel, err => { + if (err) { + return logger.error(err) + } + }) + } + } +} + +/* + * Deletes a specific channels details + */ +export async function removeChannel(ctx, channelId) { + // Test if the user is authorised + if (authorisation.inGroup('admin', ctx.authenticated) === false) { + utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to removeChannel denied.`, + 'info' + ) + return + } + + // Get the values to use + const id = unescape(channelId) + + try { + let channel + const numExistingTransactions = await TransactionModelAPI.countDocuments({ + channelID: id + }).exec() + + // Try to get the channel (Call the function that emits a promise and Koa will wait for the function to complete) + if (numExistingTransactions === 0) { + // safe to remove + channel = await ChannelModel.findByIdAndRemove(id).exec() + } else { + // not safe to remove. just flag as deleted + channel = await findChannelByIdAndUpdate(id, { + status: 'deleted', + updatedBy: utils.selectAuditFields(ctx.authenticated) + }) + } + + // All ok! So set the result + ctx.body = 'The channel was successfully deleted' + logger.info(`User ${ctx.authenticated.email} removed channel with id ${id}`) + + return processPostDeleteTriggers(channel) + } catch (err) { + // Error! So inform the user + utils.logAndSetResponse( + ctx, + 500, + `Could not remove channel by id: ${id} via the API: ${err}`, + 'error' + ) + } +} + +/* + * Manually Triggers Polling Channel + */ +export async function triggerChannel(ctx, channelId) { + // Test if the user is authorised + if (authorisation.inGroup('admin', ctx.authenticated) === false) { + utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to removeChannel denied.`, + 'info' + ) + return + } + + // Get the values to use + const id = unescape(channelId) + + // need to initialize return status otherwise will always return 404 + ctx.status = 200 + + try { + const channel = await ChannelModel.findById(id).exec() + + // Test if the result if valid + if (channel === null) { + // Channel not found! So inform the user + ctx.body = `We could not find a channel with Id:'${id}'.` + ctx.status = 404 + return + } else { + logger.info(`Manually Polling channel ${channel._id}`) + const options = { + url: `http://${config.polling.host}:${config.polling.pollingPort}/trigger`, + headers: { + 'channel-id': channel._id, + 'X-OpenHIM-LastRunAt': new Date() + }, + method: 'GET' + } + + await new Promise(resolve => { + axios(options) + .then(() => { + logger.info(`Channel Successfully polled ${channel._id}`) + // Return success status + ctx.status = 200 + resolve() + }) + .catch(err => { + logger.error(err.message) + ctx.status = 500 + resolve() + }) + }) + } + } catch (err) { + // Error! So inform the user + utils.logAndSetResponse( + ctx, + 500, + `Could not fetch channel by Id '${id}' via the API: ${err}`, + 'error' + ) + } +} \ No newline at end of file diff --git a/src/api/clients.js b/src/api/clients.js index 384118fb..66f66ddb 100644 --- a/src/api/clients.js +++ b/src/api/clients.js @@ -1,279 +1,295 @@ -'use strict' - -import logger from 'winston' - -import * as authorisation from './authorisation' -import * as utils from '../utils' -import {ChannelModelAPI} from '../model/channels' -import {ClientModelAPI} from '../model/clients' - -/* - * Adds a client - */ -export async function addClient(ctx) { - // Test if the user is authorised - if (!authorisation.inGroup('admin', ctx.authenticated)) { - utils.logAndSetResponse( - ctx, - 403, - `User ${ctx.authenticated.email} is not an admin, API access to addClient denied.`, - 'info' - ) - return - } - - const clientData = ctx.request.body - - if (clientData.clientID) { - const chResult = await ChannelModelAPI.find( - {allow: {$in: [clientData.clientID]}}, - {name: 1} - ).exec() - const clResult = await ClientModelAPI.find( - {roles: {$in: [clientData.clientID]}}, - {clientID: 1} - ).exec() - if ( - (chResult != null ? chResult.length : undefined) > 0 || - (clResult != null ? clResult.length : undefined) > 0 - ) { - return utils.logAndSetResponse( - ctx, - 409, - `A role name conflicts with clientID '${clientData.clientID}'. A role name cannot be the same as a clientID.`, - 'info' - ) - } - if (clientData.roles.includes(clientData.clientID)) { - return utils.logAndSetResponse( - ctx, - 400, - `ClientID '${clientData.clientID}' cannot be the same as a role name.`, - 'info' - ) - } - } - - try { - const client = new ClientModelAPI(clientData) - await client.save() - - logger.info( - `User ${ctx.authenticated.email} created client with id ${client.id}` - ) - ctx.body = 'Client successfully created' - ctx.status = 201 - } catch (e) { - logger.error(`Could not add a client via the API: ${e.message}`) - ctx.body = e.message - ctx.status = 400 - } -} - -/* - * Retrieves the details of a specific client - */ -export async function getClient(ctx, clientId, property) { - let projectionRestriction = null - - // if property - Setup client projection and bypass authorization - if (typeof property === 'string') { - if (property === 'clientName') { - projectionRestriction = { - _id: 0, - name: 1 - } - } else { - utils.logAndSetResponse( - ctx, - 404, - `The property (${property}) you are trying to retrieve is not found.`, - 'info' - ) - return - } - } else if (!authorisation.inGroup('admin', ctx.authenticated)) { - utils.logAndSetResponse( - ctx, - 403, - `User ${ctx.authenticated.email} is not an admin, API access to findClientById denied.`, - 'info' - ) - return - } - - clientId = unescape(clientId) - - try { - const result = await ClientModelAPI.findById( - clientId, - projectionRestriction - ) - .lean() - .exec() - if (result === null) { - utils.logAndSetResponse( - ctx, - 404, - `Client with id ${clientId} could not be found.`, - 'info' - ) - } else { - // Remove the Custom Token ID from response - if (result.customTokenID) { - delete result.customTokenID - result.customTokenSet = true - } - ctx.body = result - } - } catch (e) { - logger.error( - `Could not find client by id ${clientId} via the API: ${e.message}` - ) - ctx.body = e.message - ctx.status = 500 - } -} - -export async function findClientByDomain(ctx, clientDomain) { - // Test if the user is authorised - if (!authorisation.inGroup('admin', ctx.authenticated)) { - utils.logAndSetResponse( - ctx, - 403, - `User ${ctx.authenticated.email} is not an admin, API access to findClientByDomain denied.`, - 'info' - ) - return - } - - clientDomain = unescape(clientDomain) - - try { - const result = await ClientModelAPI.findOne({clientDomain}).exec() - if (result === null) { - utils.logAndSetResponse( - ctx, - 404, - `Could not find client with clientDomain ${clientDomain}`, - 'info' - ) - } else { - ctx.body = result - } - } catch (e) { - logger.error( - `Could not find client by client Domain ${clientDomain} via the API: ${e.message}` - ) - ctx.body = e.message - ctx.status = 500 - } -} - -export async function updateClient(ctx, clientId) { - // Test if the user is authorised - if (!authorisation.inGroup('admin', ctx.authenticated)) { - utils.logAndSetResponse( - ctx, - 403, - `User ${ctx.authenticated.email} is not an admin, API access to updateClient denied.`, - 'info' - ) - return - } - - clientId = unescape(clientId) - const clientData = ctx.request.body - - // Ignore _id if it exists, a user shouldn't be able to update the internal id - if (clientData._id) { - delete clientData._id - } - - if (clientData.clientID) { - const clResult = await ClientModelAPI.find( - {roles: {$in: [clientData.clientID]}}, - {clientID: 1} - ).exec() - if ((clResult != null ? clResult.length : undefined) > 0) { - return utils.logAndSetResponse( - ctx, - 409, - `A role name conflicts with clientID '${clientData.clientID}'. A role name cannot be the same as a clientID.`, - 'info' - ) - } - } - - try { - await ClientModelAPI.findByIdAndUpdate(clientId, clientData).exec() - logger.info( - `User ${ctx.authenticated.email} updated client with id ${clientId}` - ) - ctx.body = 'Successfully updated client.' - } catch (e) { - logger.error( - `Could not update client by ID ${clientId} via the API: ${e.message}` - ) - ctx.body = e.message - ctx.status = 500 - } -} - -export async function removeClient(ctx, clientId) { - // Test if the user is authorised - if (!authorisation.inGroup('admin', ctx.authenticated)) { - utils.logAndSetResponse( - ctx, - 403, - `User ${ctx.authenticated.email} is not an admin, API access to removeClient denied.`, - 'info' - ) - return - } - - clientId = unescape(clientId) - - try { - await ClientModelAPI.findByIdAndRemove(clientId).exec() - ctx.body = `Successfully removed client with ID ${clientId}` - logger.info( - `User ${ctx.authenticated.email} removed client with id ${clientId}` - ) - } catch (e) { - logger.error( - `Could not remove client by ID ${clientId} via the API: ${e.message}` - ) - ctx.body = e.message - ctx.status = 500 - } -} - -export async function getClients(ctx) { - // Test if the user is authorised - if (!authorisation.inGroup('admin', ctx.authenticated)) { - utils.logAndSetResponse( - ctx, - 403, - `User ${ctx.authenticated.email} is not an admin, API access to getClients denied.`, - 'info' - ) - return - } - - try { - let clients = await ClientModelAPI.find().lean().exec() - // Remove the Custom Token IDs from response - ctx.body = clients.map(client => { - if (client.customTokenID) { - delete client.customTokenID - client.customTokenSet = true - } - return client - }) - } catch (e) { - logger.error(`Could not fetch all clients via the API: ${e.message}`) - ctx.message = e.message - ctx.status = 500 - } -} +'use strict' +import logger from 'winston' +import * as authorisation from './authorisation' +import * as utils from '../utils' +import {ChannelModelAPI} from '../model/channels' +import {ClientModelAPI} from '../model/clients' +/* + * Adds a client + */ +export async function addClient(ctx) { + // Test if the user is authorised + if (!authorisation.inGroup('admin', ctx.authenticated)) { + utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to addClient denied.`, + 'info' + ) + return + } + const clientData = ctx.request.body + if (clientData.clientID) { + const chResult = await ChannelModelAPI.find( + {allow: {$in: [clientData.clientID]}}, + {name: 1} + ).exec() + const clResult = await ClientModelAPI.find( + {roles: {$in: [clientData.clientID]}}, + {clientID: 1} + ).exec() + if ( + (chResult != null ? chResult.length : undefined) > 0 || + (clResult != null ? clResult.length : undefined) > 0 + ) { + return utils.logAndSetResponse( + ctx, + 409, + `A role name conflicts with clientID '${clientData.clientID}'. A role name cannot be the same as a clientID.`, + 'info' + ) + } + if (clientData.roles.includes(clientData.clientID)) { + return utils.logAndSetResponse( + ctx, + 400, + `ClientID '${clientData.clientID}' cannot be the same as a role name.`, + 'info' + ) + } + } + try { + clientData.updatedBy = utils.selectAuditFields(ctx.authenticated); + const client = new ClientModelAPI(clientData) + await client.save() + logger.info( + `User ${ctx.authenticated.email} created client with id ${client.id}` + ) + ctx.body = 'Client successfully created' + ctx.status = 201 + } catch (e) { + logger.error(`Could not add a client via the API: ${e.message}`) + ctx.body = e.message + ctx.status = 400 + } +} +/* + * Retrieves the details of a specific client + */ +export async function getClient(ctx, clientId, property) { + let projectionRestriction = null + // if property - Setup client projection and bypass authorization + if (typeof property === 'string') { + if (property === 'clientName') { + projectionRestriction = { + _id: 0, + name: 1 + } + } else { + utils.logAndSetResponse( + ctx, + 404, + `The property (${property}) you are trying to retrieve is not found.`, + 'info' + ) + return + } + } else if (!authorisation.inGroup('admin', ctx.authenticated)) { + utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to findClientById denied.`, + 'info' + ) + return + } + clientId = unescape(clientId); + try { + const result = await ClientModelAPI.findById( + clientId, + projectionRestriction + ) + .lean() + .exec() + if (result === null) { + utils.logAndSetResponse( + ctx, + 404, + `Client with id ${clientId} could not be found.`, + 'info' + ) + } else { + // Remove the Custom Token ID from response + if (result.customTokenID) { + delete result.customTokenID + result.customTokenSet = true + } + ctx.body = result + } + } catch (e) { + logger.error( + `Could not find client by id ${clientId} via the API: ${e.message}` + ) + ctx.body = e.message + ctx.status = 500 + } +} +export async function getClientByTextClientId(ctx, clientID) { + if (!authorisation.inGroup('admin', ctx.authenticated)) { + utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to findClientById denied.`, + 'info' + ) + return + } + clientID = unescape(clientID); + try { + const result = await ClientModelAPI.findOne({ + clientID: utils.caseInsensitiveRegex(clientID) + }).exec() + if (result === null) { + utils.logAndSetResponse( + ctx, + 404, + `Client with text clientID ${clientID} could not be found.`, + 'info' + ) + } else { + // Remove the Custom Token ID from response + if (result.customTokenID) { + delete result.customTokenID + result.customTokenSet = true + } + ctx.body = result + } + } catch (e) { + logger.error( + `Could not find client by text clientID ${clientID} via the API: ${e.message}` + ) + ctx.body = e.message + ctx.status = 500 + } +} +export async function findClientByDomain(ctx, clientDomain) { + // Test if the user is authorised + if (!authorisation.inGroup('admin', ctx.authenticated)) { + utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to findClientByDomain denied.`, + 'info' + ) + return + } + clientDomain = unescape(clientDomain) + try { + const result = await ClientModelAPI.findOne({clientDomain}).exec() + if (result === null) { + utils.logAndSetResponse( + ctx, + 404, + `Could not find client with clientDomain ${clientDomain}`, + 'info' + ) + } else { + ctx.body = result + } + } catch (e) { + logger.error( + `Could not find client by client Domain ${clientDomain} via the API: ${e.message}` + ) + ctx.body = e.message + ctx.status = 500 + } +} +export async function updateClient(ctx, clientId) { + // Test if the user is authorised + if (!authorisation.inGroup('admin', ctx.authenticated)) { + utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to updateClient denied.`, + 'info' + ) + return + } + clientId = unescape(clientId) + const clientData = ctx.request.body + // Ignore _id if it exists, a user shouldn't be able to update the internal id + if (clientData._id) { + delete clientData._id + } + if (clientData.clientID) { + const clResult = await ClientModelAPI.find( + {roles: {$in: [clientData.clientID]}}, + {clientID: 1} + ).exec() + if ((clResult != null ? clResult.length : undefined) > 0) { + return utils.logAndSetResponse( + ctx, + 409, + `A role name conflicts with clientID '${clientData.clientID}'. A role name cannot be the same as a clientID.`, + 'info' + ) + } + } + try { + clientData.updatedBy = utils.selectAuditFields(ctx.authenticated); + await ClientModelAPI.findByIdAndUpdate(clientId, clientData).exec() + logger.info( + `User ${ctx.authenticated.email} updated client with id ${clientId}` + ) + ctx.body = 'Successfully updated client.' + } catch (e) { + logger.error( + `Could not update client by ID ${clientId} via the API: ${e.message}` + ) + ctx.body = e.message + ctx.status = 500 + } +} +export async function removeClient(ctx, clientId) { + // Test if the user is authorised + if (!authorisation.inGroup('admin', ctx.authenticated)) { + utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to removeClient denied.`, + 'info' + ) + return + } + clientId = unescape(clientId) + try { + await ClientModelAPI.findByIdAndRemove(clientId).exec() + ctx.body = `Successfully removed client with ID ${clientId}` + logger.info( + `User ${ctx.authenticated.email} removed client with id ${clientId}` + ) + } catch (e) { + logger.error( + `Could not remove client by ID ${clientId} via the API: ${e.message}` + ) + ctx.body = e.message + ctx.status = 500 + } +} +export async function getClients(ctx) { + // Test if the user is authorised + if (!authorisation.inGroup('admin', ctx.authenticated)) { + utils.logAndSetResponse( + ctx, + 403, + `User ${ctx.authenticated.email} is not an admin, API access to getClients denied.`, + 'info' + ) + return + } + try { + let clients = await ClientModelAPI.find().lean().exec() + // Remove the Custom Token IDs from response + ctx.body = clients.map(client => { + if (client.customTokenID) { + delete client.customTokenID + client.customTokenSet = true + } + return client + }) + } catch (e) { + logger.error(`Could not fetch all clients via the API: ${e.message}`) + ctx.message = e.message + ctx.status = 500 + } +} \ No newline at end of file diff --git a/src/api/metadata.js b/src/api/metadata.js index dbe110bd..d8ee062f 100644 --- a/src/api/metadata.js +++ b/src/api/metadata.js @@ -8,11 +8,9 @@ import {ContactGroupModelAPI} from '../model/contactGroups' import {KeystoreModelAPI} from '../model/keystore' import {MediatorModelAPI} from '../model/mediators' import {UserModelAPI} from '../model/users' -import {getClient, getClients, addClient, updateClient} from '../api/clients' -import {getChannels} from '../api/channels' +import {getClients, addClient, updateClient, getClientByTextClientId} from '../api/clients' +import {getChannels, addChannel, updateChannel, getChannelByName} from '../api/channels' import * as polling from '../polling' -import { compareSync } from 'bcryptjs' - // Map string parameters to collections const collections = { Channels: ChannelModelAPI, @@ -22,7 +20,6 @@ const collections = { ContactGroups: ContactGroupModelAPI, KeystoreModelAPI } - // Function to remove properties from export object function removeProperties(obj) { const propertyID = '_id' @@ -36,7 +33,6 @@ function removeProperties(obj) { } return obj } - // Function to return unique identifier key and value for a collection function getUniqueIdentifierForCollection(collection, doc) { let uid @@ -100,19 +96,18 @@ export async function getMetadata(ctx) { switch(model) { case 'Clients': await getClients(ctx); + console.log('clientData: ' + JSON.stringify(ctx.body)); exportObject[model] = ctx.body; break; case 'Channels': await getChannels(ctx); + console.log('channelData: ' + JSON.stringify(ctx.body)); exportObject[model] = ctx.body; break; default: exportObject[model] = await collections[model].find().lean().exec(); break; } - - console.log('array from : '+ JSON.stringify(Array.from(exportObject[model]))); - for (let doc of Array.from(exportObject[model])) { if (doc._id) { doc = removeProperties(doc) @@ -132,16 +127,9 @@ export async function getMetadata(ctx) { } } -function clientExists(clientResult) { - return clientResult && clientResult.length > 0 && clientResult[0]._id; -} - -function doesExist(ctx) { - //we need to get the - const clientResult = ctx.request.body; - return clientResult && clientResult.length > 0 && clientResult[0]._id; +function docExists(doc) { + return doc && doc._id; } - function validateDocument(key, doc, ctx) { doc = new collections[key](doc) doc.set('updatedBy', utils.selectAuditFields(ctx.authenticated)) @@ -150,7 +138,6 @@ function validateDocument(key, doc, ctx) { throw new Error(`Document Validation failed: ${error}`) } } - async function handleMetadataPost(ctx, action) { // Test if the user is authorised if (!authorisation.inGroup('admin', ctx.authenticated)) { @@ -185,19 +172,49 @@ async function handleMetadataPost(ctx, action) { result = await collections[key].find(uidObj).exec() } if(key === 'Clients') { - const clientResult = getClient(ctx, doc.clientID); + const clientResult = await getClientByTextClientId(ctx, doc.clientID); + console.log('clientResult: ' + JSON.stringify(clientResult)); + const modelCtx = ctx; + modelCtx.request.body = doc; + if(action === 'import') { + if (docExists(clientResult)) { + await updateClient(modelCtx, clientResult._id); + status = 'Updated'; + } + else { + ctx.request.body = doc; + await addClient(modelCtx); + status = 'Inserted'; + } + } + if(action === 'validate') { + if (docExists(clientResult)) { + status = 'Conflict'; + } + else { + validateDocument(key, doc, ctx); + status = 'Valid' + } + } + } + else if(key === 'Channels') { + const channelResult = await getChannelByName(ctx, doc.name); + console.log('channelResult: ' + JSON.stringify(channelResult)); + const modelCtx = ctx; + modelCtx.request.body = doc; if(action === 'import') { - if (clientExists(clientResult)) { - await updateClient(ctx, doc.clientID); + if (docExists(channelResult)) { + await updateChannel(modelCtx, channelResult._id); status = 'Updated'; } else { - await addClient(ctx); //must modify ctx body to only include this particular client data block + ctx.request.body = doc; + await addChannel(modelCtx); status = 'Inserted'; } } if(action === 'validate') { - if (clientExists(clientResult)) { + if (docExists(channelResult)) { status = 'Conflict'; } else { diff --git a/src/koaApi.js b/src/koaApi.js index c505dafa..f98a2477 100644 --- a/src/koaApi.js +++ b/src/koaApi.js @@ -1,205 +1,181 @@ -'use strict' - -import Koa from 'koa' -import bodyParser from 'koa-bodyparser' -import cors from 'kcors' -import route from 'koa-route' - -import * as about from './api/about' -import * as audits from './api/audits' -import * as authentication from './api/authentication' -import * as certificateAuthority from './api/certificateAuthority' -import * as channels from './api/channels' -import * as clients from './api/clients' -import * as contactGroups from './api/contactGroups' -import * as events from './api/events' -import * as heartbeat from './api/heartbeat' -import * as keystore from './api/keystore' -import * as logs from './api/logs' -import * as mediators from './api/mediators' -import * as metrics from './api/metrics' -import * as metadata from './api/metadata' -import * as roles from './api/roles' -import * as serverRestart from './api/restart' -import * as tasks from './api/tasks' -import * as transactions from './api/transactions' -import * as users from './api/users' -import * as visualizers from './api/visualizers' -import {config} from './config' - -export function setupApp(done) { - // Create an instance of the koa-server and add a body-parser - const app = new Koa() - app.use(cors({allowMethods: 'GET,HEAD,PUT,POST,DELETE'})) - const limitMB = config.api.maxPayloadSizeMB || 16 - app.use(bodyParser({jsonLimit: limitMB * 1024 * 1024})) - - // Expose uptime server stats route before the auth middleware so that it is publicly accessible - app.use(route.get('/heartbeat', heartbeat.getHeartbeat)) - - // Expose the set-user-password route before the auth middleware so that it is publicly accessible - app.use( - route.get('/password-reset-request/:email', users.userPasswordResetRequest) - ) - app.use(route.get('/token/:token', users.getUserByToken)) - app.use(route.put('/token/:token', users.updateUserByToken)) - - // Expose the authenticate route before the auth middleware so that it is publicly accessible - app.use(route.get('/authenticate/:username', users.authenticate)) - // Authenticate the API request - app.use(authentication.authenticate) - - // Get enabled authentication types - app.use( - route.get( - '/authentication/types', - authentication.getEnabledAuthenticationTypes - ) - ) - - // Define the api routes - app.use(route.get('/users', users.getUsers)) - app.use(route.get('/users/:email', users.getUser)) - app.use(route.post('/users', users.addUser)) - app.use(route.put('/users/:email', users.updateUser)) - app.use(route.delete('/users/:email', users.removeUser)) - - app.use(route.get('/clients', clients.getClients)) - app.use(route.get('/clients/:clientId', clients.getClient)) - app.use(route.post('/clients', clients.addClient)) - app.use( - route.get('/clients/domain/:clientDomain', clients.findClientByDomain) - ) - app.use(route.put('/clients/:clientId', clients.updateClient)) - app.use(route.delete('/clients/:clientId', clients.removeClient)) - app.use(route.get('/clients/:clientId/:property', clients.getClient)) - - app.use(route.get('/roles', roles.getRoles)) - app.use(route.post('/roles', roles.addRole)) - app.use(route.get('/roles/:name', roles.getRole)) - app.use(route.put('/roles/:name', roles.updateRole)) - app.use(route.delete('/roles/:name', roles.deleteRole)) - - app.use(route.get('/transactions', transactions.getTransactions)) - app.use(route.post('/transactions', transactions.addTransaction)) - app.use( - route.get('/transactions/:transactionId', transactions.getTransactionById) - ) - app.use( - route.get( - '/transactions/clients/:clientId', - transactions.findTransactionByClientId - ) - ) - app.use( - route.put('/transactions/:transactionId', transactions.updateTransaction) - ) - app.use( - route.delete('/transactions/:transactionId', transactions.removeTransaction) - ) - - app.use(route.get('/groups', contactGroups.getContactGroups)) - app.use(route.get('/groups/:contactGroupId', contactGroups.getContactGroup)) - app.use(route.post('/groups', contactGroups.addContactGroup)) - app.use( - route.put('/groups/:contactGroupId', contactGroups.updateContactGroup) - ) - app.use( - route.delete('/groups/:contactGroupId', contactGroups.removeContactGroup) - ) - - app.use(route.get('/channels', channels.getChannels)) - app.use(route.post('/channels', channels.addChannel)) - app.use(route.get('/channels/:channelId', channels.getChannel)) - app.use(route.get('/channels/:channelId/audits', channels.getChannelAudits)) - app.use(route.post('/channels/:channelId/trigger', channels.triggerChannel)) - app.use(route.put('/channels/:channelId', channels.updateChannel)) - app.use(route.delete('/channels/:channelId', channels.removeChannel)) - - app.use(route.get('/tasks', tasks.getTasks)) - app.use(route.post('/tasks', tasks.addTask)) - app.use(route.get('/tasks/:taskId', tasks.getTask)) - app.use(route.put('/tasks/:taskId', tasks.updateTask)) - app.use(route.delete('/tasks/:taskId', tasks.removeTask)) - - app.use(route.get('/metrics', ctx => metrics.getMetrics(ctx, false))) - app.use(route.get('/metrics/channels', ctx => metrics.getMetrics(ctx, true))) - app.use( - route.get('/metrics/channels/:channelID', (ctx, channelID) => - metrics.getMetrics(ctx, true, null, channelID) - ) - ) - app.use( - route.get('/metrics/timeseries/:timeSeries', (ctx, timeseries) => - metrics.getMetrics(ctx, false, timeseries) - ) - ) - app.use( - route.get('/metrics/timeseries/:timeSeries/channels', (ctx, timeseries) => - metrics.getMetrics(ctx, true, timeseries) - ) - ) - app.use( - route.get( - '/metrics/timeseries/:timeSeries/channels/:channelID', - (ctx, timeseries, channelID) => - metrics.getMetrics(ctx, true, timeseries, channelID) - ) - ) - - app.use(route.get('/mediators', mediators.getAllMediators)) - app.use(route.get('/mediators/:uuid', mediators.getMediator)) - app.use(route.post('/mediators', mediators.addMediator)) - app.use(route.delete('/mediators/:urn', mediators.removeMediator)) - app.use(route.post('/mediators/:urn/heartbeat', mediators.heartbeat)) - app.use(route.put('/mediators/:urn/config', mediators.setConfig)) - app.use(route.post('/mediators/:urn/channels', mediators.loadDefaultChannels)) - - app.use(route.get('/keystore/cert', keystore.getServerCert)) - app.use(route.post('/keystore/cert', keystore.setServerCert)) - app.use(route.get('/keystore/ca', keystore.getCACerts)) - app.use(route.get('/keystore/ca/:certId', keystore.getCACert)) - app.use(route.delete('/keystore/ca/:certId', keystore.removeCACert)) - app.use(route.post('/keystore/key', keystore.setServerKey)) - app.use(route.post('/keystore/ca/cert', keystore.addTrustedCert)) - app.use(route.get('/keystore/validity', keystore.verifyServerKeys)) - app.use(route.post('/keystore/passphrase', keystore.setServerPassphrase)) - - // Metadata endpoints - app.use(route.get('/metadata', metadata.getMetadata)) - app.use(route.post('/metadata/validate', metadata.validateMetadata)) - app.use(route.post('/metadata', metadata.importMetadata)) - - // Server restart endpoint - app.use(route.post('/restart', serverRestart.restart)) - - // AuditRecord endpoint - app.use(route.post('/audits', audits.addAudit)) - app.use(route.get('/audits', audits.getAudits)) - app.use(route.get('/audits/:auditId', audits.getAuditById)) - app.use(route.get('/audits-filter-options', audits.getAuditsFilterOptions)) - - // Ceritficates endpoint - app.use(route.post('/certificates', certificateAuthority.generateCert)) - - // Logs endpoint - app.use(route.get('/logs', logs.getLogs)) - - // Events endpoint - app.use(route.get('/events/:receivedTime', events.getLatestEvents)) - - // Version endpoint - app.use(route.get('/about', about.getAboutInformation)) - - // Visualizer endpoint - app.use(route.get('/visualizers', visualizers.getVisualizers)) - app.use(route.get('/visualizers/:visualizerId', visualizers.getVisualizer)) - app.use(route.post('/visualizers', visualizers.addVisualizer)) - app.use(route.put('/visualizers/:visualizerId', visualizers.updateVisualizer)) - app.use( - route.delete('/visualizers/:visualizerId', visualizers.removeVisualizer) - ) - - // Return the result - return done(app) -} +'use strict' +import Koa from 'koa' +import bodyParser from 'koa-bodyparser' +import cors from 'kcors' +import route from 'koa-route' +import * as about from './api/about' +import * as audits from './api/audits' +import * as authentication from './api/authentication' +import * as certificateAuthority from './api/certificateAuthority' +import * as channels from './api/channels' +import * as clients from './api/clients' +import * as contactGroups from './api/contactGroups' +import * as events from './api/events' +import * as heartbeat from './api/heartbeat' +import * as keystore from './api/keystore' +import * as logs from './api/logs' +import * as mediators from './api/mediators' +import * as metrics from './api/metrics' +import * as metadata from './api/metadata' +import * as roles from './api/roles' +import * as serverRestart from './api/restart' +import * as tasks from './api/tasks' +import * as transactions from './api/transactions' +import * as users from './api/users' +import * as visualizers from './api/visualizers' +import {config} from './config' +export function setupApp(done) { + // Create an instance of the koa-server and add a body-parser + const app = new Koa() + app.use(cors({allowMethods: 'GET,HEAD,PUT,POST,DELETE'})) + const limitMB = config.api.maxPayloadSizeMB || 16 + app.use(bodyParser({jsonLimit: limitMB * 1024 * 1024})) + // Expose uptime server stats route before the auth middleware so that it is publicly accessible + app.use(route.get('/heartbeat', heartbeat.getHeartbeat)) + // Expose the set-user-password route before the auth middleware so that it is publicly accessible + app.use( + route.get('/password-reset-request/:email', users.userPasswordResetRequest) + ) + app.use(route.get('/token/:token', users.getUserByToken)) + app.use(route.put('/token/:token', users.updateUserByToken)) + // Expose the authenticate route before the auth middleware so that it is publicly accessible + app.use(route.get('/authenticate/:username', users.authenticate)) + // Authenticate the API request + app.use(authentication.authenticate) + // Get enabled authentication types + app.use( + route.get( + '/authentication/types', + authentication.getEnabledAuthenticationTypes + ) + ) + // Define the api routes + app.use(route.get('/users', users.getUsers)) + app.use(route.get('/users/:email', users.getUser)) + app.use(route.post('/users', users.addUser)) + app.use(route.put('/users/:email', users.updateUser)) + app.use(route.delete('/users/:email', users.removeUser)) + app.use(route.get('/clients', clients.getClients)) + app.use(route.get('/clients/:clientId', clients.getClient)) + app.use(route.get('/clients/byClientID/:clientID', clients.getClientByTextClientId)) + app.use(route.post('/clients', clients.addClient)) + app.use( + route.get('/clients/domain/:clientDomain', clients.findClientByDomain) + ) + app.use(route.put('/clients/:clientId', clients.updateClient)) + app.use(route.delete('/clients/:clientId', clients.removeClient)) + app.use(route.get('/clients/:clientId/:property', clients.getClient)) + app.use(route.get('/roles', roles.getRoles)) + app.use(route.post('/roles', roles.addRole)) + app.use(route.get('/roles/:name', roles.getRole)) + app.use(route.put('/roles/:name', roles.updateRole)) + app.use(route.delete('/roles/:name', roles.deleteRole)) + app.use(route.get('/transactions', transactions.getTransactions)) + app.use(route.post('/transactions', transactions.addTransaction)) + app.use( + route.get('/transactions/:transactionId', transactions.getTransactionById) + ) + app.use( + route.get( + '/transactions/clients/:clientId', + transactions.findTransactionByClientId + ) + ) + app.use( + route.put('/transactions/:transactionId', transactions.updateTransaction) + ) + app.use( + route.delete('/transactions/:transactionId', transactions.removeTransaction) + ) + app.use(route.get('/groups', contactGroups.getContactGroups)) + app.use(route.get('/groups/:contactGroupId', contactGroups.getContactGroup)) + app.use(route.post('/groups', contactGroups.addContactGroup)) + app.use( + route.put('/groups/:contactGroupId', contactGroups.updateContactGroup) + ) + app.use( + route.delete('/groups/:contactGroupId', contactGroups.removeContactGroup) + ) + app.use(route.get('/channels', channels.getChannels)) + app.use(route.post('/channels', channels.addChannel)) + app.use(route.get('/channels/:channelId', channels.getChannel)) + app.use(route.get('/channels/byName/:channelName', channels.getChannelByName)) + app.use(route.get('/channels/:channelId/audits', channels.getChannelAudits)) + app.use(route.post('/channels/:channelId/trigger', channels.triggerChannel)) + app.use(route.put('/channels/:channelId', channels.updateChannel)) + app.use(route.delete('/channels/:channelId', channels.removeChannel)) + app.use(route.get('/tasks', tasks.getTasks)) + app.use(route.post('/tasks', tasks.addTask)) + app.use(route.get('/tasks/:taskId', tasks.getTask)) + app.use(route.put('/tasks/:taskId', tasks.updateTask)) + app.use(route.delete('/tasks/:taskId', tasks.removeTask)) + app.use(route.get('/metrics', ctx => metrics.getMetrics(ctx, false))) + app.use(route.get('/metrics/channels', ctx => metrics.getMetrics(ctx, true))) + app.use( + route.get('/metrics/channels/:channelID', (ctx, channelID) => + metrics.getMetrics(ctx, true, null, channelID) + ) + ) + app.use( + route.get('/metrics/timeseries/:timeSeries', (ctx, timeseries) => + metrics.getMetrics(ctx, false, timeseries) + ) + ) + app.use( + route.get('/metrics/timeseries/:timeSeries/channels', (ctx, timeseries) => + metrics.getMetrics(ctx, true, timeseries) + ) + ) + app.use( + route.get( + '/metrics/timeseries/:timeSeries/channels/:channelID', + (ctx, timeseries, channelID) => + metrics.getMetrics(ctx, true, timeseries, channelID) + ) + ) + app.use(route.get('/mediators', mediators.getAllMediators)) + app.use(route.get('/mediators/:uuid', mediators.getMediator)) + app.use(route.post('/mediators', mediators.addMediator)) + app.use(route.delete('/mediators/:urn', mediators.removeMediator)) + app.use(route.post('/mediators/:urn/heartbeat', mediators.heartbeat)) + app.use(route.put('/mediators/:urn/config', mediators.setConfig)) + app.use(route.post('/mediators/:urn/channels', mediators.loadDefaultChannels)) + app.use(route.get('/keystore/cert', keystore.getServerCert)) + app.use(route.post('/keystore/cert', keystore.setServerCert)) + app.use(route.get('/keystore/ca', keystore.getCACerts)) + app.use(route.get('/keystore/ca/:certId', keystore.getCACert)) + app.use(route.delete('/keystore/ca/:certId', keystore.removeCACert)) + app.use(route.post('/keystore/key', keystore.setServerKey)) + app.use(route.post('/keystore/ca/cert', keystore.addTrustedCert)) + app.use(route.get('/keystore/validity', keystore.verifyServerKeys)) + app.use(route.post('/keystore/passphrase', keystore.setServerPassphrase)) + // Metadata endpoints + app.use(route.get('/metadata', metadata.getMetadata)) + app.use(route.post('/metadata/validate', metadata.validateMetadata)) + app.use(route.post('/metadata', metadata.importMetadata)) + // Server restart endpoint + app.use(route.post('/restart', serverRestart.restart)) + // AuditRecord endpoint + app.use(route.post('/audits', audits.addAudit)) + app.use(route.get('/audits', audits.getAudits)) + app.use(route.get('/audits/:auditId', audits.getAuditById)) + app.use(route.get('/audits-filter-options', audits.getAuditsFilterOptions)) + // Ceritficates endpoint + app.use(route.post('/certificates', certificateAuthority.generateCert)) + // Logs endpoint + app.use(route.get('/logs', logs.getLogs)) + // Events endpoint + app.use(route.get('/events/:receivedTime', events.getLatestEvents)) + // Version endpoint + app.use(route.get('/about', about.getAboutInformation)) + // Visualizer endpoint + app.use(route.get('/visualizers', visualizers.getVisualizers)) + app.use(route.get('/visualizers/:visualizerId', visualizers.getVisualizer)) + app.use(route.post('/visualizers', visualizers.addVisualizer)) + app.use(route.put('/visualizers/:visualizerId', visualizers.updateVisualizer)) + app.use( + route.delete('/visualizers/:visualizerId', visualizers.removeVisualizer) + ) + // Return the result + return done(app) +} \ No newline at end of file From bbd7f6adb15e25e619fa2ef3a2c06f8e3df67e5f Mon Sep 17 00:00:00 2001 From: Kenneth Mutombo Date: Sun, 17 Oct 2021 00:36:10 +0200 Subject: [PATCH 5/6] Adds bug fix for the client update --- src/api/clients.js | 8 +++++--- src/api/metadata.js | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/api/clients.js b/src/api/clients.js index 66f66ddb..472a2157 100644 --- a/src/api/clients.js +++ b/src/api/clients.js @@ -134,11 +134,13 @@ export async function getClientByTextClientId(ctx, clientID) { ) return } - clientID = unescape(clientID); + //clientID = unescape(clientID); try { + const result = await ClientModelAPI.findOne({ - clientID: utils.caseInsensitiveRegex(clientID) - }).exec() + clientID: clientID + }).exec(); + if (result === null) { utils.logAndSetResponse( ctx, diff --git a/src/api/metadata.js b/src/api/metadata.js index d8ee062f..d59c3b80 100644 --- a/src/api/metadata.js +++ b/src/api/metadata.js @@ -172,8 +172,8 @@ async function handleMetadataPost(ctx, action) { result = await collections[key].find(uidObj).exec() } if(key === 'Clients') { - const clientResult = await getClientByTextClientId(ctx, doc.clientID); - console.log('clientResult: ' + JSON.stringify(clientResult)); + await getClientByTextClientId(ctx, doc.clientID); + const clientResult = ctx.body; const modelCtx = ctx; modelCtx.request.body = doc; if(action === 'import') { From d960c691f239034409b3e2da2f8fbccd85ba66ec Mon Sep 17 00:00:00 2001 From: Kenneth Mutombo Date: Sun, 17 Oct 2021 13:58:52 +0200 Subject: [PATCH 6/6] Removes unnecessary comments and console logs --- src/api/channels.js | 2 +- src/api/clients.js | 2 +- src/api/metadata.js | 6 ++---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/api/channels.js b/src/api/channels.js index 513fe03e..f64028ad 100644 --- a/src/api/channels.js +++ b/src/api/channels.js @@ -272,7 +272,7 @@ export async function getChannel(ctx, channelId) { export async function getChannelByName(ctx, channelName) { // Get the values to use - const name = unescape(channelName) + const name = channelName try { // Try to get the channel diff --git a/src/api/clients.js b/src/api/clients.js index 472a2157..59913c92 100644 --- a/src/api/clients.js +++ b/src/api/clients.js @@ -134,7 +134,7 @@ export async function getClientByTextClientId(ctx, clientID) { ) return } - //clientID = unescape(clientID); + try { const result = await ClientModelAPI.findOne({ diff --git a/src/api/metadata.js b/src/api/metadata.js index d59c3b80..d969487c 100644 --- a/src/api/metadata.js +++ b/src/api/metadata.js @@ -96,12 +96,10 @@ export async function getMetadata(ctx) { switch(model) { case 'Clients': await getClients(ctx); - console.log('clientData: ' + JSON.stringify(ctx.body)); exportObject[model] = ctx.body; break; case 'Channels': await getChannels(ctx); - console.log('channelData: ' + JSON.stringify(ctx.body)); exportObject[model] = ctx.body; break; default: @@ -198,8 +196,8 @@ async function handleMetadataPost(ctx, action) { } } else if(key === 'Channels') { - const channelResult = await getChannelByName(ctx, doc.name); - console.log('channelResult: ' + JSON.stringify(channelResult)); + await getChannelByName(ctx, doc.name); + const channelResult =ctx.body; const modelCtx = ctx; modelCtx.request.body = doc; if(action === 'import') {