synt_backend
Version:
Synt light-weight node backend service
583 lines (513 loc) • 15.1 kB
JavaScript
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;
};