Skip to content
Snippets Groups Projects
Commit 5b06158a authored by Yannis Barlas's avatar Yannis Barlas
Browse files

feat(server): add callMicroservice function

parent 6d5e4be1
No related branches found
No related tags found
2 merge requests!52Docx,!17Graphql api
...@@ -7,4 +7,5 @@ module.exports = [ ...@@ -7,4 +7,5 @@ module.exports = [
'./src/models/chatMessage', './src/models/chatMessage',
'./src/models/file', './src/models/file',
'./src/models/__tests__/helpers/fake', './src/models/__tests__/helpers/fake',
'./src/models/serviceCredential',
] ]
...@@ -36,6 +36,13 @@ module.exports = { ...@@ -36,6 +36,13 @@ module.exports = {
// globalSetup: '<rootDir>/src/models/__tests__/_setup.js', // globalSetup: '<rootDir>/src/models/__tests__/_setup.js',
// globalTeardown: '<rootDir>/src/models/__tests__/_teardown.js', // globalTeardown: '<rootDir>/src/models/__tests__/_teardown.js',
}, },
{
displayName: 'utils',
testEnvironment: 'node',
testRegex: 'src/utils/__tests__/.+test.js$',
// globalSetup: '<rootDir>/src/models/__tests__/_setup.js',
// globalTeardown: '<rootDir>/src/models/__tests__/_teardown.js',
},
], ],
maxWorkers: 1, maxWorkers: 1,
} }
...@@ -37,6 +37,8 @@ const fileStorage = { ...@@ -37,6 +37,8 @@ const fileStorage = {
// const { serviceHandshake } = require('./helpers') // const { serviceHandshake } = require('./helpers')
const { callMicroservice } = require('./utils/microservices')
const createJWT = authentication.token.create const createJWT = authentication.token.create
const verifyJWT = authentication.token.verify const verifyJWT = authentication.token.verify
...@@ -64,4 +66,6 @@ module.exports = { ...@@ -64,4 +66,6 @@ module.exports = {
boss, boss,
connectToJobQueue, connectToJobQueue,
callMicroservice,
} }
...@@ -12,6 +12,7 @@ const Identity = require('./identity/identity.model') ...@@ -12,6 +12,7 @@ const Identity = require('./identity/identity.model')
const File = require('./file/file.model') const File = require('./file/file.model')
const useTransaction = require('./useTransaction') const useTransaction = require('./useTransaction')
const ServiceCredential = require('./serviceCredential/serviceCredential.model')
module.exports = { module.exports = {
BaseModel, BaseModel,
...@@ -26,7 +27,7 @@ module.exports = { ...@@ -26,7 +27,7 @@ module.exports = {
Identity, Identity,
File, File,
// ServiceCredential,
useTransaction, useTransaction,
ServiceCredential,
} }
// const model = require('./serviceCredential.model') const model = require('./serviceCredential.model')
// module.exports = { module.exports = {
// model, model,
// modelName: 'ServiceCredential', modelName: 'ServiceCredential',
// } }
// /* eslint-disable no-console */ const logger = require('@pubsweet/logger')
// const logger = require('@pubsweet/logger')
// exports.up = async knex => { exports.up = async knex => {
// try { try {
// return knex.schema.createTable('service_credentials', table => { return knex.schema.createTable('service_credential', table => {
// table.uuid('id').primary() table.uuid('id').primary()
// table table
// .timestamp('created', { useTz: true }) .timestamp('created', { useTz: true })
// .notNullable() .notNullable()
// .defaultTo(knex.fn.now()) .defaultTo(knex.fn.now())
// table.timestamp('updated', { useTz: true }) table.timestamp('updated', { useTz: true })
// table.text('type').notNullable() table.text('type').notNullable()
// table.text('name').notNullable() table.text('name').notNullable()
// table.text('accessToken').nullable() table.text('accessToken').nullable()
// }) })
// } catch (e) { } catch (e) {
// logger.error('Service Credentials: Initial: Migration failed!') logger.error('Service Credentials: Initial: Migration failed!')
// throw new Error(e) throw new Error(e)
// } }
// } }
// exports.down = async knex => knex.schema.dropTable('service_credentials') exports.down = async knex => knex.schema.dropTable('service_credential')
// const BaseModel = require('@pubsweet/base-model') const BaseModel = require('@pubsweet/base-model')
// const { string, stringNotEmpty } = require('../_helpers/types') const { string, stringNotEmpty } = require('../_helpers/types')
// class ServiceCredential extends BaseModel { class ServiceCredential extends BaseModel {
// constructor(properties) { constructor(properties) {
// super(properties) super(properties)
// this.type = 'serviceCredential' this.type = 'serviceCredential'
// } }
// static get tableName() { static get tableName() {
// return 'serviceCredentials' return 'serviceCredential'
// } }
// static get schema() { static get schema() {
// return { return {
// type: 'object', type: 'object',
// required: ['name'], required: ['name'],
// properties: { properties: {
// name: stringNotEmpty, name: stringNotEmpty,
// accessToken: string, accessToken: string,
// }, },
// } }
// } }
// } }
// module.exports = ServiceCredential module.exports = ServiceCredential
const axios = require('axios')
const { callMicroservice } = require('../microservices')
jest.mock('../getAccessToken', () => {
return jest.fn(() => 'token')
})
jest.mock('axios')
describe('Microservices', () => {
it('calls microservice successfully with token', async () => {
axios.mockResolvedValue(true)
const res = await callMicroservice('xsweet', {})
expect(res).toBe(true)
})
it('fetches a new token when expired', async () => {
axios
.mockRejectedValueOnce({
response: {
status: 401,
data: {
msg: 'expired token',
},
},
})
.mockResolvedValueOnce(true)
const res = await callMicroservice('xsweet', {})
expect(res).toBe(true)
})
})
const get = require('lodash/get')
const config = require('config')
const axios = require('axios')
const { ServiceCredential } = require('../models')
const services = config.get('services')
const getAccessToken = async (serviceName, renew = false) => {
try {
if (!services) {
throw new Error('services are undefined')
}
const service = get(services, `${serviceName}`)
if (!service) {
throw new Error(`service ${serviceName} configuration is undefined `)
}
const { clientId, clientSecret, port, protocol, host } = service
const buff = Buffer.from(`${clientId}:${clientSecret}`, 'utf8')
const base64data = buff.toString('base64')
const serviceURL = `${protocol}://${host}${port ? `:${port}` : ''}`
const serviceHealthCheck = await axios({
method: 'get',
url: `${serviceURL}/healthcheck`,
})
const { data: healthCheckData } = serviceHealthCheck
const { message } = healthCheckData
if (message !== 'Coolio') {
throw new Error(`service ${serviceName} is down`)
}
const foundServiceCredential = await ServiceCredential.query().findOne({
name: serviceName,
})
if (!foundServiceCredential) {
const { data } = await axios({
method: 'post',
url: `${serviceURL}/api/auth`,
headers: { authorization: `Basic ${base64data}` },
})
const { accessToken } = data
await ServiceCredential.query().insert({
name: serviceName,
accessToken,
})
return accessToken
}
const { accessToken, id } = foundServiceCredential
if (!accessToken || renew) {
const { data } = await axios({
method: 'post',
url: `${serviceURL}/api/auth`,
headers: { authorization: `Basic ${base64data}` },
})
const { accessToken: freshAccessToken } = data
await ServiceCredential.query().patchAndFetchById(id, {
accessToken: freshAccessToken,
})
return freshAccessToken
}
return accessToken
} catch (e) {
const foundServiceCredential = await ServiceCredential.query().findOne({
name: serviceName,
})
if (foundServiceCredential) {
await ServiceCredential.query().patchAndFetchById(
foundServiceCredential.id,
{
accessToken: null,
},
)
}
throw new Error(e)
}
}
module.exports = getAccessToken
const axios = require('axios')
const clone = require('lodash/clone')
const getAccessToken = require('./getAccessToken')
/**
* Calls given microservice, while taking care of authentication for you.
* Services need to be defined in the config.
*
* First, on the service container, you need to generate a client id & secret,
* then add these variables as credentials in the environment file of your app.
*
* This function will:
* - Grab those variables from the environment / config
* - Communicate with the service to get authenticated and get an access token
* - Store the access token on the ServiceCredential table
* - Make a call to the service with the parameters you gave it
*
* If the access token exists already, it will be used without calling the
* service for a new one.
*
* If the access token exists, but has expired, this is also handled
* automatically by getting a new token from the service.
*
* Other errors will be thrown and should be handled by your app logic.
*/
const callMicroservice = async (serviceName, callParameters) => {
try {
if (!callParameters)
throw new Error(
`communication parameters needed for calling ${serviceName} microservice`,
)
const makeCall = async token => {
const axiosParams = clone(callParameters)
const { headers } = axiosParams
if (!headers) {
axiosParams.headers = {
authorization: `Bearer ${token}`,
}
} else {
axiosParams.headers.authorization = `Bearer ${token}`
}
return axios(axiosParams)
}
const accessToken = await getAccessToken(serviceName)
return makeCall(accessToken).catch(async err => {
const { response } = err
if (!response) {
throw new Error(`Request failed with message: ${err.code}`)
}
const { status, data } = response
const { msg } = data
if (status === 401 && msg === 'expired token') {
const freshToken = await getAccessToken(serviceName, true)
return makeCall(freshToken)
}
throw new Error(err)
})
} catch (e) {
throw new Error(e)
}
}
module.exports = {
callMicroservice,
getAccessToken,
}
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment