import axios from 'axios'; /** * Handle authentication and send/receive with the backbone */ export class Adapter { constructor(protocol, config) { this.protocol = protocol; this.config = config; this.axios = axios; this.authenticating = false; } /** * Test parsing the message based on the provided protocol schema * * The message must be successfully json decoded into an object * prior to validation * * At present this returns the validation result which is an * object containing a boolean valid field as well as details * of any errors. * @param {object} message * @returns {object} */ validate(message) { return this.protocol.validate(message); } /** * Ensure the token in this.credentials is still valid * @returns {boolean} */ tokenValid() { let valid = false; if (this.credentials) { const now = new Date().toISOString(); valid = this.credentials.expiry > now; } return valid; } /** * Return an http headers object containing a bearer token authorisation header * @returns {object} */ getAuthorizationHeader() { if (this.authenticating) { // return an pseudo response with an ignored status return Promise.reject({ response: { status: 405 } }); } if (!this.tokenValid()) return this.auth().then((response) => { return { Authorization: `Bearer ${response.data.token}`, }; }); else { return Promise.resolve({ Authorization: `Bearer ${this.credentials.token}`, }); } } /** * Do a client credentials grant and store in this.credentials * @returns {object} */ auth() { this.authenticating = true; let adapterConfig = this.config; return this.axios .get(`${adapterConfig.api}/token`, { params: { client_id: adapterConfig.client_id, secret: adapterConfig.secret, }, }) .then((response) => { this.credentials = response.data; this.authenticating = false; return response; }) .catch((error) => { this.authenticating = false; return Promise.reject(error); }); } /** * Call the GET /receive endpoint and process the messages with decode * * Returns the response * @param {boolean} is_retry * @returns {object} */ poll(is_retry = false) { let adapterConfig = this.config; return this.getAuthorizationHeader() .then((headers) => { return this.axios.get(`${adapterConfig.api}/receive`, { headers, }); }) .then((response) => { response.data.forEach((message) => { const parsed = JSON.parse(message.message); const validation = this.validate(parsed); if (validation.valid) { const type = this.protocol.getType(parsed); this.protocol.decode(type, parsed); } else { this.protocol.receivedInvalid(parsed, validation); } }); return response; }) .catch((error) => { let retry = false; switch (error.response.status) { case 403: { this.credentials = null; retry = true; } break; // ignore 405 from auth in progress case 503: { retry = true; } } if (retry && !is_retry) return this.poll(true); else return Promise.reject(error); }); } /** * Publish a message to the backbone with the specified topic * * Messages should be passed through encode before sending * @param {string} topic * @param {string} body * @param {boolean} is_retry * @returns */ publish(topic, body, is_retry = false) { let adapterConfig = this.config; return this.getAuthorizationHeader() .then((headers) => { return this.axios.post( `${adapterConfig.api}/send`, { topic, body, }, { headers, } ); }) .then((response) => { return response; }) .catch((error) => { let retry = false; switch (error.response.status) { case 403: { this.credentials = null; retry = true; } break; case 503: { retry = true; } } if (retry && !is_retry) return this.publish(topic, body, true); else return Promise.reject(error); }); } /** * Broadcast the message on the backbone * * Broadcast messages bypass the normal publisher queues * this means they can be used to deliver messages more * quickly in an emergency scenario. * * Messages should be passed through encode before sending * @param {string} body * @param {boolean} is_retry * @returns */ broadcast(body, is_retry = false) { let adapterConfig = this.config; return this.getAuthorizationHeader() .then((headers) => { return this.axios.post( `${adapterConfig.api}/notify`, { body, }, { headers, } ); }) .then((response) => { return response; }) .catch((error) => { let retry = false; switch (error.response.status) { case 403: { this.credentials = null; retry = true; } break; case 503: { retry = true; } } if (retry && !is_retry) return this.broadcast(body, true); else return Promise.reject(error); }); } }