diff --git a/public/app.js b/public/app.js index 9ef5972..42e6d03 100644 --- a/public/app.js +++ b/public/app.js @@ -24,25 +24,33 @@ if (savedTheme === 'light') { document.querySelector('.theme-toggle i').className = 'fas fa-sun' } +// HTML escaping utility to prevent XSS attacks +function escapeHtml(text) { + if (text === null || text === undefined) return '' + const div = document.createElement('div') + div.textContent = String(text) + return div.innerHTML +} + // Toast notification function showToast(message, type = 'success') { const container = document.getElementById('toastContainer') const toast = document.createElement('div') toast.className = `toast toast-${type}` - + const iconMap = { success: 'fa-check-circle', error: 'fa-exclamation-circle', info: 'fa-info-circle' } - + toast.innerHTML = ` - ${message} + ${escapeHtml(message)} ` - + container.appendChild(toast) - + setTimeout(() => { toast.remove() }, 5000) @@ -54,7 +62,7 @@ function updateStatus(data) { const badge = document.getElementById('statusBadge') const btnStart = document.getElementById('btnStart') const btnStop = document.getElementById('btnStop') - + if (data.running) { badge.className = 'status-badge status-running' badge.textContent = 'RUNNING' @@ -78,29 +86,30 @@ function updateMetrics(data) { function updateAccounts(data) { accounts = data const container = document.getElementById('accountsList') - + if (data.length === 0) { container.innerHTML = '

No accounts configured

' return } - + + // SECURITY FIX: Escape all user-provided data to prevent XSS container.innerHTML = data.map(acc => `
- +
- +
`).join('') @@ -116,21 +125,22 @@ function addLog(log) { function renderLogs() { const container = document.getElementById('logsContainer') - + if (logs.length === 0) { container.innerHTML = '

No logs yet...

' return } - + + // SECURITY FIX: Escape all log data to prevent XSS container.innerHTML = logs.map(log => ` -
- [${new Date(log.timestamp).toLocaleTimeString()}] - ${log.platform} - [${log.title}] - ${log.message} +
+ [${escapeHtml(new Date(log.timestamp).toLocaleTimeString())}] + ${escapeHtml(log.platform)} + [${escapeHtml(log.title)}] + ${escapeHtml(log.message)}
`).join('') - + // Auto-scroll to bottom container.scrollTop = container.scrollHeight } @@ -144,7 +154,7 @@ async function fetchData() { fetch('/api/metrics'), fetch('/api/logs?limit=100') ]) - + updateStatus(await statusRes.json()) updateAccounts(await accountsRes.json()) updateMetrics(await metricsRes.json()) @@ -223,15 +233,15 @@ function refreshData() { function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' ws = new WebSocket(`${protocol}//${window.location.host}`) - + ws.onopen = () => { console.log('WebSocket connected') } - + ws.onmessage = (event) => { try { const data = JSON.parse(event.data) - + if (data.type === 'init') { logs = data.data.logs || [] renderLogs() @@ -256,12 +266,12 @@ function connectWebSocket() { console.error('WebSocket message error:', error) } } - + ws.onclose = () => { console.log('WebSocket disconnected, reconnecting...') setTimeout(connectWebSocket, 3000) } - + ws.onerror = (error) => { console.error('WebSocket error:', error) } diff --git a/src/index.ts b/src/index.ts index 5986c50..4a24fdf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ import Humanizer from './util/browser/Humanizer' import { formatDetailedError, normalizeRecoveryEmail, shortErrorMessage, Util } from './util/core/Utils' import Axios from './util/network/Axios' import { QueryDiversityEngine } from './util/network/QueryDiversityEngine' -import { log } from './util/notifications/Logger' +import { log, stopWebhookCleanup } from './util/notifications/Logger' import JobState from './util/state/JobState' import { loadAccounts, loadConfig } from './util/state/Load' import { MobileRetryTracker } from './util/state/MobileRetryTracker' @@ -908,18 +908,22 @@ async function main(): Promise { const errorMsg = reason instanceof Error ? reason.message : String(reason) const stack = reason instanceof Error ? reason.stack : undefined log('main', 'FATAL', `UnhandledRejection: ${errorMsg}${stack ? `\nStack: ${stack.split('\n').slice(0, 3).join(' | ')}` : ''}`, 'error') + stopWebhookCleanup() // CLEANUP FIX: Stop webhook cleanup interval gracefulExit(1) }) process.on('uncaughtException', (err: Error) => { log('main', 'FATAL', `UncaughtException: ${err.message}${err.stack ? `\nStack: ${err.stack.split('\n').slice(0, 3).join(' | ')}` : ''}`, 'error') + stopWebhookCleanup() // CLEANUP FIX: Stop webhook cleanup interval gracefulExit(1) }) process.on('SIGTERM', () => { log('main', 'SHUTDOWN', 'Received SIGTERM, shutting down gracefully...', 'log') + stopWebhookCleanup() // CLEANUP FIX: Stop webhook cleanup interval gracefulExit(0) }) process.on('SIGINT', () => { log('main', 'SHUTDOWN', 'Received SIGINT (Ctrl+C), shutting down gracefully...', 'log') + stopWebhookCleanup() // CLEANUP FIX: Stop webhook cleanup interval gracefulExit(0) }) } diff --git a/src/util/browser/SmartWait.ts b/src/util/browser/SmartWait.ts index 6e4e493..7f7ede2 100644 --- a/src/util/browser/SmartWait.ts +++ b/src/util/browser/SmartWait.ts @@ -151,181 +151,5 @@ export async function waitForElementSmart( } } -/** - * Wait for navigation to complete intelligently - * Uses URL change + DOM ready instead of fixed timeouts - */ -export async function waitForNavigationSmart( - page: Page, - options: { - expectedUrl?: string | RegExp - maxWaitMs?: number - logFn?: (msg: string) => void - } = {} -): Promise<{ completed: boolean; timeMs: number; url: string }> { - const startTime = Date.now() - const maxWaitMs = options.maxWaitMs ?? 15000 - const logFn = options.logFn ?? (() => { }) - - try { - // Wait for URL to change (if we expect it to) - if (options.expectedUrl) { - const urlPattern = typeof options.expectedUrl === 'string' - ? new RegExp(options.expectedUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) - : options.expectedUrl - - let urlChanged = false - const checkInterval = 100 - const maxChecks = maxWaitMs / checkInterval - - for (let i = 0; i < maxChecks; i++) { - const currentUrl = page.url() - if (urlPattern.test(currentUrl)) { - urlChanged = true - logFn(`✓ URL changed to expected pattern (${Date.now() - startTime}ms)`) - break - } - await page.waitForTimeout(checkInterval) - } - - if (!urlChanged) { - const elapsed = Date.now() - startTime - logFn(`⚠ URL did not match expected pattern after ${elapsed}ms`) - return { completed: false, timeMs: elapsed, url: page.url() } - } - } - - // Wait for page to be ready after navigation - const readyResult = await waitForPageReady(page, { - logFn - }) - - const elapsed = Date.now() - startTime - return { completed: readyResult.ready, timeMs: elapsed, url: page.url() } - - } catch (error) { - const elapsed = Date.now() - startTime - const errorMsg = error instanceof Error ? error.message : String(error) - logFn(`✗ Navigation wait failed after ${elapsed}ms: ${errorMsg}`) - return { completed: false, timeMs: elapsed, url: page.url() } - } -} - -/** - * Click element with smart waiting (wait for element + click + verify action) - */ -export async function clickElementSmart( - page: Page, - selector: string, - options: { - waitBeforeClick?: number - waitAfterClick?: number - verifyDisappeared?: boolean - maxWaitMs?: number - logFn?: (msg: string) => void - } = {} -): Promise<{ success: boolean; timeMs: number }> { - const startTime = Date.now() - const waitBeforeClick = options.waitBeforeClick ?? 100 - const waitAfterClick = options.waitAfterClick ?? 500 - const logFn = options.logFn ?? (() => { }) - - try { - // Wait for element to be clickable - const elementResult = await waitForElementSmart(page, selector, { - state: 'visible', - initialTimeoutMs: options.maxWaitMs ? Math.floor(options.maxWaitMs * 0.4) : 2000, - extendedTimeoutMs: options.maxWaitMs ? Math.floor(options.maxWaitMs * 0.6) : 5000, - logFn - }) - - if (!elementResult.found || !elementResult.element) { - return { success: false, timeMs: Date.now() - startTime } - } - - // Small delay for stability - if (waitBeforeClick > 0) { - await page.waitForTimeout(waitBeforeClick) - } - - // Click the element - await elementResult.element.click() - logFn('✓ Clicked element') - - // Wait for action to process - if (waitAfterClick > 0) { - await page.waitForTimeout(waitAfterClick) - } - - // Verify element disappeared (optional) - if (options.verifyDisappeared) { - const disappeared = await page.locator(selector).isVisible() - .then(() => false) - .catch(() => true) - - if (disappeared) { - logFn('✓ Element disappeared after click (expected)') - } - } - - const elapsed = Date.now() - startTime - return { success: true, timeMs: elapsed } - - } catch (error) { - const elapsed = Date.now() - startTime - const errorMsg = error instanceof Error ? error.message : String(error) - logFn(`✗ Click failed after ${elapsed}ms: ${errorMsg}`) - return { success: false, timeMs: elapsed } - } -} - -/** - * Type text into input field with smart waiting - */ -export async function typeIntoFieldSmart( - page: Page, - selector: string, - text: string, - options: { - clearFirst?: boolean - delay?: number - maxWaitMs?: number - logFn?: (msg: string) => void - } = {} -): Promise<{ success: boolean; timeMs: number }> { - const startTime = Date.now() - const delay = options.delay ?? 20 - const logFn = options.logFn ?? (() => { }) - - try { - // Wait for input field - const elementResult = await waitForElementSmart(page, selector, { - state: 'visible', - initialTimeoutMs: options.maxWaitMs ? Math.floor(options.maxWaitMs * 0.4) : 2000, - extendedTimeoutMs: options.maxWaitMs ? Math.floor(options.maxWaitMs * 0.6) : 5000, - logFn - }) - - if (!elementResult.found || !elementResult.element) { - return { success: false, timeMs: Date.now() - startTime } - } - - // Clear field if requested - if (options.clearFirst) { - await elementResult.element.clear() - } - - // Type text with delay - await elementResult.element.type(text, { delay }) - logFn('✓ Typed into field') - - const elapsed = Date.now() - startTime - return { success: true, timeMs: elapsed } - - } catch (error) { - const elapsed = Date.now() - startTime - const errorMsg = error instanceof Error ? error.message : String(error) - logFn(`✗ Type failed after ${elapsed}ms: ${errorMsg}`) - return { success: false, timeMs: elapsed } - } -} +// DEAD CODE REMOVED: waitForNavigationSmart, clickElementSmart, and typeIntoFieldSmart +// These functions were never used in the codebase and have been removed to reduce complexity diff --git a/src/util/core/Utils.ts b/src/util/core/Utils.ts index 049a4f7..0d94485 100644 --- a/src/util/core/Utils.ts +++ b/src/util/core/Utils.ts @@ -9,23 +9,8 @@ export function getErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error) } -/** - * Format a standardized error message for logging - * Ensures consistent error message formatting across all modules - * - * @param context - Context string (e.g., 'SEARCH-BING', 'LOGIN') - * @param error - Error object or unknown value - * @param prefix - Optional custom prefix (defaults to 'Error') - * @returns Formatted error message - * - * @example - * formatErrorMessage('SEARCH', err) // 'Error in SEARCH: Network timeout' - * formatErrorMessage('LOGIN', err, 'Failed') // 'Failed in LOGIN: Invalid credentials' - */ -export function formatErrorMessage(context: string, error: unknown, prefix: string = 'Error'): string { - const errorMsg = getErrorMessage(error) - return `${prefix} in ${context}: ${errorMsg}` -} +// DEAD CODE REMOVED: formatErrorMessage() was never used (only JSDoc examples existed) +// Use formatDetailedError() instead for error formatting with optional stack traces /** * Utility class for common operations