chore: separate utility code

This commit is contained in:
Rim 2025-04-16 16:16:32 -04:00
parent a5771c3499
commit b6921dcbde
2 changed files with 212 additions and 190 deletions

198
app.js
View File

@ -9,6 +9,14 @@ const favicon = require('serve-favicon');
const fs = require('fs'); const fs = require('fs');
const app = express(); const app = express();
const port = process.env.PORT || 3512; const port = process.env.PORT || 3512;
const {
processJsonOutput,
sanitizeHeaders,
timeoutPromise,
ensureLogin,
handleApiError,
activeSessions
} = require('./src/js/serverUtils.js');
require('./src/js/utils.js'); require('./src/js/utils.js');
app.set('trust proxy', true); app.set('trust proxy', true);
@ -29,196 +37,6 @@ app.use(
); );
app.use(demoTracker.demoModeMiddleware); app.use(demoTracker.demoModeMiddleware);
// Initialize key replacements
let keyReplacements = {};
try {
const replacementsPath = path.join(
__dirname,
'src',
'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 });
}
const replaceJsonKeys = (obj) => {
if (!obj || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) {
return obj.map((item) => replaceJsonKeys(item));
}
const newObj = {};
Object.keys(obj).forEach((key) => {
// Replace key if it exists in replacements
const newKey = keyReplacements[key] || key;
// DEBUG: Log replacements when they happen
// if (newKey !== key) {
// logger.debug(`Replacing key "${key}" with "${newKey}"`);
// }
// Also check if the value should be replaced (if it's a string)
let value = obj[key];
if (typeof value === 'string' && keyReplacements[value]) {
value = keyReplacements[value];
// logger.debug(`Replacing value "${obj[key]}" with "${value}"`);
}
// Process value recursively if it's an object or array
newObj[newKey] = replaceJsonKeys(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 Error(`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');
}
};
// 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())}`);
// Try to extract more useful information from the error
let errorMessage = error.message || 'Unknown API error';
let errorName = error.name || 'ApiError';
// Handle the specific JSON parsing error
if (errorName === 'SyntaxError' && errorMessage.includes('JSON')) {
logger.debug('JSON parsing error detected');
return res.status(200).json({
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()),
});
}
// Send a more graceful response
return res.status(200).json({
status: 'error',
message: errorMessage,
error_type: errorName,
timestamp: global.Utils.toIsoString(new Date()),
});
};
// 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;
}
// Set up demo mode cleanup interval // Set up demo mode cleanup interval
setInterval( setInterval(
() => demoTracker.demoModeRequestTracker.cleanupExpiredEntries(), () => demoTracker.demoModeRequestTracker.cleanupExpiredEntries(),

204
src/js/serverUtils.js Normal file
View File

@ -0,0 +1,204 @@
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 });
}
const replaceJsonKeys = (obj) => {
if (!obj || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) {
return obj.map((item) => replaceJsonKeys(item));
}
const newObj = {};
Object.keys(obj).forEach((key) => {
// Replace key if it exists in replacements
const newKey = keyReplacements[key] || key;
// DEBUG: Log replacements when they happen
// if (newKey !== key) {
// logger.debug(`Replacing key "${key}" with "${newKey}"`);
// }
// Also check if the value should be replaced (if it's a string)
let value = obj[key];
if (typeof value === 'string' && keyReplacements[value]) {
value = keyReplacements[value];
// logger.debug(`Replacing value "${obj[key]}" with "${value}"`);
}
// Process value recursively if it's an object or array
newObj[newKey] = replaceJsonKeys(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 Error(`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');
}
};
// 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())}`);
// Try to extract more useful information from the error
let errorMessage = error.message || 'Unknown API error';
let errorName = error.name || 'ApiError';
// Handle the specific JSON parsing error
if (errorName === 'SyntaxError' && errorMessage.includes('JSON')) {
logger.debug('JSON parsing error detected');
return res.status(200).json({
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()),
});
}
// Send a more graceful response
return res.status(200).json({
status: 'error',
message: errorMessage,
error_type: errorName,
timestamp: global.Utils.toIsoString(new Date()),
});
};
// 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
};