From 333733099beb05ffbdfcec6343aab66912298131 Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Fri, 30 Aug 2019 23:19:04 +0200 Subject: [PATCH] add cron service to obtain oauth token and store it to the DB --- constants/constants.js | 1 + cronServices/oauth.js | 8 ++ environment.env | 4 + migrations/20190830134013-add-tokens-table.js | 27 +++++ models/accessToken.js | 12 +++ package-lock.json | 23 ++++- package.json | 4 +- services/officeRnD/oauth.js | 99 +++++++++++++++++++ 8 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 cronServices/oauth.js create mode 100644 migrations/20190830134013-add-tokens-table.js create mode 100644 models/accessToken.js create mode 100644 services/officeRnD/oauth.js diff --git a/constants/constants.js b/constants/constants.js index 84f5a32..7306753 100644 --- a/constants/constants.js +++ b/constants/constants.js @@ -62,6 +62,7 @@ const officeRnDAPIErrors = { FAILED_TO_DELETE_FEES: 'Failed to delete fees in ORD', FAILED_TO_ADD_FEES: 'Failed to add fees in ORD', MEMBERSHIPS_ARE_NOT_LOADED_CORRECTLY: 'Memberships are not loaded correctly', + OAUTH_FAILED: 'Failed to fetch new OAUTH token', }; const integrationServiceErrors = { PROCESSING_TRY_AGAIN: 'Incident calculations are in progress. Please try again in a few minutes', diff --git a/cronServices/oauth.js b/cronServices/oauth.js new file mode 100644 index 0000000..81a482a --- /dev/null +++ b/cronServices/oauth.js @@ -0,0 +1,8 @@ +'use strict'; + +const { refreshOauthToken } = require('../services/officeRnD/oauth'); + +refreshOauthToken().then(() => process.exit()).catch((error) => { + console.log(error); + process.exit(); +}); diff --git a/environment.env b/environment.env index a3e73a1..4409e6d 100644 --- a/environment.env +++ b/environment.env @@ -32,6 +32,10 @@ DISCOUNT_LEVEL_2_HOURS=Hours requred to apply DISCOUNT_LEVEL_2_PERCENTAGE discou DISCOUNT_LEVEL_2_PERCENTAGE=Discount to apply in percentage, if DISCOUNT_LEVEL_2_HOURS of billable hours is booked DISCOUNT_PLANS=Plan names for which discount is available. Comma-separated +ORD_OAUTH_CLIENT_ID=Client ID for this app, created in ORD; Used to obtain OAUTH token +ORD_OAUTH_CLIENT_SECRET=Client secret for this app; Used to obtain OAUTH token +ORD_OAUTH_URL=https://identity.officernd.com/oauth/token + #More about pool option : http://docs.sequelizejs.com/class/lib/sequelize.js~Sequelize.html DB_POOL_MAX_CONNECTIONS=Maximum number of connection in pool (ex. 18) DB_POOL_ACQUIRE=The maximum time, in milliseconds, that pool will try to get connection before throwing error (ex. 120000) diff --git a/migrations/20190830134013-add-tokens-table.js b/migrations/20190830134013-add-tokens-table.js new file mode 100644 index 0000000..311498d --- /dev/null +++ b/migrations/20190830134013-add-tokens-table.js @@ -0,0 +1,27 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('accessTokens', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + token: Sequelize.TEXT, + validUntil: Sequelize.DATE, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('accessTokens'); + } +}; diff --git a/models/accessToken.js b/models/accessToken.js new file mode 100644 index 0000000..90f4269 --- /dev/null +++ b/models/accessToken.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = (sequelize, DataTypes) => { + const processing = sequelize.define('accessToken', { + token: DataTypes.TEXT, + validUntil: DataTypes.DATE, + }, {}); + processing.associate = function(models) { + // associations can be defined here + }; + return processing; +}; diff --git a/package-lock.json b/package-lock.json index bae1c22..f6114cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -684,8 +684,7 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, "deep-extend": { "version": "0.6.0", @@ -3029,6 +3028,16 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "query-string": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.8.2.tgz", + "integrity": "sha512-J3Qi8XZJXh93t2FiKyd/7Ec6GNifsjKXUsVFkSBj/kjLsDylWhnCz4NT1bkPcKotttPW+QbKGqqPH8OoI2pdqw==", + "requires": { + "decode-uri-component": "^0.2.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -3519,6 +3528,11 @@ "through": "2" } }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -3562,6 +3576,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", diff --git a/package.json b/package.json index 8dd67db..f87b7e6 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "start-client": "cd client && yarn start", "start": "node server.js", "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client", - "check-booking-changes": "node ./cronServices/checkBookingChanges.js" + "check-booking-changes": "node ./cronServices/checkBookingChanges.js", + "refresh-oauth": "node ./cronServices/oauth.js" }, "engines": { "node": "11.12.x" @@ -32,6 +33,7 @@ "moment": "^2.24.0", "moment-timezone": "^0.5.25", "pg": "^7.11.0", + "query-string": "^6.8.2", "sequelize": "^5.8.6", "sequelize-cli": "^5.4.0" }, diff --git a/services/officeRnD/oauth.js b/services/officeRnD/oauth.js new file mode 100644 index 0000000..c2778d5 --- /dev/null +++ b/services/officeRnD/oauth.js @@ -0,0 +1,99 @@ +'use strict'; +require('dotenv').config(); +const moment = require('moment'); +const queryString = require('query-string'); + +const { API } = require('../../helpers/api'); +const db = require('../../models/index'); +const { officeRnDAPIErrors } = require('../../constants/constants'); + +const updateToken = (token, validUntil) => { + return new Promise((resolve, reject) => { + const values = {token, validUntil}; + db.accessToken.update(values, {where:{}}) + .then(([numberOfUpdatedRows]) => { + if (numberOfUpdatedRows > 0){ + resolve(true); + }else{ + db.accessToken.bulkCreate([{token, validUntil}]) + .then(() => { + resolve(false); + }) + .catch((error) => reject(error)); + } + }) + .catch((error) => reject(error)); + }); +}; + +const getToken = () => { + return new Promise((resolve, reject) => { + db.accessToken.findAll() + .then((results) => { + if (results && results.length > 0){ + const token = results[0].getDataValue('token'); + const validUntil = results[0].getDataValue('validUntil'); + const validUntilMoment = moment(validUntil); + if (validUntilMoment.isBefore(moment())){ + refreshOauthToken() + .then((oauthResult) => resolve(oauthResult)) + .catch((error) => reject(error)); + }else{ + resolve({token, validUntil}); + } + }else{ + refreshOauthToken() + .then((oauthResult) => resolve(oauthResult)) + .catch((error) => reject(error)); + } + }) + .catch((error) => reject(error)); + }); +}; + +const refreshOauthToken = () => { + return new Promise((resolve, reject) => { + const clientID = process.env.ORD_OAUTH_CLIENT_ID; + const clientSecret = process.env.ORD_OAUTH_CLIENT_SECRET; + const OAUTHUrl = process.env.ORD_OAUTH_URL; + + const OAUTHRequestBody = { + client_id: clientID, + client_secret: clientSecret, + grant_type: 'client_credentials', + scope: 'officernd.api.read officernd.api.write', + }; + const config = { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + API.post(OAUTHUrl, queryString.stringify(OAUTHRequestBody), config) + .then((oauthResponse) => { + const responseData = oauthResponse && oauthResponse.data ? oauthResponse.data : null; + if (responseData){ + const { access_token: accessToken, expires_in: expiresIn } = responseData; + if (accessToken && expiresIn){ + const validUntil = moment().add(expiresIn, 'seconds').toISOString(); + updateToken(accessToken, validUntil) + .then(() => resolve({token: accessToken, validUntil})) + .catch((error) => reject(error)); + }else{ + reject(officeRnDAPIErrors.OAUTH_FAILED); + } + }else{ + reject(officeRnDAPIErrors.OAUTH_FAILED); + } + }) + .catch((error) => { + console.log('Error obtaining OAUTH token : ', error); + reject(); + }); + }); +}; + +module.exports = { + getToken, + refreshOauthToken, +};