From 804e36ebaf1ad927580af1804b05ad516f7d909a Mon Sep 17 00:00:00 2001 From: Jordan Walsh Date: Fri, 10 Mar 2017 10:28:09 +1100 Subject: [PATCH] added refresh and check expiry functions. Also updated the sample app --- lib/application.js | 223 +++++++++++++++++++++++++++----------------- sample_app/index.js | 124 +++++++++++++++--------- 2 files changed, 216 insertions(+), 131 deletions(-) diff --git a/lib/application.js b/lib/application.js index 43e205f4..d3902552 100644 --- a/lib/application.js +++ b/lib/application.js @@ -98,55 +98,73 @@ Object.assign(Application.prototype, { options = options || {}; return new Promise(function(resolve, reject) { var params = {}; - if (options.summarizeErrors === false) - params.summarizeErrors = false; - //Added to support more than 2dp being added. - if (options.unitdp) - params.unitdp = options.unitdp; + self.checkExpiry() + .then(function() { + if (options.summarizeErrors === false) + params.summarizeErrors = false; - var endPointUrl = options.api === 'payroll' ? self.options.payrollAPIEndPointUrl : self.options.coreAPIEndPointUrl; - var url = self.options.baseUrl + endPointUrl + path; - if (!_.isEmpty(params)) - url += '?' + querystring.stringify(params); + //Added to support more than 2dp being added. + if (options.unitdp) + params.unitdp = options.unitdp; - self.oa[method](url, self.options.accessToken, self.options.accessSecret, { xml: body }, function(err, data, res) { + var endPointUrl = options.api === 'payroll' ? self.options.payrollAPIEndPointUrl : self.options.coreAPIEndPointUrl; + var url = self.options.baseUrl + endPointUrl + path; + if (!_.isEmpty(params)) + url += '?' + querystring.stringify(params); - if (err && data && data.indexOf('oauth_problem') >= 0) { - var errObj = new Error(method.toUpperCase() + ' call failed with: ' + err.statusCode); - errObj.data = qs.parse(data); - reject(errObj); - callback && callback(errObj); - return; - } + self.oa[method](url, self.options.accessToken, self.options.accessSecret, { xml: body }, function(err, data, res) { - self.xml2js(data) - .then(function(obj) { - if (err) { - var exception = ""; - if (obj.ApiException) - exception = obj.ApiException; - else if (obj.Response.ErrorNumber) - exception = obj.Response; - var errObj = new Error(method.toUpperCase() + ' call failed with: ' + err.statusCode + ' and exception: ' + JSON.stringify(exception, null, 2)); + if (err && data && data.indexOf('oauth_problem') >= 0) { + var errObj = new Error(method.toUpperCase() + ' call failed with: ' + err.statusCode); + errObj.data = qs.parse(data); reject(errObj); callback && callback(errObj); - } else { - var ret = { response: obj.Response, res: res }; - if (options.entityConstructor) { - ret.entities = self.convertEntities(obj.Response, options); - } - resolve(ret); - callback && callback(null, obj, res, ret.entities); + return; } - }) - .catch(function(err) { - logger.error(err); - throw err; - }) + self.xml2js(data) + .then(function(obj) { + if (err) { + var exception = ""; + if (obj.ApiException) + exception = obj.ApiException; + else if (obj.Response.ErrorNumber) + exception = obj.Response; + var errObj = new Error(method.toUpperCase() + ' call failed with: ' + err.statusCode + ' and exception: ' + JSON.stringify(exception, null, 2)); + reject(errObj); + callback && callback(errObj); + } else { + var ret = { response: obj.Response, res: res }; + if (options.entityConstructor) { + ret.entities = self.convertEntities(obj.Response, options); + } + resolve(ret); + callback && callback(null, obj, res, ret.entities); + } + + }) + .catch(function(err) { + logger.error(err); + throw err; + }) + + }); + }) + .catch(function(err) { + logger.debug(err); + if (err && err.data) { + var dataParts = qs.parse(err.data); + + var errObj = new Error(method.toUpperCase() + ' call failed with: ' + err.statusCode); + errObj.data = dataParts; + reject(errObj); + callback && callback(errObj); + return; + } + }); + - }); }); }, delete: function(path, options, callback) { @@ -157,42 +175,57 @@ Object.assign(Application.prototype, { var endPointUrl = options.api === 'payroll' ? self.options.payrollAPIEndPointUrl : self.options.coreAPIEndPointUrl; var url = self.options.baseUrl + endPointUrl + path; - self.oa.delete(url, self.options.accessToken, self.options.accessSecret, function(err, data, res) { - if (options.stream && !err) { - // Already done - return resolve(); - } - if (err && data && data.indexOf('oauth_problem') >= 0) { - var errObj = new Error('DELETE call failed with: ' + err.statusCode); - errObj.data = qs.parse(data); - reject(errObj); - callback && callback(errObj); - return; - } + self.checkExpiry() + .then(function() { + self.oa.delete(url, self.options.accessToken, self.options.accessSecret, function(err, data, res) { + if (options.stream && !err) { + // Already done + return resolve(); + } + if (err && data && data.indexOf('oauth_problem') >= 0) { + var errObj = new Error('DELETE call failed with: ' + err.statusCode); + errObj.data = qs.parse(data); + reject(errObj); + callback && callback(errObj); + return; + } - if (err) { - var errObj = new Error('DELETE call failed with: ' + err.statusCode + ' and message: ' + err.data); - reject(errObj); - callback && callback(errObj); - return; - } + if (err) { + var errObj = new Error('DELETE call failed with: ' + err.statusCode + ' and message: ' + err.data); + reject(errObj); + callback && callback(errObj); + return; + } - //Some delete operations don't return any content (e.g. HTTP204) so simply resolve the promise - if (!data || data === "") { - return resolve(); - } + //Some delete operations don't return any content (e.g. HTTP204) so simply resolve the promise + if (!data || data === "") { + return resolve(); + } - self.xml2js(data) - .then(function(obj) { - var ret = { response: obj.Response, res: res }; - resolve(ret); - callback && callback(null, obj, res); - }) - .catch(function(err) { - logger.error(err); - throw err; - }) - }, { stream: options.stream }); + self.xml2js(data) + .then(function(obj) { + var ret = { response: obj.Response, res: res }; + resolve(ret); + callback && callback(null, obj, res); + }) + .catch(function(err) { + logger.error(err); + throw err; + }) + }, { stream: options.stream }); + }) + .catch(function(err) { + logger.debug(err); + if (err && err.data) { + var dataParts = qs.parse(err.data); + + var errObj = new Error('DELETE call failed with: ' + err.statusCode); + errObj.data = dataParts; + reject(errObj); + callback && callback(errObj); + return; + } + }); }); }, get: function(path, options, callback) { @@ -213,6 +246,18 @@ Object.assign(Application.prototype, { getResource(options.pager.start || 1) else getResource(); + }) + .catch(function(err) { + logger.debug(err); + if (err && err.data) { + var dataParts = qs.parse(err.data); + + var errObj = new Error('GET call failed with: ' + err.statusCode); + errObj.data = dataParts; + reject(errObj); + callback && callback(errObj); + return; + } }); function getResource(offset) { @@ -418,15 +463,28 @@ Object.assign(Application.prototype, { return obj; }, checkExpiry: function() { - var d1 = new Date(this.options.tokenExpiry), - d2 = new Date(); - if (d2 > d1) { + /** + * CheckExpiry is a helper function that will compare the current token expiry to the current time. + * + * As there is potential for a time difference, instead of waiting all the way until the current time + * has passed the expiry time, we instead add 3 minutes to the current time, and use that as a comparison. + * + * This ensures that if the token is 'nearing' the expiry, it'll attempt to be refreshed. + */ + + var expiry = new Date(this.options.tokenExpiry), + checkTime = addMinutes(new Date(), 3); + + if (checkTime >= expiry) { return this.refreshAccessToken(); } else { - logger.debug("Dates are fine, no need for refresh"); return Promise.resolve(); } + + function addMinutes(date, minutes) { + return new Date(date.getTime() + minutes * 60000); + } } }) @@ -490,13 +548,12 @@ var RequireAuthorizationApplication = Application.extend({ reject(err); else { var exp = new Date(); - exp.setSeconds(exp.getSeconds() + results.oauth_expires_in); - + exp.setTime(exp.getTime() + (results.oauth_expires_in * 1000)); self.setOptions({ accessToken: results.oauth_token, accessSecret: results.oauth_token_secret, sessionHandle: results.oauth_session_handle, - tokenExpiry: exp.toISOString() + tokenExpiry: exp.toString() }); resolve({ results: results }); } @@ -513,13 +570,12 @@ var RequireAuthorizationApplication = Application.extend({ reject(err); else { var exp = new Date(); - exp.setSeconds(exp.getSeconds() + results.oauth_expires_in); - + exp.setTime(exp.getTime() + (results.oauth_expires_in * 1000)); self.setOptions({ accessToken: results.oauth_token, accessSecret: results.oauth_token_secret, sessionHandle: results.oauth_session_handle, - tokenExpiry: exp.toISOString() + tokenExpiry: exp.toString() }); resolve({ results: results }); } @@ -537,9 +593,6 @@ var RequireAuthorizationApplication = Application.extend({ this.options.accessSecret = options.accessSecret; this.options.sessionHandle = options.sessionHandle; this.options.tokenExpiry = options.tokenExpiry; - - console.log("options set!"); - console.log(this.options); } }); diff --git a/sample_app/index.js b/sample_app/index.js index 2b6c1928..e1999fc6 100644 --- a/sample_app/index.js +++ b/sample_app/index.js @@ -123,6 +123,20 @@ function authorizedOperation(req, res, returnTo, callback) { } } +function handleErr(err, req, res, returnTo) { + console.log(err); + if (err.data && err.data.oauth_problem && err.data.oauth_problem == "token_rejected") { + authorizeRedirect(req, res, returnTo); + } else { + res.redirect('error'); + } +} + +app.get('/error', function(req, res) { + console.log(req.query.error); + res.render('index', { error: req.query.error }); +}) + // Home Page app.get('/', function(req, res) { res.render('index', { @@ -143,7 +157,7 @@ app.get('/access', function(req, res) { res.redirect(returnTo || '/'); }) .catch(function(err) { - console.log(err); + handleErr(err, req, res, 'error'); }) } }); @@ -160,7 +174,7 @@ app.get('/organisations', function(req, res) { }); }) .catch(function(err) { - console.log(err); + handleErr(err, req, res, 'organisations'); }) }) }); @@ -176,6 +190,9 @@ app.get('/taxrates', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'taxrates'); + }) }) }); @@ -190,6 +207,9 @@ app.get('/users', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'users'); + }) }) }); @@ -205,22 +225,11 @@ app.get('/employees', function(req, res) { }); }) .catch(function(err) { - console.log(err) - res.render('employees', { - error: err, - active: { - employees: true - } - }) + handleErr(err, req, res, 'employees'); }) }) }); -app.get('/error', function(req, res) { - console.log(req.query.error); - res.render('index', { error: req.query.error }); -}) - app.get('/contacts', function(req, res) { authorizedOperation(req, res, '/contacts', function(xeroClient) { var contacts = []; @@ -233,6 +242,9 @@ app.get('/contacts', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'contacts'); + }) function pagerCallback(err, response, cb) { contacts.push.apply(contacts, response.data); @@ -253,6 +265,9 @@ app.get('/banktransactions', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'banktransactions'); + }) function pagerCallback(err, response, cb) { bankTransactions.push.apply(bankTransactions, response.data); @@ -273,6 +288,9 @@ app.get('/journals', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'journals'); + }) function pagerCallback(err, response, cb) { journals.push.apply(journals, response.data); @@ -293,6 +311,9 @@ app.get('/banktransfers', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'banktransfers'); + }) function pagerCallback(err, response, cb) { bankTransfers.push.apply(bankTransfers, response.data); @@ -312,6 +333,9 @@ app.get('/payments', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'payments'); + }) }) }); @@ -326,6 +350,9 @@ app.get('/trackingcategories', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'trackingcategories'); + }) }) }); @@ -340,6 +367,9 @@ app.get('/accounts', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'accounts'); + }) }) }); @@ -356,43 +386,12 @@ app.get('/timesheets', function(req, res) { }); }) .catch(function(err) { - console.log(err) - res.render('timesheets', { - error: err, - active: { - timesheets: true - } - }) + handleErr(err, req, res, 'timesheets'); }) }) }); -app.use('/createtimesheet', function(req, res) { - if (req.method == 'GET') { - return res.render('createtimesheet'); - } else if (req.method == 'POST') { - authorizedOperation(req, res, '/createtimesheet', function(xeroClient) { - var timesheet = xeroClient.payroll.timesheets.newTimesheet({ - EmployeeID: '065a115c-ba9c-4c03-b8e3-44c551ed8f21', - StartDate: new Date(2014, 8, 23), - EndDate: new Date(2014, 8, 29), - Status: 'Draft', - TimesheetLines: [{ - EarningsTypeID: 'a9ab82bf-c421-4840-b245-1df307c2127a', - NumberOfUnits: [5, 0, 0, 0, 0, 0, 0] - }] - }); - timesheet.save() - .then(function(ret) { - res.render('createtimesheet', { timesheets: ret.entities }) - }) - .catch(function(err) { - res.render('createtimesheet', { err: err }) - }) - }) - } -}); app.get('/invoices', function(req, res) { @@ -407,6 +406,9 @@ app.get('/invoices', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'invoices'); + }) }) }); @@ -422,6 +424,9 @@ app.get('/items', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'items'); + }) }) }); @@ -458,6 +463,33 @@ app.use('/createinvoice', function(req, res) { } }); +app.use('/createtimesheet', function(req, res) { + if (req.method == 'GET') { + return res.render('createtimesheet'); + } else if (req.method == 'POST') { + authorizedOperation(req, res, '/createtimesheet', function(xeroClient) { + var timesheet = xeroClient.payroll.timesheets.newTimesheet({ + EmployeeID: '065a115c-ba9c-4c03-b8e3-44c551ed8f21', + StartDate: new Date(2014, 8, 23), + EndDate: new Date(2014, 8, 29), + Status: 'Draft', + TimesheetLines: [{ + EarningsTypeID: 'a9ab82bf-c421-4840-b245-1df307c2127a', + NumberOfUnits: [5, 0, 0, 0, 0, 0, 0] + }] + }); + timesheet.save() + .then(function(ret) { + res.render('createtimesheet', { timesheets: ret.entities }) + }) + .catch(function(err) { + res.render('createtimesheet', { err: err }) + }) + + }) + } +}); + app.use('/emailinvoice', function(req, res) { if (req.method == 'GET' && !req.query.a) { res.render('emailinvoice', { id: req.query.id });