This script will run a simple CORS server for Fortnox API: https://developer.fortnox.se/documentation/
It will also refresh the API token every 30 minutes and will use it automatically allowing us access to Fortnox API. We do not need to provide Fortnox API token.
There should be a token file with client ID & secret with same name as this script file ending in .json
It will automatically regenerate token for next requests.
It uses security using Authorization URL query parameter.
Replace PASTE_YOUR_KEY_HERE
with your own key and pass it with all requests that you make.
(async function() {
var localMode = typeof addEventListener === "undefined" && typeof input === "undefined" ? true : false;
if (localMode === true) {
console.log(`
Global base packages required:
form-data
node-fetch@2.6.7
`);
var workerThreads = require('worker_threads');
var fs = require("fs");
var https = require("https");
var http = require("http");
var exec = require("child_process").exec;
exec("export NODE_PATH=$(npm root --quiet -g)");
var FormData = require("form-data");
var fetch = require("node-fetch");
var appConfig = {
"mainFile": "main.js",
"port": 7051,
"noCertificate": true,
"bundle": false
};
(function() {
var parts = __filename.split("/");
var fileName = parts.pop();
var file = parts.join("/") + "/.bundle." + fileName + "on";
if (fs.existsSync(file)) {
var options = JSON.parse((function() {
var contents = fs.readFileSync(file, "utf8");
contents = contents.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '').trim();
return contents;
})());
for (var key in options) appConfig[key] = options[key];
}
})();
if (typeof appConfig.reverseProxy !== "undefined" && typeof appConfig.reverseProxy.domains !== "undefined") {
(function() {
var redbird = require('redbird')({
port: 80,
letsencrypt: {
path: __dirname + '/certs'
},
ssl: {
port: 443,
key: "certs/key.pem",
cert: "certs/cert.pem"
}
});
for (var domain in appConfig.reverseProxy.domains) {
redbird.register(domain, appConfig.reverseProxy.domains[domain]);
}
})();
}
if (typeof appConfig.port === "number") {
if (typeof appConfig.host !== "string") appConfig.host = "127.0.0.1";
var httpOrHttps = appConfig.noCertificate === true ? http : https;
if (workerThreads.isMainThread) {
var server = httpOrHttps.createServer({
key: `-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAr68IoGyAC1HCh6N8djdeFYewWKbqywedHCVOpF/bFGP47KJP
W26Yng81OgK/4RkXp07My1B16jeFN5NXqmfdtQENTckxtmJA2j6/4d2y0m9aacLl
dx1a4mNzbM78Ln9smEk0+SWp6fypN0kg9OER3ljQpCb8YspgwHbnJLlky2c6q84S
cC8Oc2hhPXo/CJit2xoI3m8SAaqfDsHAmyqd2bsBBaH9ssgpaIxGQ2QWFfHZJOSE
D5AUEO+HuJQverSNzxAsLItuKbUfvGvm/tUvXymlStmDyWIwNvfdneuozZ5WPIEP
d43s++QKmI5qlA1Z+j+wcY3zFyqUM1NcWBUhrQIDAQABAoIBACYBtdoO3vyT6YOy
iKCCheYefrYPFkhqE0EdiQ/idODKZ/W5f3WGTZoULC2qnpwx834MfB2YAIp1DGrj
g1moMryPx7MGTazpQKJ2ZMiWT7Nax7KNqVrFjP3hCf2GIeRlSLcBT2Z/EW0/bdQ6
C9kuP9FcYXbBwGQW6Ct7DbJSMU4XYUErl4sXb7H9vwARlWxJ4Gq8fldagy7T+oX2
OwObO9/e8GdkJBlr5PM2ZZOES3avGjJnPGFwQ/wCreNaqt8ir+fFmd6odLjxh2mH
sn7WwIC/Eyp/TcM/O7bZT3SfSCWbKsk2NGQCX3cV49Zr37+4slujtAnK6sJI2Jr6
riPPvEECgYEA2DgXHphkZwTGuhMMCnaDuKbv3y7iJ15oggTo8CAMfCb7sE1FEPOs
0gnYG7p8xa1HPNGx5PkQCpIIz8pMC9+0Dqxu7bX5t0D0JQDDB+8UhtGbEgXPrAF1
NHpWHil6Or/KmfF5kXe15gKkkohVFrJw0HyMnBAvSbzd/L2mwa1xOQkCgYEA0AG6
n4Im1aFOnOnHaXD6w4CP1ehysHkFoxad+EeUu9d1dfIrPlUCTwtL0tHRDR0uehA4
PaUPThFllTCuqGTXawxuAdNxsa8KttvjTPEI29dqB/4IgIYTNXqZnfrgNWC3mNLw
InyrlR8RsrRAS1R6AyWxWh9Nm0iKAKhsxgdOgIUCgYB4mUB79i/6LfXSD5GlvFjY
A3TDnVjS8JuF+csbNCUCkpPL0C13uRJpzMfXH3s8ntufFq8Msca6vp1fmMw1yz6Y
+KCeweNYzUff477ki/t8/yhpMwiUPfPro1ipViUw44zTtJZEButUMaEtghFDqZ+3
CeE7ouNdU5TVxcpfOKhwUQKBgApmm7tQGbsC5thnxCXclV1jN0394oY6dvKxtdJt
Wd2Op3vvUQQ74fKr4O24uhhKxkEqQHWspDhGHGs6VPFsoWzj4ThMJ1o4I3QDSLlX
MBc2DUI7DJfInHtHFxlUKxPgMy38Fi/TRg0d0Ze69aAOqE8x+k1EVXAXT3c69L1u
Lhm1AoGAcOJy7MewUUegFUcbg+hYTd8zXZFX5Tzl5eiExqyWxBnp2/Nuasw4950X
Xr0lSnFx2cXrqiLjgYxFQdCwaJmL0uAuYjALzKvsQaCpbNJ+kMQ28mlDp3lYHmXp
s8HGa0SNzjRLQWRMBw0dx7IAAsuyFmv/6e6jvg2ksxrUQ8ViidE=
-----END RSA PRIVATE KEY-----
`,
cert: `-----BEGIN CERTIFICATE-----
MIICmjCCAYICCQCiHtY6daqa3zANBgkqhkiG9w0BAQUFADAPMQ0wCwYDVQQKDARj
ZXJ0MB4XDTIxMDQyNjE0NDQ0MFoXDTQ4MDkxMDE0NDQ0MFowDzENMAsGA1UECgwE
Y2VydDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK+vCKBsgAtRwoej
fHY3XhWHsFim6ssHnRwlTqRf2xRj+OyiT1tumJ4PNToCv+EZF6dOzMtQdeo3hTeT
V6pn3bUBDU3JMbZiQNo+v+HdstJvWmnC5XcdWuJjc2zO/C5/bJhJNPklqen8qTdJ
IPThEd5Y0KQm/GLKYMB25yS5ZMtnOqvOEnAvDnNoYT16PwiYrdsaCN5vEgGqnw7B
wJsqndm7AQWh/bLIKWiMRkNkFhXx2STkhA+QFBDvh7iUL3q0jc8QLCyLbim1H7xr
5v7VL18ppUrZg8liMDb33Z3rqM2eVjyBD3eN7PvkCpiOapQNWfo/sHGN8xcqlDNT
XFgVIa0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAM6yKAW3NJi4unaxH5kUaqAwb
HKfeZANu8UP1n6GwpD4n/unC2EGRo5gkUVKZonErPEIv6wL6KDX1CKFT46AtSnMi
QIpNriYPdSsBsADvnwRDTlCiv667Jzjb0jnI38aBZ7vw0z+QHbplfy4Ss6Q3NkEN
HGXibAidrQrj/zptLxAsp+MYfg380uQhJHIcXbqynCozKtORFL3FcR+SDb6J3h1W
0atSlKPkAsRDTCSMQT700d3w5UaQXyx+Ac4xEuEqvZXJLKAOETBfcgsaIIVyc6H2
1khfWxkMvEEbkHQfSX8ps1G5An3jUdq2nofwgphxBCEriG1neVfbTP3ZS6mE4A==
-----END CERTIFICATE-----
`
}).listen(appConfig.port, appConfig.host !== "127.0.0.1" ? appConfig.host : undefined);
server.on("request", async function(req, res) {
req.text = async function() {
return new Promise(function(resolve, reject) {
var text = "";
req.on("data", function(chunk) {
text += chunk.toString();
});
req.on("end", function(chunk) {
resolve(text);
});
});
}
req.arrayBuffer = async function() {
return new Promise(function(resolve, reject) {
var chunkedData = Buffer.from("");
req.on("data", function(chunk) {
chunkedData = Buffer.concat([chunkedData, chunk]);
});
req.on("end", function(chunk) {
resolve(chunkedData);
});
});
}
req.headers.get = function(match) {
for (var key in req.headers) {
if (key.toLowerCase() === match.toLowerCase()) return req.headers[key];
}
};
req.url = (appConfig.noCertificate === true ? "http" : "https") + "://" + appConfig.host + ":" + appConfig.port + req.url;
console.log(req.url);
var result = await addEventListener("fetch", {
request: req
});
if (
app.has(result) === true &&
app.has(result.data) === true &&
app.has(result.options) === true &&
app.has(result.options.status) === true &&
app.has(result.options.headers) === true
) {
for (var key in result.options.headers) res.setHeader(key, result.options.headers[key]);
res.statusCode = result.options.status;
if (typeof result.data === "object" && app.has(result.data.byteLength) === true) {
res.end(Buffer.from(result.data, "binary"));
} else {
res.end(result.data);
}
} else {
if (app.has(app.utils) && app.has(app.utils) && app.has(app.utils.frontend) && app.has(app.utils.frontend.static)) {
app.utils.frontend.static(req, res);
} else {
res.statusCode = 400;
res.end("Could not process in local mode.");
}
}
});
console.log("Server is listening on " + appConfig.host + ":" + appConfig.port);
addEventListener = async function(action, event) {
return await app.handleRequest(event.request);
};
}
}
}
var app = {
get fileName() {
return __filename.split("/").pop().split(".").shift();
},
start: function() {
},
indexDynamic: async function(request) {
if (app.has(app.cron.refresh.tokenData)) {
var url = new URL(request.url);
url.host = "api.fortnox.se";
url.port = 443;
url.protocol = "https";
url.searchParams.delete("authorization");
console.log(request.method + ": " + url.toString());
var headers = app.utils.header.parse(request.headers);
headers.host = url.host;
headers["Authorization"] = "Bearer " + app.cron.refresh.tokenData.access_token;
var requestOptions = {
method: request.method,
headers: headers
};
if (request.method.toLowerCase() === "post" || request.method.toLowerCase() === "put") requestOptions.body = await request.text();
var result = await fetch(url.toString(), requestOptions);
var headers = {};
for (var key in result.headers) {
if (typeof request.headers[key] === "string") headers[key] = request.headers[key];
}
return new app.response(await result.text(), {
status: result.status,
headers: app.utils.header.parse(result.headers)
});
} else {
return new app.response("No tokenData.", {
status: 400
});
}
}
};
app.startUps = [];
app.workerStartUps = [];
app.callbacks = {
static: []
};
app["build"] = {};
app["config"] = {
"api": {
authorization: "PASTE_YOUR_KEY_HERE",
log: {
url: true
}
},
};
app["cron"] = {
"refresh": (function() {
var fs = require("fs");
var mod = {
interval: 30 * 60,
fetch: async function() {
var file = __dirname + "/" + app.fileName + ".json";
var tokenData = {};
if (fs.existsSync(file) === true) {
tokenData = JSON.parse(fs.readFileSync(file, "utf8"));
}
if (app.has(tokenData) && app.has(tokenData.refresh_token) && app.has(tokenData.client_id) && app.has(tokenData.client_secret)) {
var result = await fetch("https://apps.fortnox.se/oauth-v1/token", {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded",
"Authorization": "Basic " + Buffer.from(tokenData.client_id + ":" + tokenData.client_secret).toString("base64")
},
body: "grant_type=refresh_token&refresh_token=" + tokenData.refresh_token
});
if (result.status >= 200 && result.status <= 201) {
var newToken = await result.json();
for (var key in newToken) tokenData[key] = newToken[key];
fs.writeFileSync(file, JSON.stringify(tokenData), "utf8");
console.log("Token refreshed: " + newToken.refresh_token);
mod.tokenData = tokenData;
} else {
console.log(result.status, await result.text());
}
} else {
console.log(app.consoleColors.fgRed, "No refresh token found with client ID and client secret.");
}
setTimeout(mod.fetch, 1000 * mod.interval);
}
};
app.startUps.push(mod.fetch);
return mod;
})(),
};
app["publish"] = {};
app["utils"] = {
"header": (function() {
var mod = {
parse: function(headers) {
var list = {};
for (var key in headers) {
if (typeof headers[key] === "string") list[key] = headers[key];
}
return list;
}
};
return mod;
})(),
};
var config = app.config;
var modules = app.modules;
app.has = function(value) {
var found = true;
for (var i = 0; i <= arguments.length - 1; i++) {
var value = arguments[i];
if (!(typeof value !== "undefined" && value !== null && value !== "")) found = false;
}
return found;
};
(function(left, right) {
var copyConfig = function(left, right) {
if (app.has(left) && app.has(right)) {
for (var key in left) {
var value = left[key];
if (typeof value !== "object") {
right[key] = value;
} else {
copyConfig(value, right[key]);
}
}
}
};
copyConfig(left, right);
})(appConfig.config, config);
app.response = function(data, options) {
if (typeof options === "undefined") options = {};
if (typeof options.headers === "undefined") options.headers = {};
if (!app.has(options.headers["Access-Control-Allow-Origin"])) options.headers["Access-Control-Allow-Origin"] = "*";
if (!app.has(options.headers["Access-Control-Allow-Headers"])) options.headers["Access-Control-Allow-Headers"] = "content-type, content";
if (!app.has(options.headers["Access-Control-Allow-Methods"])) options.headers["Access-Control-Allow-Methods"] = "HEAD, GET, PUT, DELETE, POST, OPTIONS";
if (
typeof Response !== "undefined" &&
!(
typeof __dirname !== "undefined" &&
__dirname.toLowerCase().split("/users/").length > 1
) &&
app.has(appConfig.workerName)
) {
return new Response(data, options);
} else {
return {
data: data,
options: options
};
}
};
app.logAndResponse = function(data, options) {
console.log(data);
return app.response(data, options);
},
app.camelCase = function camelize(str, capitalFirst) {
if (!app.has(capitalFirst)) capitalFirst = false;
var result = str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(word, index) {
return index === 0 ? word.toLowerCase() : word.toUpperCase();
}).replace(/\s+/g, '');
if (capitalFirst) result = result.substr(0, 1).toUpperCase() + result.substr(1, 999);
return result;
};
app.properCase = function(str) {
return str.replace(
/\w\S*/g,
function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
}
);
};
if (app.has(app.api) === true) { // callbacks
(function() {
var callbackLevel = function(apiLevel) {
if (app.has(apiLevel) && !app.has(apiLevel.length)) {
for (var moduleName in apiLevel) {
if (app.has(apiLevel[moduleName]) === true) {
callbackLevel(apiLevel[moduleName]);
for (var key in apiLevel[moduleName]) {
(function(moduleName, key) {
var func = apiLevel[moduleName][key];
if (key.split("Callback").length > 1 && typeof func === "function") {
apiLevel[moduleName][key.split("Callback").shift() + "Multi"] = async function(count, name, callback, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15) {
return new Promise(function(resolve, reject) {
if (!app.has(count)) count = 1;
var rCount = 0;
var resolveCount;
for (var i = 0; i <= count - 1; i++) {
(async function(index) {
var countResult = await apiLevel[moduleName][key.split("Callback").shift()](name, async function(arg1, arg2, arg3, arg4, arg5) {
if (typeof callback === "function") {
var result = await callback(arg1, arg2, arg3, arg4, arg5);
if (result === true && !app.has(resolveCount)) {
console.log("MULTI INDEX:", index);
resolveCount = index;
}
return result;
}
}, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15);
rCount += 1;
if (resolveCount === index || rCount >= count) resolve(countResult);
})(i);
}
});
};
apiLevel[moduleName][key.split("Callback").shift()] = async function(name, callback, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15) {
if (typeof callback !== "function") {
arg15 = arg13;
arg14 = arg12;
arg13 = arg11;
arg12 = arg10;
arg11 = arg9;
arg10 = arg8;
arg9 = arg7;
arg8 = arg6;
arg7 = arg5;
arg6 = arg4;
arg5 = arg3;
arg4 = arg2;
arg3 = arg1;
arg2 = callback;
arg1 = name;
}
var output, error;
await apiLevel[moduleName][key](async function(data, page) {
var result = typeof callback === "function" ? await callback(data, page) : undefined;
if (app.has(result) && app.has(result.length)) {
if (!app.has(output)) output = [];
output = output.concat(result);
} else {
output = data;
}
return result;
},
function(err, errorText) {
error = {
error: err,
errorText
};
}, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15);
var obj = {};
if (typeof callback !== "function") return output;
obj[name] = output;
obj.error = error;
return obj;
};
}
})(moduleName, key);
}
}
}
}
};
callbackLevel(app.api);
})();
}
app.handleRequest = async function(request) {
if (typeof request === "undefined") return false;
var url = new URL(request.url);
if (request.method === "OPTIONS") return app.response("Ok", {
status: 200
});
if (app.has(config) && app.has(config.api) && app.has(config.api.authorization)) {
var authorization = app.has(request.headers) ? (app.has(request.headers.get) ? request.headers.get("authorization") : request.headers.authorization) : "";
if (!app.has(authorization)) authorization = url.searchParams.get("authorization");
if (typeof config.api.authorization === "string") {
if (authorization !== config.api.authorization) return app.response("Unauthorized.", {
status: 400
});
} else {
if (typeof config.api.authorization === "object" && config.api.authorization !== null) {
if (app.has(config.api.authorization.length)) {
if (config.api.authorization.indexOf(authorization) < 0) return app.response("Unauthorized.", {
status: 400
});
} else {
if (!app.has(config.api.authorization[authorization])) return app.response("Unauthorized.", {
status: 400
});
request.condition = config.api.authorization[authorization];
}
}
}
}
var parts = url.pathname.split("/");
parts.shift();
if (parts.length === 1 && parts[0].trim() === "") parts[0] = "index";
if (app.indexMode === true) parts = ["index"];
if (parts.length > 0 && parts[0].trim() !== "") {
var level = app;
for (var i = 0; i <= parts.length - 1; i++) {
var part = app.camelCase(parts[i].trim().split("-").join(" "));
if (typeof level[part] !== "undefined" || typeof level["indexDynamic"] !== "undefined") {
level = !(level === app && typeof level["indexDynamic"] !== "undefined") ? level[part] : level;
if (i === parts.length - 1) {
if (typeof level === "object" && level !== null && typeof level["index"] === "function") {
var caller = {};
caller[part] = async function() {
return await level["index"](request);
}
if (!app.has(request.headers.get("start-only"))) {
return await caller[part]();
} else {
caller[part]();
return app.response("Started.", {
status: 200
});
}
}
if (typeof level === "function") {
var caller = {};
caller[part] = async function() {
return await level(request);
}
if (!app.has(request.headers.get("start-only"))) {
return await caller[part]();
} else {
caller[part]();
return app.response("Started.", {
status: 200
});
}
}
}
if (typeof level === "object" && level !== null && typeof level["indexDynamic"] === "function") {
var caller = {};
caller[part] = async function() {
return await level["indexDynamic"](request);
}
if (!app.has(request.headers.get("start-only"))) {
return await caller[part]();
} else {
caller[part]();
return app.response("Started.", {
status: 200
});
}
}
}
}
}
if (typeof localMode === "undefined" || localMode !== true) return app.response("Unknown request: " + url.pathname, {
status: 400
});
};
if (typeof localMode === "undefined" && typeof addEventListener !== "undefined") {
addEventListener('fetch', event => {
event.respondWith(app.handleRequest(event.request));
});
}
app.consoleColors = {
reset: "\x1b[0m%s\x1b[0m",
bright: "\x1b[1m%s\x1b[0m",
dim: "\x1b[2m%s\x1b[0m",
underscore: "\x1b[4m%s\x1b[0m",
blink: "\x1b[5m%s\x1b[0m",
reverse: "\x1b[7m%s\x1b[0m",
hidden: "\x1b[8m%s\x1b[0m",
fgBlack: "\x1b[30m%s\x1b[0m",
fgRed: "\x1b[31m%s\x1b[0m",
fgGreen: "\x1b[32m%s\x1b[0m",
fgYellow: "\x1b[33m%s\x1b[0m",
fgBlue: "\x1b[34m%s\x1b[0m",
fgMagenta: "\x1b[35m%s\x1b[0m",
fgCyan: "\x1b[36m%s\x1b[0m",
fgWhite: "\x1b[37m%s\x1b[0m",
fgGray: "\x1b[90m%s\x1b[0m",
bgBlack: "\x1b[40m%s\x1b[0m",
bgRed: "\x1b[41m%s\x1b[0m",
bgGreen: "\x1b[42m%s\x1b[0m",
bgYellow: "\x1b[43m%s\x1b[0m",
bgBlue: "\x1b[44m%s\x1b[0m",
bgMagenta: "\x1b[45m%s\x1b[0m",
bgCyan: "\x1b[46m%s\x1b[0m",
bgWhite: "\x1b[47m%s\x1b[0m",
bgGray: "\x1b[100m%s\x1b[0m"
};
if (typeof app.start === "function") {
app.start();
}
if ((typeof localMode !== "undefined" && workerThreads.isMainThread) || typeof localMode === "undefined") {
for (var i = 0; i <= app.startUps.length - 1; i++) {
if (typeof app.startUps[i] === "function") {
app.startUps[i]();
}
}
for (var i = 0; i <= app.startUps.length - 1; i++) {
if (!(typeof app.startUps[i] === "function")) {
if (
typeof localMode !== "undefined" ||
(typeof appConfig !== "undefined" && app.has(appConfig.frontend) && appConfig.frontend)
) setTimeout(app.startUps[i].callback, app.startUps[i].time);
}
}
}
if (typeof workerThreads !== "undefined" && !workerThreads.isMainThread) {
for (var i = 0; i <= app.workerStartUps.length - 1; i++) {
if (typeof app.workerStartUps[i] === "function") {
app.workerStartUps[i]();
}
}
}
})();