synt_backend
Version:
Synt light-weight node backend service
664 lines (592 loc) • 19.1 kB
JavaScript
var _ = require("lodash"),
querystring = require("querystring"),
https = require("https");
const db = require("./../mysql/models/index");
class Client {
constructor(name) {
var defaults = {
env: "production",
debug: false,
prefixDebug: "[keypoint] ",
};
this.name = name;
// Merge defaults with passed objects
this.options = _.merge({}, defaults);
this.apiUrl = "demo-api.keypoint.be";
this.loginUrl = "demo-login.keypoint.be";
// Define OAuth object
this.oauth = {
expires_at: new Date(),
token: {},
refresh_token: null,
};
return this;
}
init() {
// Get info from database
return db.KeypointClient.findOne({ where: { name: this.name } }).then(
(client) => {
// client options
var options = {
client_id: client.client_id,
client_secret: client.client_secret,
env: process.env.NODE_ENV || "development",
debug: process.env.NODE_ENV ? false : true,
prefixDebug: "[keypoint] ",
};
this.apiUrl =
process.env.NODE_ENV === "production"
? "demo-api.keypoint.be"
: "demo-api.keypoint.be";
this.loginUrl =
process.env.NODE_ENV === "production"
? "demo-login.keypoint.be"
: "demo-login.keypoint.be";
// 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 = {
expires_at: client.expires_at,
token: { access_token: client.access_token },
refresh_token: client.refresh_token,
username: client.username,
password: client.password,
};
this.oauth = _.merge({}, this.oauth, oauth);
return this;
}
);
}
updateDatabaseTokens(token) {
this.oauth.token = token;
this.oauth.refresh_token = token.refresh_token;
// FIXME: Date.now()
token.expires_at = Date.now() + parseInt(token.expires_in) * 1000;
this.oauth.expires_at = token.expires_at;
console.log("Token: " + JSON.stringify(token));
if (!token.error) {
db.KeypointClient.update(
{
access_token: token.access_token,
expires_at: token.expires_at,
refresh_token: token.refresh_token,
},
{ where: { name: this.name } }
);
} else {
db.KeypointClient.update(
{
access_token: null,
expires_at: new Date(),
refresh_token: null,
},
{ where: { name: this.name } }
);
}
}
/**
* Retrieve oauth token from API endpoint
* @param {Function} callback Gets called after request is complete
*/
token(grantType, callback) {
if (this.options.debug) {
console.log(
this.options.prefixDebug + '.token - send token: "' + grantType + '"'
);
}
var data = {
grant_type: grantType,
client_id: this.options.client_id,
client_secret: this.options.client_secret,
};
switch (grantType) {
case "password":
data.username = this.oauth.username;
data.password = this.oauth.password;
break;
case "refresh_token":
data.refresh_token = this.oauth.refresh_token;
break;
}
var self = this;
this.sendRequest("/token", "POST", {}, data, function (err, token) {
if (self.options.debug) {
console.log(
self.options.prefixDebug +
".refresh_token - Retreived token: " +
JSON.stringify(token)
);
}
// strange stuff after refresh
if (token.id_token) {
token.access_token = token.id_token;
}
self.updateDatabaseTokens(token);
callback(err, token);
});
}
/**
* 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.expires_at > Date.now() && this.oauth.token?.access_token) {
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.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.token("refresh_token", function (err, token) {
cb(err, true, token);
});
} else if (this.oauth.username && this.oauth.password) {
if (this.options.debug) {
console.log(
this.options.prefixDebug +
".checkAuth - Either the token has expired, or the client is not authorized but has username / password"
);
}
// 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.token("password", 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 === "/token" || endpoint === "/userinfo"), //https://demo-login.keypoint.be/.well-known/openid-configuration
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);
} else {
body = data || null;
}
// Set headers
var headers = {
"Cache-Control": "no-cache",
Accept: "application/json",
};
// If this is a POST, set appropriate headers
if (method === "POST" || method === "PUT") {
if (body) {
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: requiresAuth ? self.apiUrl : self.loginUrl,
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 = [
(requiresAuth ? "/api/v3" : "/connect") + endpoint,
"?",
paramString,
].join("");
} else {
options.path = (requiresAuth ? "/api/v3" : "/connect") + 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
console.log("no response data to parse");
}
if (
!self.retried &&
(responseData.Message ===
"Authorization has been denied for this request." ||
(typeof responseData === "string" &&
responseData.indexOf("Forbidden. No access") > -1) ||
responseData === "")
) {
// auth failure try again
if (self.options.debug) {
console.log(
self.options.prefixDebug + ".sendRequest - auth fail - retry"
);
}
let token = {
access_token: null,
expires_in: new Date(),
refresh_token: null,
};
self.retried = true;
self.updateDatabaseTokens(token);
self.sendRequest(endpoint, method, params, data, callback);
} else {
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 auth is required
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();
}
}
createPromise(endpoint, method, params, data) {
return new Promise((resolve, reject) => {
this.sendRequest(
endpoint,
method,
params,
data,
function (err, response) {
if (err) return reject(err);
if (response.Message) {
return resolve({
success: false,
Message: response.Message,
ModelState: response.ModelState,
});
} else {
return resolve(response);
}
}
);
});
}
getInvolvedPartyTypes() {
return this.createPromise("/InvolvedPartyType", "GET");
}
getCase(id) {
return this.createPromise("/case/" + id, "GET");
}
getDeclaration(id) {
return this.createPromise("/declaration/" + id, "GET");
}
getFileContent(id) {
return this.createPromise("/file/" + id + "/content", "GET");
}
postVMEActor(VME) {
const Location = {
...VME.Company.toJSON(),
name: "VME " + VME.alias,
};
const Address = {
street: Location.address.split(" ")[0],
number: Location.address.split(" ")[1] || null,
nbr: Location.address.split(" ")[1] || null,
postalCode: Location.address.split(" ")[2],
city: Location.address.split(" ")[3],
countryCode: Location.country_code,
};
const Synt = VME.Users.find((U) => U.UserVME.type === "synt_authoriser"); // There may be no Synt
const data = {
actorType: 3, //VME
kboNbr: VME.Company.vat_number,
establishmentNumber: null,
contactInfo: {
companyName: VME.Company.name,
accountNbr: null,
phone: Synt?.phone,
mobilePhone: Synt?.phone,
email: Synt?.email,
contactTitle: " Synt" + VME.alias,
contactFirstName: Synt?.first_name,
contactLastName: Synt?.last_name,
notes: null,
language: 0,
address: Address,
invoiceAddress: Address,
contactName: VME.alias,
},
fsmaNumber: null,
};
return this.createPromise("/actor", "POST", null, data);
}
postSyntActor(VME) {
const Synt = VME.Users.find((U) => U.UserVME.type === "synt_authoriser");
const data = {
actorType: 4, //Syndicus
kboNbr: null,
establishmentNumber: null,
contactInfo: {
companyName: null,
accountNbr: null,
phone: Synt.phone,
pobilePhone: Synt.phone,
email: Synt.email,
contactTitle: "Synt " + VME.alias,
contactFirstName: Synt?.first_name,
contactLastName: Synt?.last_name,
notes: null,
language: 0,
address: null,
invoiceAddress: null,
contactName: VME.alias,
},
};
return this.createPromise("/Actor", "POST", null, data);
}
async postDeclaration(Incident, VME, Declarer) {
if (!VME.actor_id) {
let Actor = await this.postVMEActor(VME);
VME.actor_id = Actor.id;
VME.save();
}
const Location = {
...VME.Company.toJSON(),
name: "VME " + VME.alias,
};
const Address = {
street: Location.address.split(" ")[0],
number: Location.address.split(" ")[1],
nbr: Location.address.split(" ")[1],
city: Location.address.split(" ")[3],
postalCode: Location.address.split(" ")[2],
countryCode: Location.country_code,
};
const Synt = VME.Users.find((U) => U.UserVME.type === "synt_authoriser");
const data = {
declareTo: 1,
insuranceDomain: null,
insurancePolicyType: null,
policyNumber: null,
policyId: null,
remark: Incident.report_description, //new Date() + ": " + Incident.report_description + " ",
excess: {
excessType: "None",
excessAmount: 0,
excessIncreasedAmount: 0,
remark: "",
},
coverageConfirmed: false,
event: {
insuranceCircumstance: null,
insuranceGuarantee: null,
eventAddress: Address,
damageType: "Other",
damageDate: new Date(),
damageLocation: Location.address,
damageCause: `${Incident.is_urgent ? "URGENT: " : ""}${
Incident.ReportType.name
} (${Incident.ReportType.category}): ${Incident.report_description}`,
policeReportNumber: "",
},
parties: [
{
actorId: VME.actor_id,
reference: "REF" + Incident.id,
actorType: 3, // "Homeowner association",
kboNbr: VME.Company.vat_number, //CHECK
fsmaNbr: null,
establishmentNbr: null,
isClient: true,
comment: null,
language: "NL",
contactInfo: {
title: "Synt VME " + VME.alias,
companyName: VME.Company.name,
contactFirstName: Synt?.first_name || "[unknown]",
contactLastName: Synt?.last_name || "[unknown]",
phone: Synt?.phone || "0485660432",
mobilePhone: Synt?.phone || "0485660432",
emails: [Synt?.email || "help@synt.be"],
address: Address,
},
},
],
involvedParties: [
{
party: {
externalReference: null,
kboNbr: VME.Company.vat_number,
establishmentNbr: null,
isClient: false,
fsmaNbr: null,
comment: null,
address: Address,
language: "NL",
contactInfo: {
companyName: VME.alias,
title: "Declarant VME " + VME.alias,
contactFirstName: Declarer.first_name || "[unknown]",
contactLastName: Declarer.last_name || "[unknown]",
phone: Declarer.phone || "0485660432",
mobilePhone: Declarer.phone || "0485660432",
emails: [Declarer.email || "help@synt.be"],
},
},
involvedType: null,
involvementType: null,
},
],
};
/*
Declaration => id
Webhook => Case is aangemaakt
Case => id => caseid === issueid (ClientLinkUrl)
CaseType => array (most recent by TimeStamp)
VIA REF INTERN vme definieren
Email versturen naar de klant is mogelijk => Maar beter via WebHook en notification // Declaration contact (IsTrue)
Initieel factuur via email (client)
Beter via WebHook om File op te halen - Programmeerwerk voor Tijs
*/
return this.createPromise("/case", "POST", null, data);
}
/*
postDeclarationRemark(declaration_id, remark) {
return this.createPromise(
"/declaration/" + declaration_id + "/remark",
"POST",
remark,
remark
);
}
*/
getBase64(url) {
return new Promise((resolve, reject) => {
var request = require("request").defaults({ encoding: null });
request.get(url, function (error, response, body) {
if (!error && response.statusCode == 200) {
let data = Buffer.from(body).toString("base64");
resolve(data);
}
reject(error);
});
});
}
postCaseFile(case_id, File) {
// FIXME: presignedUrl (get then)
return this.getBase64(File.presignedUrl)
.then((data) => {
const file = {
fileGroupId: 8, // FOTO
fileNameWithExtention: File.original_name,
content: data,
};
return this.createPromise(
"/case/" + case_id + "/file",
"POST",
null,
file
);
})
.catch((err) => console.log(err));
}
postWebhook(url, triggers) {
return this.createPromise("/webhook/register/", "POST", null, {
url,
triggers,
});
}
}
/**
* 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;
};