264 lines
6.8 KiB
JavaScript
264 lines
6.8 KiB
JavaScript
const { logger } = require('./logger.js');
|
|
const API = require('./index.js');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
require('./utils.js');
|
|
|
|
// Initialize key replacements
|
|
let keyReplacements = {};
|
|
|
|
try {
|
|
const replacementsPath = path.join(
|
|
__dirname,
|
|
'..',
|
|
'data',
|
|
'replacements.json'
|
|
);
|
|
if (fs.existsSync(replacementsPath)) {
|
|
const replacementsContent = fs.readFileSync(replacementsPath, 'utf8');
|
|
keyReplacements = JSON.parse(replacementsContent);
|
|
// logger.debug("Replacements loaded successfully");
|
|
} else {
|
|
logger.warn('replacements.json not found, key replacement disabled');
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error loading replacements file:', { error: error.message });
|
|
}
|
|
|
|
// Optimized replaceJsonKeys function
|
|
const replaceJsonKeys = (obj) => {
|
|
if (!obj || typeof obj !== 'object') return obj;
|
|
|
|
// Fast-path for arrays
|
|
if (Array.isArray(obj)) {
|
|
// Only process array if it has items
|
|
return obj.length > 0 ? obj.map(replaceJsonKeys) : obj;
|
|
}
|
|
|
|
const newObj = {};
|
|
const objKeys = Object.keys(obj);
|
|
|
|
// Fast-path for empty objects
|
|
if (objKeys.length === 0) return obj;
|
|
|
|
// Cache key check
|
|
const hasKeyReplacements = Object.keys(keyReplacements).length > 0;
|
|
|
|
for (const key of objKeys) {
|
|
// Replace key if replacements exist
|
|
const newKey =
|
|
hasKeyReplacements && keyReplacements[key] ? keyReplacements[key] : key;
|
|
let value = obj[key];
|
|
|
|
// Replace string values if needed
|
|
if (
|
|
hasKeyReplacements &&
|
|
typeof value === 'string' &&
|
|
keyReplacements[value]
|
|
) {
|
|
value = keyReplacements[value];
|
|
}
|
|
|
|
// Process recursively only if object/array
|
|
newObj[newKey] =
|
|
value && typeof value === 'object' ? replaceJsonKeys(value) : value;
|
|
}
|
|
|
|
return newObj;
|
|
};
|
|
|
|
// Utility regex function
|
|
const sanitizeJsonOutput = (data) => {
|
|
if (!data) return data;
|
|
|
|
// Convert to string to perform regex operations
|
|
const jsonString = JSON.stringify(data);
|
|
|
|
// Define regex pattern that matches HTML entities
|
|
const regexPattern =
|
|
/<span class=".*?">|<\/span>|">/g;
|
|
|
|
// Replace unwanted patterns
|
|
const sanitizedString = jsonString.replace(regexPattern, '');
|
|
|
|
// Parse back to object
|
|
try {
|
|
return JSON.parse(sanitizedString);
|
|
} catch (e) {
|
|
logger.error('Error parsing sanitized JSON:', e);
|
|
return data; // Return original data if parsing fails
|
|
}
|
|
};
|
|
|
|
// Replace the processJsonOutput function with this more efficient version
|
|
const processJsonOutput = (
|
|
data,
|
|
options = { sanitize: true, replaceKeys: true }
|
|
) => {
|
|
// Use a more efficient deep clone approach instead of JSON.parse(JSON.stringify())
|
|
function deepClone(obj) {
|
|
if (obj === null || typeof obj !== 'object') {
|
|
return obj;
|
|
}
|
|
|
|
if (Array.isArray(obj)) {
|
|
return obj.map((item) => deepClone(item));
|
|
}
|
|
|
|
const clone = {};
|
|
Object.keys(obj).forEach((key) => {
|
|
clone[key] = deepClone(obj[key]);
|
|
});
|
|
|
|
return clone;
|
|
}
|
|
|
|
// Create a deep copy of the data to avoid reference issues
|
|
let processedData = deepClone(data);
|
|
|
|
// Apply sanitization if needed
|
|
if (options.sanitize) {
|
|
processedData = sanitizeJsonOutput(processedData);
|
|
}
|
|
|
|
// Apply key replacement if needed
|
|
if (options.replaceKeys) {
|
|
processedData = replaceJsonKeys(processedData);
|
|
}
|
|
|
|
return processedData;
|
|
};
|
|
|
|
// Store active sessions to avoid repeated logins
|
|
const activeSessions = new Map();
|
|
|
|
// Utility function to create a timeout promise
|
|
const timeoutPromise = (ms) => {
|
|
return new Promise((_, reject) => {
|
|
setTimeout(
|
|
() => reject(new ApiTimeoutError(`Request timed out after ${ms}ms`)),
|
|
ms
|
|
);
|
|
});
|
|
};
|
|
|
|
// Helper function to ensure login
|
|
const ensureLogin = async (ssoToken) => {
|
|
if (!activeSessions.has(ssoToken)) {
|
|
logger.info(
|
|
`Attempting to login with SSO token: ${ssoToken.substring(0, 5)}...`
|
|
);
|
|
// logger.info(`Attempting to login with SSO token: ${ssoToken}`);
|
|
const loginResult = await Promise.race([
|
|
API.login(ssoToken),
|
|
timeoutPromise(10000), // 10 second timeout
|
|
]);
|
|
|
|
logger.debug(`Login successful: ${JSON.stringify(loginResult)}`);
|
|
logger.debug(`Session created at: ${global.Utils.toIsoString(new Date())}`);
|
|
activeSessions.set(ssoToken, new Date());
|
|
} else {
|
|
logger.debug('Using existing session');
|
|
}
|
|
};
|
|
|
|
// Create custom error classes
|
|
class ApiTimeoutError extends Error {
|
|
constructor(message = 'API request timed out') {
|
|
super(message);
|
|
this.name = 'ApiTimeoutError';
|
|
}
|
|
}
|
|
|
|
/*
|
|
class ApiAuthError extends Error {
|
|
constructor(message = 'Authentication failed') {
|
|
super(message);
|
|
this.name = 'ApiAuthError';
|
|
}
|
|
}
|
|
|
|
class ApiDataError extends Error {
|
|
constructor(message = 'Data processing error') {
|
|
super(message);
|
|
this.name = 'ApiDataError';
|
|
}
|
|
} */
|
|
|
|
// Helper function to handle API errors
|
|
const handleApiError = (error, res) => {
|
|
logger.error('API Error:', error);
|
|
logger.error(`Error Stack: ${error.stack}`);
|
|
logger.error(`Error Time: ${global.Utils.toIsoString(new Date())}`);
|
|
|
|
// Map error types to appropriate responses
|
|
const errorResponses = {
|
|
ApiTimeoutError: {
|
|
status: 200,
|
|
body: {
|
|
status: 'error',
|
|
message: 'The request timed out. Please try again.',
|
|
error_type: 'timeout',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
},
|
|
},
|
|
ApiAuthError: {
|
|
status: 200,
|
|
body: {
|
|
status: 'error',
|
|
message: 'Authentication failed. Please check your SSO token.',
|
|
error_type: 'auth_failure',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
},
|
|
},
|
|
SyntaxError: {
|
|
status: 200,
|
|
body: {
|
|
status: 'error',
|
|
message:
|
|
'Failed to parse API response. This usually means the SSO token is invalid or expired.',
|
|
error_type: 'InvalidResponseError',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
},
|
|
},
|
|
default: {
|
|
status: 200,
|
|
body: {
|
|
status: 'error',
|
|
message: error.message || 'An unknown error occurred',
|
|
error_type: error.name || 'UnknownError',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
},
|
|
},
|
|
};
|
|
|
|
// Get the appropriate response or use default
|
|
const response = errorResponses[error.name] || errorResponses.default;
|
|
|
|
return res.status(response.status).json(response.body);
|
|
};
|
|
|
|
// Helper function to remove sensitive data from headers
|
|
function sanitizeHeaders(headers) {
|
|
const safeHeaders = { ...headers };
|
|
|
|
// Remove potential sensitive information
|
|
const sensitiveHeaders = ['authorization', 'cookie', 'set-cookie'];
|
|
sensitiveHeaders.forEach((header) => {
|
|
if (safeHeaders[header]) {
|
|
safeHeaders[header] = '[REDACTED]';
|
|
}
|
|
});
|
|
|
|
return safeHeaders;
|
|
}
|
|
|
|
module.exports = {
|
|
activeSessions,
|
|
timeoutPromise,
|
|
ensureLogin,
|
|
handleApiError,
|
|
sanitizeHeaders,
|
|
processJsonOutput,
|
|
};
|