UNPKG

synt_backend

Version:

Synt light-weight node backend service

583 lines (513 loc) 15.1 kB
var _ = require("lodash"), querystring = require("querystring"), https = require("https"); const db = require("./../mysql/models/index"); /** * Client * @param {Object} options Options object * @return {Client} Returns itself */ class Client { constructor(name) { // Defaults var defaults = { client_id: null, env: "production", debug: false, prefixDebug: "[exact-online] ", redirect_uri: null, }; this.name = name; // Merge defaults with passed objects this.options = _.merge({}, defaults); if (this.options.debug) { console.log(this.options.prefixDebug + "Client - createClient"); } // Define OAuth object this.oauth = { request_code: null, authorized: false, expires_at: null, token: {}, refresh_token: null, }; // Define division var this.division = null; // Define API url this.apiUrl = "start.exactonline.be"; return this; } init() { // Get info from database return db.ExactOnlineClient.findOne({ where: { name: this.name } }).then( (client) => { // client options var options = { client_id: client.client_id, client_secret: client.client_secret, env: "development", // process.env.NODE_ENV || "development", debug: process.env.NODE_ENV ? false : true, prefixDebug: "[exact-online] ", redirect_uri: client.redirect_uri, }; // Merge defaults with passed objects this.options = _.merge({}, this.options, options); if (this.options.debug) { console.log(this.options.prefixDebug + "Client - Options Added"); } // Define OAuth object let oauth = { request_code: client.code, authorized: client.expires_at > Date.now(), expires_at: client.expires_at, token: { access_token: client.access_token }, refresh_token: client.refresh_token, }; this.oauth = _.merge({}, this.oauth, oauth); return this; } ); } /** * Authorizes the client * @param {Function} callback Gets called after request is complete */ authorize(code, callback) { var self = this; if (this.options.debug) { console.log( this.options.prefixDebug + '.authorize - Retreiving access token with code: "' + code + '"' ); } this.token( code, "authorization_code", this.options.redirect_uri, function (err, token) { if (self.options.debug) { console.log( self.options.prefixDebug + ".authorize - Retreived token: " + JSON.stringify(token) ); } self.updateDatabaseTokens(token); callback(err, token.refresh_token); } ); } /** * Retrieve oauth token from API endpoint * @param {Function} callback Gets called after request is complete */ token(code, grantType, redirect_uri, callback) { if (this.options.debug) { console.log( this.options.prefixDebug + '.token - send token: "' + code + '"' ); } var data = { grant_type: grantType, client_id: this.options.client_id, client_secret: this.options.client_secret, }; switch (grantType) { case "authorization_code": data.code = code; data.redirect_uri = redirect_uri; data.force_login = 0; break; case "refresh_token": data.refresh_token = code; break; } this.sendRequest("/oauth2/token", "POST", {}, data, callback); } /** * Checks the client for valid authentication * Generates a new token if needed and possible * @param {Function} cb calllback */ checkAuth(cb) { if (this.options.debug) { console.log(this.options.prefixDebug + ".checkAuth - Checking auth"); } // Check if the client is authorized if (this.oauth.authorized && this.oauth.expires_at > Date.now()) { if (this.options.debug) { console.log( this.options.prefixDebug + ".checkAuth - Client is authorized and token is still valid" ); } // Client is authorized and token is still valid cb(null, true, this.oauth.token); } else if ( (this.oauth.authorized && this.oauth.expires_at < Date.now()) || (!this.oauth.authorized && this.oauth.refresh_token) ) { if (this.options.debug) { console.log( this.options.prefixDebug + ".checkAuth - Either the token has expired, or the client is not authorized but has a refresh token" ); } // Either the token has expired, or the client is not authorized but has a refresh token // With this info a new token can be requested this.refreshToken(function (err, token) { cb(err, true, token); }); } else if (this.oauth.request_code) { if (this.options.debug) { console.log( this.options.prefixDebug + ".checkAuth - No token or refresh token exists, but a request code does" ); } // If no token or refresh token exists, but a request code does, authorize the client this.authorize(this.oauth.request_code, function (err, token) { cb(err, true, token); }); } else { if (this.options.debug) { console.log( this.options.prefixDebug + ".checkAuth - No token, refresh token or request code found, this client is in no way authenticated" ); } // No token, refresh token or request code found, this client is in no way authenticated cb(null, false, null); } } /** * Sends a request to an endpoint * @param {string} endpoint API endpoint * @param {string} method HTTP method * @param {object} params URL params * @param {object} data POST data * @param {Function} callback Callback after request is done */ sendRequest(endpoint, method, params, data, callback) { var requiresAuth = !(endpoint === "/oauth2/token"), self = this, body; // Check arguments if (typeof params === "function") { callback = params; } else if (typeof data === "function") { callback = data; } // if a (POST) data object is passed, stringify it if (!requiresAuth && typeof data === "object") { body = querystring.stringify(data); } else if (requiresAuth && typeof data === "object") { body = JSON.stringify(data); endpoint = encodeURI(endpoint); } // Set headers var headers = { "Cache-Control": "no-cache", Accept: "application/json", }; // If this is a POST, set appropriate headers if (method === "POST" || method === "PUT") { headers["Content-Length"] = body.length; headers["Content-Type"] = "application/json"; // If this is an endpoint that is used to authenticate, set the content type to application/x-www-form-urlencoded if (!requiresAuth) { headers["Content-Type"] = "application/x-www-form-urlencoded"; } } // Wrap the request in a function var doRequest = function () { if (self.options.debug) { console.log(self.options.prefixDebug + ".sendRequest - doRequest"); if (requiresAuth) { console.log(self.options.prefixDebug + ".sendRequest - requiresAuth"); } } // Set request options var options = { host: self.apiUrl, port: 443, method: method, headers: headers, }; // Stringify URL params var paramString = querystring.stringify(params); // Check if the params object exists and is not empty if (typeof params === "object" && paramString !== "") { // If exists and not empty, add them to the endpoint options.path = ["/api" + endpoint, "?", paramString].join(""); } else { options.path = "/api" + endpoint; } // Make the request var req = https.request(options, function (res) { var responseData = ""; // Set the response to utf-8 encoding res.setEncoding("utf-8"); // Add chunk to responseData res.on("data", function (chunk) { responseData += chunk; }); // Request ended, wrap up res.on("end", function () { try { responseData = JSON.parse(responseData); } catch (e) { // Don't parse responseData } callback(null, responseData); }); }); // Handle API errors req.on("error", function (err, data) { callback(err, data); }); // Write request body if (options.method === "POST" || options.method === "PUT") { req.write(body); } // End request req.end(); }; // Check if this request needs authorization if (requiresAuth) { // Authorization is required, check if auth is valid self.checkAuth(function (err, isValid, token) { if (isValid) { // Set authorization header headers["Authorization"] = "Bearer " + token.access_token; // Make the request doRequest(); } else { // Auth is not valid, return a custom error callback( new Error( "No valid authentication found, please set either a token request code, or a valid refresh token" ), null ); } }); } else { // No authorization is required, just make the request doRequest(); } } updateDatabaseTokens(token) { this.oauth.authorized = true; this.oauth.token = token; this.oauth.refresh_token = token.refresh_token; token.expires_at = Date.now() + parseInt(token.expires_in) * 1000; this.oauth.expires_at = token.expires_at; if (!token.error) { db.ExactOnlineClient.update( { access_token: token.access_token, expires_at: token.expires_at, refresh_token: token.refresh_token, }, { where: { name: this.name } } ); } else { db.ExactOnlineClient.update( { access_token: null, expires_at: null, refresh_token: null, }, { where: { name: this.name } } ); } } refreshToken(callback) { var self = this; if (this.options.debug) { console.log( self.options.prefixDebug + '.refresh_token - Refreshing token with refresh_token: "' + self.oauth.refresh_token + '"' ); } this.token( this.oauth.refresh_token, "refresh_token", this.options.redirect_uri, function (err, token) { if (self.options.debug) { console.log( self.options.prefixDebug + ".refresh_token - Retreived token: " + JSON.stringify(token) ); } self.updateDatabaseTokens(token); callback(err, token); } ); } createPromise( endpoint, method, params = null, data = null, divisionRequired = true ) { let self = this; return new Promise((resolve, reject) => { if (divisionRequired && !self.division) { resolve({ success: false, error: "Division required." }); } this.sendRequest( endpoint, method, params, data, function (err, response) { if (err) reject(err); if (response.error) { resolve(_.merge({ success: false }, response.error)); } else { resolve(response.d.results || response.d[0] || response.d); } } ); }); } setDivision(division) { if (this.options.debug) { console.log( this.options.prefixDebug + '.setDivision - Setting division "' + division + '"' ); } this.division = division; } getDivisions() { return this.createPromise( `/v1/${this.division}/system/AllDivisions`, "GET" ); } me() { return this.createPromise( "/v1/current/Me?$top=1", "GET", null, null, false ); } salesInvoices() { return this.createPromise( `/v1/${this.division}/SalesInvoice/SalesInvoices`, "GET" ); } purchaseEntries() { return this.createPromise( `/v1/${this.division}/purchaseentry/PurchaseEntries?$select=*`, "GET" ); } payments() { return this.createPromise( `/v1/${this.division}/Cashflow/Payments?$select=AmountDC,AccountName,Status,TransactionReportingYear&$orderby=TransactionReportingYear desc`, "GET" ); } receivables() { return this.createPromise( `/v1/${this.division}/Cashflow/Receivables?$select=AmountDC,AccountName,Status,TransactionReportingYear&$orderby=TransactionReportingYear desc`, "GET" ); } getGLAccounts() { return this.createPromise( `/v1/${this.division}/financial/GLAccounts?$select=ID,Code,Description`, "GET" ); } postSalesEntry(form) { // Required: Journal (700), SalesEntryLines, Customer return this.createPromise( `/v1/${this.division}/salesentry/SalesEntries`, "POST", null, form ); } postPurchaseEntry(form) { // Required: Journal (600), PurchaseEntryLines, Supplier return this.createPromise( `/v1/${this.division}/purchaseentry/PurchaseEntries`, "POST", null, form ); } getSupplier(form) { // required: VATNumber, Name return this.createPromise( `/v1/${this.division}/crm/Accounts?$top=1&$filter=VATNumber eq '${form.VATNumber}'`, "GET" ); } getCustomer(form) { // required: VATNumber, Name return this.createPromise( `/v1/${this.division}/crm/Accounts?$top=1&$filter=Name eq '${form.Name}'`, "GET" ); } async postSupplier(form) { // required: VATNumber, Name let Supplier = await this.getSupplier(form); if (Supplier.ID) { // get if possible return Supplier; } else { // create if not exists return this.createPromise( `/v1/${this.division}/crm/Accounts`, "POST", null, { IsSupplier: true, ...form } ); } } async postCustomer(form) { // required: Name let Customer = await this.getCustomer(form); if (Customer.ID) { // get if possible return Customer; } else { // create if not exists return this.createPromise( `/v1/${this.division}/crm/Accounts`, "POST", null, { Status: "C", ...form } ); } } } /** * Client constuctor * @param {Object} options Options object * @return {Client} Returns a new instance of the Client object */ module.exports.createClient = async function (name) { let client = new Client(name); await client.init(); return client; };