@digitalnodecom/node-red-contrib-netsuiteauth
Version:
A custom Node-RED node for NetSuite OAuth 1.0
193 lines (159 loc) • 7.11 kB
JavaScript
const crypto = require('crypto');
const axios = require('axios');
const url = require('url'); // Add URL module for better URL parsing
function generateNonce() {
return crypto.randomBytes(16).toString('hex');
}
function generateBaseString(method, url, oauthParams, queryParams = {}) {
const allParams = {
...oauthParams,
...queryParams
};
const sortedParams = Object.keys(allParams)
.sort()
.reduce((acc, key) => {
acc[key] = allParams[key].toString();
return acc;
}, {});
const paramString = Object.entries(sortedParams)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
// Extract base URL without query parameters
const baseUrl = url.split('?')[0];
return [
method.toUpperCase(),
encodeURIComponent(baseUrl),
encodeURIComponent(paramString)
].join('&');
}
function generateAuthHeader(node, method, fullUrl, queryParams = {}) {
const realm = node.oauth.realm;
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = generateNonce();
// Parse the URL to separate base URL and existing query parameters
const parsedUrl = new URL(fullUrl);
const baseUrl = parsedUrl.origin + parsedUrl.pathname;
// Combine existing URL query parameters with passed queryParams
const combinedParams = {};
// First get any params already in the URL
for (const [key, value] of parsedUrl.searchParams) {
combinedParams[key] = value;
}
// Then add any additional query params
if (queryParams instanceof URLSearchParams) {
for (const [key, value] of queryParams) {
combinedParams[key] = value;
}
} else {
Object.assign(combinedParams, queryParams);
}
let oauthParams = {
oauth_consumer_key: node.oauth.consumerKey,
oauth_nonce: nonce,
oauth_signature_method: 'HMAC-SHA256',
oauth_timestamp: timestamp,
oauth_token: node.oauth.token,
oauth_version: '1.0'
};
const baseString = generateBaseString(method, baseUrl, oauthParams, combinedParams);
const signingKey = `${encodeURIComponent(node.oauth.consumerSecret)}&${encodeURIComponent(node.oauth.tokenSecret)}`;
const signature = crypto.createHmac('sha256', signingKey).update(baseString).digest('base64');
// Construct OAuth Header
let authHeader = 'OAuth ' + [
`realm="${encodeURIComponent(realm)}"`,
`oauth_consumer_key="${encodeURIComponent(node.oauth.consumerKey)}"`,
`oauth_token="${encodeURIComponent(node.oauth.token)}"`,
`oauth_signature_method="HMAC-SHA256"`,
`oauth_timestamp="${encodeURIComponent(timestamp)}"`,
`oauth_nonce="${encodeURIComponent(nonce)}"`,
`oauth_version="1.0"`,
`oauth_signature="${encodeURIComponent(signature)}"`
].join(', ');
return authHeader;
}
async function executeRequest(node, config, msg) {
try {
// Get URL and method from config or msg
let requestUrl = config.url || msg.url;
const method = config.method || msg.method || 'GET';
if (!requestUrl) {
throw new Error("The URL is missing. Please provide a valid URL.");
}
if (!node.oauth || !node.oauth.consumerKey || !node.oauth.consumerSecret || !node.oauth.token || !node.oauth.tokenSecret) {
throw new Error("OAuth credentials are missing or incomplete.");
}
// Check if this is a pagination URL from a previous response
// If so, use it directly without modifying
const isPaginationUrl = msg.isPaginationUrl === true;
if (!isPaginationUrl) {
// Only append these parameters if not using a pagination URL
const netsuiteobject = config.netsuiteobject || msg.netsuiteobject;
const objectid = config.objectid || msg.objectid;
const objectexternalid = config.objectexternalid || msg.objectexternalid;
// Build the final URL with resource path components
if (netsuiteobject) {
requestUrl += `/${netsuiteobject}`;
}
if (objectid) {
requestUrl += `/${objectid}`;
}
if (objectexternalid) {
requestUrl += `/eid:${objectexternalid}`;
}
}
// Parse the URL to extract existing query params
const parsedUrl = new URL(requestUrl);
const existingParams = parsedUrl.searchParams;
// Only add limit/offset if not using pagination URL
if (!isPaginationUrl) {
const limit = config.limit || msg.limit;
const offset = config.offset || msg.offset;
if (limit && !existingParams.has('limit')) existingParams.set('limit', limit);
if (offset && !existingParams.has('offset')) existingParams.set('offset', offset);
}
// Generate the OAuth header with the URL's base part and all combined params
const authHeader = generateAuthHeader(node, method, requestUrl, existingParams);
// Prepare the final request options
const bodyNetsuite = config.bodyNetsuite || msg.bodyNetsuite;
const requestOptions = {
method: method.toUpperCase(),
url: requestUrl,
headers: {
'Authorization': authHeader,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
data: bodyNetsuite ? JSON.parse(bodyNetsuite) : null,
};
node.status({ fill: "blue", shape: "dot", text: "requesting..." });
const response = await axios(requestOptions);
msg.payload = {
headers: response.headers,
statusCode: response.status,
data: response.data,
};
node.status({ fill: "green", shape: "dot", text: "success" });
node.send(msg);
} catch (error) {
node.status({ fill: "red", shape: "ring", text: error.message });
node.error("NetSuite Error: " + error.message, msg);
if (error.response && error.response.data && error.response.data["o:errorDetails"]) {
let detailMessage = error.response.data["o:errorDetails"][0]?.detail || "No detail available";
msg.error = detailMessage;
node.error("NetSuite Error Detail: " + detailMessage, msg);
} else {
msg.error = error.message;
}
node.send(msg);
}
}
module.exports = function (RED) {
function NetsuiteRestApiRequest(config) {
RED.nodes.createNode(this, config);
const node = this;
this.oauth = RED.nodes.getNode(config.oauth);
node.on('input', async function (msg) {
await executeRequest(node, config, msg);
});
}
RED.nodes.registerType('netsuite-rest-api-request', NetsuiteRestApiRequest)};