├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── xeroClient.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | #idea IDE 36 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Node Vision 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xero-client 2 | Node.JS integration client for Xero accounting software. Allows integrating [Public apps](http://developer.xero.com/documentation/getting-started/api-application-types/) 3 | 4 | # Features: 5 | 6 | - full xero authentication flow 7 | - send and receive all entities to (from) Xero 8 | - automatically saves auth data to req.session.xeroAuth for token and secret reuse ( can be worked around if you don't want to use session) 9 | 10 | 11 | # Usage 12 | 13 | ## 1) Install with npm into your project 14 | `npm install xero-client --save` 15 | 16 | ## 2) Initialize client 17 | 18 | - All params are mandatory 19 | 20 | ``` 21 | var xeroClient = require('xero-client')({ 22 | xeroConsumerKey: 'key', //if omitted, env variable XERO_CONSUMER_KEY will be used 23 | xeroConsumerSecret: 'secret', //if omitted, env variable XERO_CONSUMER_SECRET will be used 24 | xeroCallbackUrl: 'http://...' //if omitted, env variable XERO_CALLBACK_URL will be used 25 | }); 26 | ``` 27 | `... initialize session` 28 | 29 | ## 3) Set routes 30 | 31 | You will need 2 routes: 32 | - authentication entry point 33 | - callback when client is successfully authorized 34 | 35 | Example: 36 | ``` 37 | var express = require('express'); 38 | var router = express.Router(); 39 | router 40 | .get('/authenticate', xeroClient.authenticate) 41 | .get('/callback', xeroClient.callback); 42 | ``` 43 | 44 | ## 4) Use client: 45 | ``` 46 | //contacts and invoices have a thin wrapper around raw requests 47 | xeroClient.syncContacts(contacts, req, function(err, xeroContacts){ 48 | if (err){ 49 | //handle errors 50 | } 51 | //do something with xeroContacts 52 | } 53 | //any other entity can be sent or received with raw request: 54 | xeroClient._putRequest(req, 'https://api.xero.com/api.xro/2.0/Payments', 'Payments', payments, function(err, xeroPayments){ 55 | if (err){ 56 | //handle errors 57 | } 58 | //do something with xeroPayments 59 | } 60 | //xeroClient._getRequest(req, url, root, callback) 61 | //xeroClient._postRequest(req, url, xmlRoot, data, callback) 62 | 63 | ``` 64 | 65 | ### To see complete app built with node.js and react using xero-client see [node-react-xero-app on github](https://github.com/node-vision/node-react-xero-app) and read post [How to integrate node.js/react app with Xero](https://nodevision.com.au/blog/post/how-to-integrate-nodejsreact-app-with-xero) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var xeroClient = require('./xeroClient'); 4 | 5 | module.exports = function(){ 6 | var res = {}; 7 | res.syncContacts = xeroClient.syncContacts; 8 | res.syncInvoices = xeroClient.syncInvoices; 9 | res.getOrganizationInfo = xeroClient.getOrganizationInfo; 10 | res.syncStatus = xeroClient.syncStatus; 11 | res.authenticate = xeroClient.requestXeroRequestToken; 12 | res.callback = xeroClient.requestXeroAccessToken; 13 | res._getRequest = xeroClient._getRequest; 14 | res._postRequest = xeroClient._postRequest; 15 | res._putRequest = xeroClient._putRequest; 16 | 17 | return function(config){ 18 | config = config || {}; 19 | config.xeroConsumerKey = config.xeroConsumerKey || process.env.XERO_CONSUMER_KEY; 20 | config.xeroConsumerSecret = config.xeroConsumerSecret || process.env.XERO_CONSUMER_SECRET; 21 | config.xeroCallbackUrl = config.xeroCallbackUrl || process.env.XERO_CALLBACK_URL; 22 | xeroClient.setConfig(config); 23 | return res; 24 | }; 25 | 26 | }(); 27 | 28 | 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xero-client", 3 | "version": "0.1.1", 4 | "description": "Node.Js integration client for Xero accounting software", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/node-vision/xero-client.git" 12 | }, 13 | "keywords": [ 14 | "xero", 15 | "accounting", 16 | "node", 17 | "integration", 18 | "public", 19 | "client" 20 | ], 21 | "author": "Roman Mandryk", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/node-vision/xero-client/issues" 25 | }, 26 | "homepage": "https://github.com/node-vision/xero-client#readme", 27 | "dependencies": { 28 | "easyxml": "^2.0.1", 29 | "express-session": "^1.13.0", 30 | "inflect": "^0.3.0", 31 | "oauth": "^0.9.14" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /xeroClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Roman Mandryk on 19/04/2016. 3 | */ 4 | 5 | /** 6 | * Client connecting to Xero Api - https://github.com/node-vision/xero-client 7 | */ 8 | 9 | var OAuth = require("oauth"); 10 | var EasyXml = require('easyxml'); 11 | var inflect = require('inflect'); 12 | 13 | var REQUEST_URL = 'https://api.xero.com/oauth/RequestToken'; 14 | var ACCESS_URL = 'https://api.xero.com/oauth/AccessToken'; 15 | var API_BASE_URL = 'https://api.xero.com/api.xro/2.0/'; 16 | var ORGANIZATION_URL = API_BASE_URL + 'Organisation'; 17 | var INVOICES_URL = API_BASE_URL + 'Invoices'; 18 | var INVOICES_POST_URL = INVOICES_URL + '?SummarizeErrors=false'; 19 | var CONTACTS_URL = API_BASE_URL + 'Contacts'; 20 | var AUTHORIZE_URL = 'https://api.xero.com/oauth/Authorize?oauth_token='; 21 | 22 | var config = {}; 23 | 24 | // Xero API defaults to application/xml content-type 25 | var customHeaders = { 26 | "Accept": "application/json", 27 | "Connection": "close" 28 | }; 29 | 30 | var oauth = {}; 31 | 32 | exports.setConfig = function(cfg){ 33 | config = cfg; 34 | oauth = new OAuth.OAuth( 35 | REQUEST_URL, 36 | ACCESS_URL, 37 | config.xeroConsumerKey, 38 | config.xeroConsumerSecret, 39 | '1.0A', 40 | null, 41 | 'HMAC-SHA1', 42 | null, 43 | customHeaders 44 | ); 45 | // This is important - Xero will redirect to this URL after successful authentication 46 | // and provide the request token as query parameters 47 | oauth._authorize_callback = config.xeroCallbackUrl; 48 | }; 49 | 50 | /** 51 | * Initiate the request to Xero to get an oAuth Request Token. 52 | * With the token, we can send the user to Xero's authorize page 53 | * @param req 54 | * @param res 55 | */ 56 | exports.requestXeroRequestToken = function (req, res) { 57 | 58 | oauth.getOAuthRequestToken(function (error, oauth_token, oauth_token_secret, results) { 59 | 60 | if (error) { 61 | console.log(error); 62 | return res.status(500).send('failed'); 63 | } 64 | 65 | // store the token in the session 66 | req.session.xeroAuth = { 67 | token: oauth_token, 68 | token_secret: oauth_token_secret 69 | }; 70 | 71 | // redirect the user to Xero's authorize url page 72 | return res.redirect(AUTHORIZE_URL + oauth_token); 73 | }); 74 | }; 75 | 76 | 77 | /** 78 | * Perform the callback leg of the three-legged oAuth. 79 | * Given the auth_token and auth_verifier from xero, request the AccessToken 80 | * @param req 81 | * @param res 82 | */ 83 | exports.requestXeroAccessToken = function (req, res) { 84 | var oAuthData = req.session.xeroAuth; 85 | if (!oAuthData) { 86 | return res.status(500).send('failed'); 87 | } 88 | oAuthData.verifier = req.query.oauth_verifier; 89 | 90 | 91 | oauth.getOAuthAccessToken( 92 | oAuthData.token, 93 | oAuthData.token_secret, 94 | oAuthData.verifier, 95 | function (error, oauth_access_token, oauth_access_token_secret, results) { 96 | 97 | if (error) { 98 | console.error(error); 99 | return res.status(403).send("Authentication Failure!"); 100 | } 101 | 102 | req.session.xeroAuth = { 103 | token: oAuthData.token, 104 | token_secret: oAuthData.token_secret, 105 | verifier: oAuthData.verifier, 106 | access_token: oauth_access_token, 107 | access_token_secret: oauth_access_token_secret, 108 | //expires in 30 mins 109 | access_token_expiry: new Date(new Date().getTime() + 30 * 60 * 1000) 110 | }; 111 | return res.send('Successfully authorized, closing...'); 112 | } 113 | ); 114 | }; 115 | 116 | /** 117 | * Returns status of xero authentication token and latest sync results * 118 | * @param req 119 | * @param callback 120 | */ 121 | exports.syncStatus = function(req, callback){ 122 | var isAuthenticated = (req.session.xeroAuth 123 | && req.session.xeroAuth.access_token_expiry 124 | && new Date(req.session.xeroAuth.access_token_expiry).getTime() >= new Date().getTime()); 125 | var json = {isAuthenticated: isAuthenticated}; 126 | if (isAuthenticated){ 127 | json.contactsSynced = req.session.xeroAuth.contactsSynced; 128 | json.invoicesSynced = req.session.xeroAuth.invoicesSynced; 129 | json.lastSyncTime = req.session.xeroAuth.lastSyncTime; 130 | json.accessTokenExpiry = req.session.xeroAuth.access_token_expiry; 131 | } 132 | callback(json); 133 | }; 134 | 135 | 136 | 137 | /** 138 | * get Organization info 139 | * @param req 140 | * @param callback 141 | */ 142 | exports.getOrganizationInfo = function (req, callback) { 143 | oauth.get(ORGANIZATION_URL, 144 | req.session.xeroAuth.access_token, 145 | req.session.xeroAuth.access_token_secret, 146 | function (e, data, response) { 147 | if (e) { 148 | console.error(e); 149 | return; 150 | } 151 | var res = JSON.parse(data); 152 | callback(res); 153 | }); 154 | }; 155 | 156 | 157 | /** 158 | * synchronizes all contacts (in Xero format) from local app to XERO 159 | * @param contacts - list of contacts in Xero format 160 | * @param req 161 | * @param callback 162 | */ 163 | exports.syncContacts = function (contacts, req, callback) { 164 | makePostRequest(req, CONTACTS_URL, 'Contacts', contacts, function (err, contacts) { 165 | if (err) { 166 | return callback(err); 167 | } 168 | //update number of updated contacts for client 169 | req.session.xeroAuth.contactsSynced = contacts.length; 170 | callback(null, contacts); 171 | }); 172 | 173 | }; 174 | 175 | /** 176 | * synchronizes all invoices (in Xero format) from local app to XERO 177 | * @param invoices 178 | * @param req 179 | * @param callback 180 | */ 181 | exports.syncInvoices = function (invoices, req, callback) { 182 | makePostRequest(req, INVOICES_POST_URL, 'Invoices', invoices, function (err, xeroInvoices) { 183 | if (err) { 184 | return callback(err); 185 | } 186 | //update number of updated invoices for client 187 | req.session.xeroAuth.invoicesSynced = invoices.length; 188 | 189 | callback(null, xeroInvoices); 190 | }); 191 | }; 192 | 193 | /** 194 | * raw get request function 195 | * @param req - required to get req.session.xeroAuth parameters (xero token and secret) 196 | * @param url - e.g. https://api.xero.com/api.xro/2.0/Invoices 197 | * @param root - e.g. 'Invoices' 198 | * @param callback (err, result) - returns error or parsed js array of results 199 | */ 200 | function makeGetRequest(req, url, root, callback) { 201 | oauth.get(url, 202 | req.session.xeroAuth.access_token, 203 | req.session.xeroAuth.access_token_secret, 204 | function (e, data, response) { 205 | if (e) { 206 | return callback(e); 207 | } 208 | var res = JSON.parse(data); 209 | callback(null, res[root]); 210 | }); 211 | } 212 | 213 | /** 214 | * raw put request function 215 | * @param req - required to get req.session.xeroAuth parameters (xero token and secret) 216 | * @param url - e.g. https://api.xero.com/api.xro/2.0/Payments 217 | * @param xmlRoot - e.g. 'Payments' 218 | * @param data - javascript array of objects in Xero format 219 | * @param callback (err, result) - returns error or parsed js array of results 220 | */ 221 | function makePutRequest(req, url, xmlRoot, data, callback) { 222 | makePostOrPutRequest(req, url, xmlRoot, data, callback, true); 223 | } 224 | 225 | /** 226 | * raw post request function 227 | * @param req - required to get req.session.xeroAuth parameters (xero token and secret) 228 | * @param url - e.g. https://api.xero.com/api.xro/2.0/Invoices 229 | * @param xmlRoot - e.g. 'Invoices' 230 | * @param data - javascript array of objects in Xero format 231 | * @param callback (err, result) - returns error or parsed js array of results 232 | */ 233 | function makePostRequest(req, url, xmlRoot, data, callback) { 234 | makePostOrPutRequest(req, url, xmlRoot, data, callback, false); 235 | } 236 | 237 | function makePostOrPutRequest(req, url, xmlRoot, data, callback, usePUT) { 238 | //var root = path.match(/([^\/\?]+)/)[1]; 239 | if (!data || !data.length) { 240 | return callback(null, []); 241 | } 242 | if (!req.session.xeroAuth){ 243 | req.session.xeroAuth = {}; 244 | } 245 | var root = xmlRoot; 246 | var post_body = new EasyXml({rootElement: inflect.singularize(root), rootArray: root, manifest: true}).render(data); 247 | //console.log(post_body); 248 | var content_type = 'application/xml'; 249 | oauth._putOrPost(usePUT ? 'PUT' : 'POST', 250 | url, 251 | req.session.xeroAuth.access_token, 252 | req.session.xeroAuth.access_token_secret, 253 | //we can get json but have to post Xml! - https://community.xero.com/developer/discussion/2900620/ 254 | post_body, 255 | content_type, 256 | function (e, data, response) { 257 | if (e) { 258 | console.error(e); 259 | return callback(e); 260 | } 261 | var res = JSON.parse(data); 262 | callback(null, res[xmlRoot]); 263 | }); 264 | } 265 | 266 | exports._getRequest = makeGetRequest; 267 | exports._postRequest = makePostRequest; 268 | exports._putRequest = makePutRequest; --------------------------------------------------------------------------------