xero-oauth2.0
Version:
Middleware application to connect Xero by using oAUth2.0 The Code Wok Flow method
564 lines (480 loc) • 15 kB
JavaScript
var _ = require('lodash'),
querystring = require('querystring'),
https = require('https'),
http = require('http'),
URL = require('url'),
OAuthUtils = require('oauth/lib/_utils');
/**
*
* @param {*} clientId
* @param {*} clientSecret
* @param {*} redirectURI
*/
function xeroOAuth2(clientId, clientSecret, redirectURI) {
this._clientId = clientId
this._clientSecret = clientSecret
this._accessTokenName = "Basic";
this._authMethod = "Bearer";
this._redirectURI = redirectURI;
}
/**
* returns the resource uri depend on api
* @param {*} api
*/
function getResourceURI(api) {
switch (api) {
case 'payroll-au':
return 'https://api.xero.com/payroll.xro/1.0';
case 'payroll-nz':
return 'https://api.xero.com/payroll.xro/2.0';
case 'payroll-uk':
return 'https://api.xero.com/payroll.xro/2.0';
case 'accounts':
return `https://api.xero.com/api.xro/2.0`;
case 'assets':
return 'https://api.xero.com/assets.xro/1.0';
case 'projects':
return 'https://api.xero.com/projects.xro/2.0';
case 'bank feeds':
return 'https://api.xero.com/bankfeeds.xro/1.0';
case 'connection':
return 'https://api.xero.com';
case 'identity':
return 'https://identity.xero.com';
case 'consent':
return 'https://login.xero.com';
default:
throw 'api not found';
}
}
/**
*
* @param {*} redirect_uri :String
* @param {*} scope:String[]
*/
function getConsentURL(scope) {
if (!scope || scope.length === 0) {
return null;
}
if (typeof scope === 'Array') {
scope = scope.join(" ");
}
var url = "";
try {
url = getResourceURI('consent') + `/identity/connect/authorize?response_type=code&client_id=${this._clientId}&redirect_uri=${this._redirectURI}&scope=${scope}&state=123`
} catch (err) {
return callback(err);
};
return url;
}
/**
*
* @param {*} code
* @param {*} callback
*/
function getAcceesToken(code, callback) {
var params = params || {};
var token = new Buffer(`${this._clientId}:${this._clientSecret}`).toString('base64');
var post_header = {
'Authorization': `Basic ${token}`,
'Content-type': 'application/x-www-form-urlencoded'
}
params['code'] = code;
params['grant_type'] = 'authorization_code';
params['redirect_uri'] = this._redirectURI;
var post_data = querystring.stringify(params);
var url = "";
try {
url = getResourceURI('identity') + `/connect/token`
} catch (err) {
callback(err);
};
this._request('POST', url, post_header, post_data, token, function (err, data) {
if (err) {
callback(err);
} else {
var results;
try {
results = JSON.parse(data);
} catch (err) {
results = querystring.parse(data);
}
var access_token = results["access_token"];
var refresh_token = results["refresh_token"];
delete results["refresh_token"];
callback(null, access_token, refresh_token, results)
}
});
}
/**
*
* @param {*} refreshToken
* @param {*} callback
*/
function refreshToken(refreshToken, callback) {
var params = params || {};
var token = new Buffer(`${this._clientId}:${this._clientSecret}`).toString('base64');
var post_header = {
'Authorization': `Basic ${token}`,
'Content-type': 'application/x-www-form-urlencoded'
}
params['grant_type'] = 'refresh_token';
params['refresh_token'] = refreshToken;
var post_data = querystring.stringify(params);
var url = "";
try {
url = getResourceURI('identity') + `/connect/token`
} catch (err) {
return callback(err);
};
this._request('POST', url, post_header, post_data, token, function (err, data) {
if (err) {
callback(err);
} else {
var results;
try {
results = JSON.parse(data);
} catch (err) {
results = querystring.parse(data);
}
var access_token = results["access_token"];
var refresh_token = results["refresh_token"];
delete results["refresh_token"];
callback(null, access_token, refresh_token, results)
}
});
}
/**
*
* @param {*} acces_token
* @param {*} callback
*/
function getTenants(access_token, callback) {
var url = "";
var headers = {
'Authorization': `${this._authMethod} ${access_token}`,
'Accept': 'application/json'
}
try {
url = getResourceURI('connection') + '/connections'
} catch (err) {
callback(err);
};
this._request('GET', url, headers, null, null, function (err, data) {
if (err) {
callback(err);
} else {
try {
results = JSON.parse(data);
} catch (err) {
results = querystring.parse(data);
}
callback(null, results);
}
});
}
/**
* disconnect organization
* @param {*} tenantId
* @param {*} callback
*/
function disconnect(tenantId, callback) {
var url = "";
try {
url = getResourceURI('connection') + `/connections/${tenantId}`
} catch (err) {
callback(err);
};
this._request('DELETE', url, null, null, null, function (err, data) {
if (err) {
callback(err);
} else {
callback(null, data);
}
})
}
/**
* revoke current token
* only token with offline access could revoke
* @param {*} refreshToken
* @param {*} callback
*/
function revokeToken(refreshToken, callback) {
var url = "";
try {
url = getResourceURI('identity') + `/connect/revocation`
} catch (err) {
callback(err);
};
var token = new Buffer(`${this._clientId}:${this._clientSecret}`).toString('base64');
var post_header = {
'Authorization': `Basic ${token}`,
'Content-type': 'application/x-www-form-urlencoded'
}
var post_body = `token=${refreshToken}`
this._request('POST', url, post_header, post_body, null, function (err, data) {
if (err) {
callback(err);
} else {
callback(null, data);
}
})
}
/**
* determines which protocol need to be pick based on url
* @param {*} parsedUrl
*/
function chooseHttpLibrary(parsedUrl) {
//default to https
var http_library = https;
if (parsedUrl.protocol !== "https:") {
http_library = http;
}
return http_library;
}
/**
*
* @param {*} method
* @param {*} url
* @param {*} headers
* @param {*} post_body
* @param {*} acces_token
* @param {*} callback
*/
function request(method, url, headers, post_body, acces_token, callback) {
var parsedUrl = URL.parse(url, true);
if (parsedUrl.protocol === "https:" && !parsedUrl.port) {
parsedUrl.port = 443;
}
var http_library = chooseHttpLibrary(parsedUrl);
var request_headers = {};
if (headers) {
for (var key in headers) {
request_headers[key] = headers[key];
}
}
//set host
request_headers['Host'] = parsedUrl.host;
if (!request_headers['User-Agent']) {
request_headers['User-Agent'] = 'Node-oauth';
}
//set request content length
if (post_body) {
if (Buffer.isBuffer(post_body)) {
request_headers['Content-Length'] = post_body.length;
} else {
request_headers['Content-Length'] = Buffer.byteLength(post_body);
}
} else {
request_headers['Content-Length'] = 0;
}
// setting up token if not already define in the header
if (acces_token && !('Authorization' in request_headers)) {
if (!parsedUrl.query) {
parsedUrl.query = {};
}
parsedUrl.query[this._accessTokenName] = acces_token;
}
var queryStr = querystring.stringify(parsedUrl.query);
if (queryStr) queryStr = "?" + queryStr;
var options = {
host: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname + queryStr,
method: method,
headers: request_headers
}
//execute request
executeRequest(http_library, options, post_body, callback);
}
/**
* execute request
* @param {*} http_library
* @param {*} options
* @param {*} post_body
* @param {*} callback
*/
function executeRequest(http_library, options, post_body, callback) {
var result = "";
var allowEarlyClose = OAuthUtils.isAnEarlyCloseHost(options.host);
var callbackCalled = false;
function passBackControl(response, result) {
if (!callbackCalled) {
callbackCalled = true;
if (!(response.statusCode >= 200 && response.statusCode <= 299) && (response.statusCode !== 301) && (response.statusCode !== 302)) {
callback({ statusCode: response.statusCode, data: result })
} else {
callback(null, result, response)
}
}
}
var request = http_library.request(options);
//on response
request.on('response', function (response) {
//data
response.on('data', function (chunk) {
result += chunk;
})
//close
response.on('close', function (err) {
if (allowEarlyClose) {
passBackControl(response, result);
}
});
//end
response.addListener('end', function () {
passBackControl(response, result);
});
});
//on error
request.on('error', function (err) {
callbackCalled = true;
callback(err);
});
if ((options.method === 'POST' || options.method === 'PUT') && post_body) {
request.write(post_body);
}
request.end();
}
/**
*
* @param {*} api
* @param {*} apiPath
* @param {*} access_token
* @param {*} tenant_id
* @param {*} options
* @param {*} callback
*/
function getRequest(api, apiPath, access_token, tenant_id, options, callback) {
var url
var headers = {
'Authorization': `${this._authMethod} ${access_token}`,
'Accept': 'application/json'
}
if (tenant_id) {
headers['Xero-tenant-id'] = tenant_id;
try {
url = getResourceURI(api) + apiPath
} catch (err) {
callback(err);
};
} else {
callback({ message: 'tenant id not found' })
}
//headers
if (options && options.headers) {
_.forEach(options.headers, function (item, key) {
post_header[key] = item;
})
}
this._request('GET', url, headers, null, null, function (err, data, response) {
if (err) {
callback(err,null,response);
} else {
var results
try {
results = JSON.parse(data);
responseObj = response;
} catch (err) {
callback(querystring.parse(data),null,response);
}
callback(null, results, response);
}
});
}
/**
*
* @param {*} api
* @param {*} apiPath
* @param {*} post_data
* @param {*} access_token
* @param {*} tenant_id
* @param {*} options
* @param {*} callback
*/
function postRequest(api, apiPath, post_data, access_token, tenant_id, options, callback) {
//Payroll - Australia haven't launched the api 2.0 yet hence header had to put xml type
var post_header = {
'Authorization': `${this._authMethod} ${access_token}`,
'Xero-tenant-id': `${tenant_id}`,
};
var url = null;
//headers
if (options && options.headers) {
_.forEach(options.headers, function (item, key) {
post_header[key] = item;
})
}
try {
url = getResourceURI(api) + apiPath
} catch (err) {
return callback(err);
};
this._request('POST', url, post_header, post_data, null, function (err, data, response) {
if (err) {
callback(err,null,response);
} else {
var results;
try {
results = JSON.parse(data);
} catch (err) {
results = querystring.parse(data);
}
callback(null, results, response);
}
});
}
/**
*
* @param {*} apiPath
* @param {*} content_type
* @param {*} post_data
* @param {*} access_token
* @param {*} tenant_id
* @param {*} options
* @param {*} callback
*/
function postAttachment(apiPath, post_data, access_token, tenant_id, options, callback) {
var post_header = {
'Authorization': `${this._authMethod} ${access_token}`,
'Xero-tenant-id': `${tenant_id}`,
};
var url = null;
//headers
if (options && options.headers) {
_.forEach(options.headers, function (item, key) {
post_header[key] = item;
})
}
try {
url = getResourceURI('accounts') + apiPath;
} catch (err) {
return callback(err);
};
this._request('POST', url, post_header, post_data, null, function (err, data, response) {
if (err) {
callback(err,null,response);
} else {
var results;
try {
results = JSON.parse(data);
} catch (err) {
results = querystring.parse(data);
}
callback(null, results, response);
}
});
}
exports.xeroOAuth2 = xeroOAuth2;
exports.xeroOAuth2.prototype._getConsentURL = getConsentURL;
exports.xeroOAuth2.prototype._getAccessToken = getAcceesToken;
exports.xeroOAuth2.prototype._refreshToken = refreshToken;
exports.xeroOAuth2.prototype._getTenants = getTenants;
exports.xeroOAuth2.prototype._disconnect = disconnect;
exports.xeroOAuth2.prototype._revokeToken = revokeToken;
exports.xeroOAuth2.prototype._chooseHttpLibrary = chooseHttpLibrary;
exports.xeroOAuth2.prototype._request = request;
exports.xeroOAuth2.prototype._executeRequest = executeRequest;
exports.xeroOAuth2.prototype._get = getRequest;
exports.xeroOAuth2.prototype._post = postRequest;
exports.xeroOAuth2.prototype._postAttachment = postAttachment;