codtracker-js/src/js/serverUtils.js

283 lines
7.2 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, replacements) => {
// Handle non-objects early
if (!obj || typeof obj !== 'object') return obj;
// Fast path for arrays
if (Array.isArray(obj)) {
return obj.length === 0 ?
obj
: obj.map((item) => replaceJsonKeys(item, replacements));
}
// Fast path for empty objects
const keys = Object.keys(obj);
if (keys.length === 0) return obj;
// Process normal objects
const newObj = {};
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const newKey = replacements[key] || key;
let value = obj[key];
// Replace string values if they match a key in replacements
if (typeof value === 'string' && replacements[value]) {
value = replacements[value];
} else if (value && typeof value === 'object') {
// Only recurse for objects/arrays
value = replaceJsonKeys(value, replacements);
}
newObj[newKey] = 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 =
/&lt;span class=&quot;.*?&quot;&gt;|&lt;\/span&gt;|&quot;&gt;/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,
keyReplacements: {},
}
) => {
// Early return for null or non-objects
if (data === null || typeof data !== 'object') {
return data;
}
// Copy data first to avoid reference issues
let processedData = deepClone(data);
// Apply sanitization if needed
if (options.sanitize) {
processedData = sanitizeJsonOutput(processedData);
}
// Apply key replacement if needed - pass replacements directly
if (
options.replaceKeys &&
Object.keys(options.keyReplacements || {}).length > 0
) {
processedData = replaceJsonKeys(processedData, options.keyReplacements);
}
return processedData;
};
/**
* Optimized deep clone function
* @param {any} obj - The object to clone
* @returns {any} - The cloned object
*/
const deepClone = (obj) => {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// Fast path for arrays
if (Array.isArray(obj)) {
const length = obj.length;
const clone = new Array(length);
for (let i = 0; i < length; i++) {
clone[i] = deepClone(obj[i]);
}
return clone;
}
// Fast path for objects
const clone = {};
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
clone[key] = deepClone(obj[key]);
}
return clone;
};
// 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,
};