From 20b41d90ab5bc98bde7f7205f3e6202be6da5ab0 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 01:59:05 +0100 Subject: [PATCH 1/2] fix: restore missing downloadAvatar helper and fix TypeScript build errors --- frontend/dist-electron/main.js | 332 +++++++------- frontend/electron/main.ts | 811 +++++++++++++++------------------ 2 files changed, 525 insertions(+), 618 deletions(-) diff --git a/frontend/dist-electron/main.js b/frontend/dist-electron/main.js index 2788a10..8d35802 100644 --- a/frontend/dist-electron/main.js +++ b/frontend/dist-electron/main.js @@ -20,18 +20,11 @@ const isDev = !electron_1.app.isPackaged; electron_1.app.name = "Ultimate Ban Tracker"; // Load environment variables dotenv_1.default.config({ path: path_1.default.join(electron_1.app.getAppPath(), '..', '.env') }); -// --- Server Configuration --- +// --- App State --- +let mainWindow = null; +let tray = null; let backend = null; -const initBackend = () => { - const config = store.get('serverConfig'); - if (config && config.enabled && config.url) { - console.log(`[Backend] Initializing with URL: ${config.url}`); - backend = new backend_1.BackendService(config.url, config.token); - } - else { - backend = null; - } -}; +electron_1.app.isQuitting = false; const store = new electron_store_1.default({ defaults: { accounts: [], @@ -55,62 +48,113 @@ const downloadAvatar = async (steamId, url) => { return undefined; } }; -electron_1.protocol.registerSchemesAsPrivileged([ - { scheme: 'steam-resource', privileges: { secure: true, standard: true, supportFetchAPI: true } } -]); -// --- Main Window --- -let mainWindow = null; -function createWindow() { - mainWindow = new electron_1.BrowserWindow({ - width: 1280, - height: 800, - title: "Ultimate Ban Tracker Desktop", - backgroundColor: '#171a21', - autoHideMenuBar: true, - webPreferences: { - preload: path_1.default.join(__dirname, 'preload.js'), - nodeIntegration: false, - contextIsolation: true, - }, - }); - mainWindow.setMenu(null); - if (isDev) { - mainWindow.loadURL('http://localhost:5173'); +// --- Backend --- +const initBackend = () => { + const config = store.get('serverConfig'); + if (config && config.enabled && config.url) { + backend = new backend_1.BackendService(config.url, config.token); } else { - mainWindow.loadFile(path_1.default.join(__dirname, '..', 'dist', 'index.html')); + backend = null; } -} -// --- Sync Logic --- +}; +// --- System Tray --- +const createTray = () => { + const iconPath = path_1.default.join(electron_1.app.getAppPath(), isDev ? 'assets-build/icon.png' : '../assets-build/icon.png'); + if (!fs_1.default.existsSync(iconPath)) + return; + const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 }); + tray = new electron_1.Tray(icon); + tray.setToolTip('Ultimate Ban Tracker'); + tray.on('click', () => { + if (mainWindow) { + mainWindow.show(); + mainWindow.focus(); + } + }); + updateTrayMenu(); +}; +const updateTrayMenu = () => { + if (!tray) + return; + const accounts = store.get('accounts'); + const config = store.get('serverConfig'); + const contextMenu = electron_1.Menu.buildFromTemplate([ + { label: `Ultimate Ban Tracker v${electron_1.app.getVersion()}`, enabled: false }, + { type: 'separator' }, + { + label: 'Switch Account', + submenu: accounts.length > 0 ? accounts.map(acc => ({ + label: `${acc.personaName} ${acc.loginName ? `(${acc.loginName})` : ''}`, + enabled: !!acc.loginName, + click: () => handleSwitchAccount(acc.loginName) + })) : [{ label: 'No accounts found', enabled: false }] + }, + { + label: 'Sync Now', + enabled: !!config?.enabled, + click: () => syncAccounts() + }, + { type: 'separator' }, + { label: 'Show Dashboard', click: () => { if (mainWindow) + mainWindow.show(); } }, + { label: 'Quit', click: () => { electron_1.app.isQuitting = true; electron_1.app.quit(); } } + ]); + tray.setContextMenu(contextMenu); +}; +// --- Steam Logic --- +const killSteam = async () => { + return new Promise((resolve) => { + const command = process.platform === 'win32' ? 'taskkill /f /im steam.exe' : 'pkill -9 steam'; + (0, child_process_1.exec)(command, () => setTimeout(resolve, 1000)); + }); +}; +const startSteam = () => { + const command = process.platform === 'win32' ? 'start steam://open/main' : 'steam &'; + (0, child_process_1.exec)(command); +}; +const handleSwitchAccount = async (loginName) => { + if (!loginName) + return false; + try { + await killSteam(); + const accounts = store.get('accounts'); + const account = accounts.find(a => a.loginName === loginName); + if (process.platform === 'win32') { + const regCommand = `reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "${loginName}" /f`; + const rememberCommand = `reg add "HKCU\\Software\\Valve\\Steam" /v RememberPassword /t REG_DWORD /d 1 /f`; + await new Promise((res, rej) => (0, child_process_1.exec)(`${regCommand} && ${rememberCommand}`, (e) => e ? rej(e) : res())); + if (account && account.loginConfig) + steam_client_1.steamClient.injectAccountConfig(loginName, account.loginConfig); + } + else if (process.platform === 'linux') { + await steam_client_1.steamClient.setAutoLoginUser(loginName, account?.loginConfig, account?.steamId); + } + startSteam(); + return true; + } + catch (e) { + return false; + } +}; +// --- Sync Worker --- const syncAccounts = async () => { initBackend(); let accounts = store.get('accounts'); let hasChanges = false; - // 1. PULL SHARED ACCOUNTS FROM SERVER if (backend) { - console.log('[Sync] Phase 1: Pulling from server...'); try { const shared = await backend.getSharedAccounts(); for (const s of shared) { const exists = accounts.find(a => a.steamId === s.steamId); if (!exists) { - console.log(`[Sync] Discovered new account on server: ${s.personaName}`); accounts.push({ _id: `shared_${s.steamId}`, - steamId: s.steamId, - personaName: s.personaName, - avatar: s.avatar, - profileUrl: s.profileUrl, - vacBanned: s.vacBanned, - gameBans: s.gameBans, - cooldownExpiresAt: s.cooldownExpiresAt, - loginName: s.loginName || '', - steamLoginSecure: s.steamLoginSecure, - loginConfig: s.loginConfig, - sessionUpdatedAt: s.sessionUpdatedAt, - autoCheckCooldown: s.steamLoginSecure ? true : false, - status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', - lastBanCheck: new Date().toISOString() + steamId: s.steamId, personaName: s.personaName, avatar: s.avatar, profileUrl: s.profileUrl, + vacBanned: s.vacBanned, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt, + loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginConfig: s.loginConfig, + sessionUpdatedAt: s.sessionUpdatedAt, autoCheckCooldown: !!s.steamLoginSecure, + status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString() }); hasChanges = true; } @@ -118,7 +162,6 @@ const syncAccounts = async () => { const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0); const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0); if (sDate > lDate) { - console.log(`[Sync] Updating session for ${exists.personaName} (Server is newer)`); if (s.loginName) exists.loginName = s.loginName; if (s.loginConfig) @@ -138,28 +181,23 @@ const syncAccounts = async () => { } } } - catch (e) { - console.error('[Sync] Pull failed'); - } + catch (e) { } } - // BROADCAST PULL RESULTS IMMEDIATELY if (hasChanges) { store.set('accounts', accounts); if (mainWindow) mainWindow.webContents.send('accounts-updated', accounts); + updateTrayMenu(); } if (accounts.length === 0) return; - // 2. BACKGROUND STEALTH CHECKS - console.log(`[Sync] Phase 2: Starting background checks for ${accounts.length} accounts...`); const updatedAccounts = [...accounts]; let scrapeChanges = false; for (const account of updatedAccounts) { try { const now = new Date(); const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0); - const hoursSinceCheck = (now.getTime() - lastCheck.getTime()) / 3600000; - if (hoursSinceCheck > 6 || !account.personaName) { + if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) { const profile = await (0, steam_web_1.fetchProfileData)(account.steamId, account.steamLoginSecure); const bans = await (0, steam_web_1.scrapeBanStatus)(profile.profileUrl, account.steamLoginSecure); account.personaName = profile.personaName; @@ -189,23 +227,14 @@ const syncAccounts = async () => { if (account.cooldownExpiresAt && new Date(account.cooldownExpiresAt) > now) continue; const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0); - const hoursSinceScrape = (now.getTime() - lastScrape.getTime()) / 3600000; - if (hoursSinceScrape > 8) { - const jitter = Math.floor(Math.random() * 60000) + 5000; - await new Promise(r => setTimeout(r, jitter)); + if ((now.getTime() - lastScrape.getTime()) / 3600000 > 8) { + await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000)); try { const result = await (0, scraper_1.scrapeCooldown)(account.steamId, account.steamLoginSecure); account.authError = false; account.lastScrapeTime = new Date().toISOString(); if (result.isActive) { - if (result.expiresAt) { - account.cooldownExpiresAt = result.expiresAt.toISOString(); - } - else if (!account.cooldownExpiresAt) { - const placeholder = new Date(); - placeholder.setHours(placeholder.getHours() + 24); - account.cooldownExpiresAt = placeholder.toISOString(); - } + account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString(); if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt); } @@ -231,14 +260,13 @@ const syncAccounts = async () => { store.set('accounts', updatedAccounts); if (mainWindow) mainWindow.webContents.send('accounts-updated', updatedAccounts); + updateTrayMenu(); } - console.log('[Sync] Sync cycle finished.'); }; const scheduleNextSync = () => { - const delay = isDev ? 120000 : (Math.random() * 30 * 60000) + 30 * 60000; - setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, delay); + setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, isDev ? 120000 : 1800000); }; -// --- Steam Auto-Discovery --- +// --- Discovery --- const addingAccounts = new Set(); const handleLocalAccountsFound = async (localAccounts) => { const currentAccounts = store.get('accounts'); @@ -261,17 +289,11 @@ const handleLocalAccountsFound = async (localAccounts) => { const localPath = await downloadAvatar(profile.steamId, profile.avatar); currentAccounts.push({ _id: Date.now().toString() + Math.random().toString().slice(2, 5), - steamId: local.steamId, - personaName: profile.personaName || local.personaName || local.accountName, - loginName: local.accountName, - autoCheckCooldown: false, - avatar: profile.avatar, - localAvatar: localPath, - profileUrl: profile.profileUrl, + steamId: local.steamId, personaName: profile.personaName || local.accountName, + loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar, + localAvatar: localPath, profileUrl: profile.profileUrl, status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none', - vacBanned: bans.vacBanned, - gameBans: bans.gameBans, - lastBanCheck: new Date().toISOString() + vacBanned: bans.vacBanned, gameBans: bans.gameBans, lastBanCheck: new Date().toISOString() }); hasChanges = true; } @@ -283,8 +305,29 @@ const handleLocalAccountsFound = async (localAccounts) => { store.set('accounts', currentAccounts); if (mainWindow) mainWindow.webContents.send('accounts-updated', currentAccounts); + updateTrayMenu(); } }; +// --- Main Window Creation --- +function createWindow() { + mainWindow = new electron_1.BrowserWindow({ + width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true, + webPreferences: { preload: path_1.default.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true } + }); + mainWindow.setMenu(null); + mainWindow.on('close', (event) => { + if (!electron_1.app.isQuitting) { + event.preventDefault(); + mainWindow?.hide(); + } + return false; + }); + if (isDev) + mainWindow.loadURL('http://localhost:5173'); + else + mainWindow.loadFile(path_1.default.join(__dirname, '..', 'dist', 'index.html')); +} +// --- App Lifecycle --- electron_1.app.whenReady().then(() => { electron_1.protocol.handle('steam-resource', (request) => { let rawPath = decodeURIComponent(request.url.replace('steam-resource://', '')); @@ -301,13 +344,19 @@ electron_1.app.whenReady().then(() => { } }); createWindow(); + createTray(); initBackend(); setTimeout(syncAccounts, 5000); scheduleNextSync(); steam_client_1.steamClient.startWatching(handleLocalAccountsFound); }); +electron_1.app.on('window-all-closed', () => { if (process.platform !== 'darwin' && electron_1.app.isQuitting) + electron_1.app.quit(); }); +electron_1.app.on('activate', () => { if (electron_1.BrowserWindow.getAllWindows().length === 0) + createWindow(); +else + mainWindow?.show(); }); // --- IPC Handlers --- -console.log('[Main] Registering IPC Handlers...'); electron_1.ipcMain.handle('get-accounts', () => store.get('accounts')); electron_1.ipcMain.handle('get-server-config', () => store.get('serverConfig')); electron_1.ipcMain.handle('update-server-config', (event, config) => { @@ -333,7 +382,6 @@ electron_1.ipcMain.handle('login-to-server', async () => { if (captured) return; captured = true; - console.log('[ServerAuth] Securely captured token'); let serverSteamId = undefined; try { const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()); @@ -346,7 +394,6 @@ electron_1.ipcMain.handle('login-to-server', async () => { authWindow.close(); resolve(true); }; - // METHOD 1: Sniff HTTP Headers const filter = { urls: [`${config.url}/*`] }; authWindow.webContents.session.webRequest.onHeadersReceived(filter, (details, callback) => { const headers = details.responseHeaders || {}; @@ -355,7 +402,6 @@ electron_1.ipcMain.handle('login-to-server', async () => { saveServerAuth(authToken); callback({ cancel: false }); }); - // METHOD 2: Watch Window Title (Fallback) authWindow.on('page-title-updated', (event, title) => { if (title.includes('AUTH_TOKEN:')) { const token = title.split('AUTH_TOKEN:')[1]; @@ -363,7 +409,7 @@ electron_1.ipcMain.handle('login-to-server', async () => { saveServerAuth(token); } }); - authWindow.on('closed', () => { resolve(false); }); + authWindow.on('closed', () => resolve(false)); }); }); electron_1.ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId })); @@ -371,7 +417,6 @@ electron_1.ipcMain.handle('sync-now', async () => { await syncAccounts(); return electron_1.ipcMain.handle('add-account', async (event, { identifier }) => { try { initBackend(); - // OPTIMIZATION: Check community server first if (backend) { const shared = await backend.getCommunityAccounts(); const existing = shared.find((s) => s.steamId === identifier || s.profileUrl.includes(identifier)); @@ -380,23 +425,16 @@ electron_1.ipcMain.handle('add-account', async (event, { identifier }) => { if (accounts.find(a => a.steamId === existing.steamId)) throw new Error('Account already tracked'); const newAccount = { - _id: `shared_${existing.steamId}`, - steamId: existing.steamId, - personaName: existing.personaName, - avatar: existing.avatar, - profileUrl: existing.profileUrl, - vacBanned: existing.vacBanned, - gameBans: existing.gameBans, - cooldownExpiresAt: existing.cooldownExpiresAt, - loginName: existing.loginName || '', - steamLoginSecure: existing.steamLoginSecure, - loginConfig: existing.loginConfig, - sessionUpdatedAt: existing.sessionUpdatedAt, - autoCheckCooldown: existing.steamLoginSecure ? true : false, - status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none', + _id: `shared_${existing.steamId}`, steamId: existing.steamId, personaName: existing.personaName, + avatar: existing.avatar, profileUrl: existing.profileUrl, vacBanned: existing.vacBanned, + gameBans: existing.gameBans, cooldownExpiresAt: existing.cooldownExpiresAt, + loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure, + loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt, + autoCheckCooldown: !!existing.steamLoginSecure, status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString() }; store.set('accounts', [...accounts, newAccount]); + updateTrayMenu(); return newAccount; } } @@ -405,13 +443,13 @@ electron_1.ipcMain.handle('add-account', async (event, { identifier }) => { const localAvatar = await downloadAvatar(profile.steamId, profile.avatar); const accounts = store.get('accounts'); const newAccount = { - _id: Date.now().toString(), - steamId: profile.steamId, personaName: profile.personaName, loginName: '', - avatar: profile.avatar, localAvatar: localAvatar, profileUrl: profile.profileUrl, + _id: Date.now().toString(), steamId: profile.steamId, personaName: profile.personaName, + loginName: '', avatar: profile.avatar, localAvatar: localAvatar, profileUrl: profile.profileUrl, autoCheckCooldown: false, vacBanned: bans.vacBanned, gameBans: bans.gameBans, status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString() }; store.set('accounts', [...accounts, newAccount]); + updateTrayMenu(); return newAccount; } catch (error) { @@ -424,6 +462,7 @@ electron_1.ipcMain.handle('update-account', (event, id, data) => { if (index !== -1) { accounts[index] = { ...accounts[index], ...data }; store.set('accounts', accounts); + updateTrayMenu(); return accounts[index]; } return null; @@ -431,6 +470,7 @@ electron_1.ipcMain.handle('update-account', (event, id, data) => { electron_1.ipcMain.handle('delete-account', (event, id) => { const accounts = store.get('accounts'); store.set('accounts', accounts.filter((a) => a._id !== id)); + updateTrayMenu(); return true; }); electron_1.ipcMain.handle('share-account-with-user', async (event, steamId, targetSteamId) => { @@ -446,56 +486,15 @@ electron_1.ipcMain.handle('share-account-with-user', async (event, steamId, targ }); electron_1.ipcMain.handle('get-community-accounts', async () => { initBackend(); return backend ? await backend.getCommunityAccounts() : []; }); electron_1.ipcMain.handle('get-server-users', async () => { initBackend(); return backend ? await backend.getServerUsers() : []; }); -const killSteam = async () => { - return new Promise((resolve) => { - const command = process.platform === 'win32' ? 'taskkill /f /im steam.exe' : 'pkill -9 steam'; - (0, child_process_1.exec)(command, () => setTimeout(resolve, 1000)); - }); -}; -const startSteam = () => { - const command = process.platform === 'win32' ? 'start steam://open/main' : 'steam &'; - (0, child_process_1.exec)(command); -}; -electron_1.ipcMain.handle('switch-account', async (event, loginName) => { - if (!loginName) - return false; - try { - await killSteam(); - const accounts = store.get('accounts'); - const account = accounts.find(a => a.loginName === loginName); - if (process.platform === 'win32') { - const regCommand = `reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "${loginName}" /f`; - const rememberCommand = `reg add "HKCU\\Software\\Valve\\Steam" /v RememberPassword /t REG_DWORD /d 1 /f`; - await new Promise((res, rej) => (0, child_process_1.exec)(`${regCommand} && ${rememberCommand}`, (e) => e ? rej(e) : res())); - if (account && account.loginConfig) - steam_client_1.steamClient.injectAccountConfig(loginName, account.loginConfig); - } - else if (process.platform === 'linux') { - await steam_client_1.steamClient.setAutoLoginUser(loginName, account?.loginConfig, account?.steamId); - } - startSteam(); - return true; - } - catch (e) { - return false; - } -}); +electron_1.ipcMain.handle('switch-account', async (event, loginName) => await handleSwitchAccount(loginName)); electron_1.ipcMain.handle('open-external', (event, url) => electron_1.shell.openExternal(url)); electron_1.ipcMain.handle('open-steam-login', async (event, expectedSteamId) => { const loginSession = electron_1.session.fromPartition('persist:steam-login'); await loginSession.clearStorageData({ storages: ['cookies', 'localstorage', 'indexdb'] }); return new Promise((resolve) => { const loginWindow = new electron_1.BrowserWindow({ - width: 800, - height: 700, - parent: mainWindow || undefined, - modal: true, - title: 'Login to Steam (Ensure "Remember Me" is checked!)', - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - partition: 'persist:steam-login' - } + width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Steam', + webPreferences: { nodeIntegration: false, contextIsolation: true, partition: 'persist:steam-login' } }); loginWindow.loadURL('https://steamcommunity.com/login/home/?goto=my/gcpd/730'); const checkCookie = setInterval(async () => { @@ -505,13 +504,10 @@ electron_1.ipcMain.handle('open-steam-login', async (event, expectedSteamId) => if (secureCookie) { const steamId = decodeURIComponent(secureCookie.value).split('|')[0]; if (steamId) { - if (expectedSteamId && steamId !== expectedSteamId) { - console.error(`[Auth] ID Mismatch! Expected ${expectedSteamId}, got ${steamId}`); + if (expectedSteamId && steamId !== expectedSteamId) return; - } clearInterval(checkCookie); const cookieString = cookies.map(c => `${c.name}=${c.value}`).join('; '); - console.log(`[Auth] Captured session for SteamID: ${steamId}`); const accounts = store.get('accounts'); const accountIndex = accounts.findIndex(a => a.steamId === steamId); if (accountIndex !== -1) { @@ -526,25 +522,18 @@ electron_1.ipcMain.handle('open-steam-login', async (event, expectedSteamId) => account.loginConfig = config; } try { - console.log(`[Auth] Performing initial scrape for ${account.personaName}...`); const result = await (0, scraper_1.scrapeCooldown)(account.steamId, cookieString); account.lastScrapeTime = new Date().toISOString(); - if (result.isActive && result.expiresAt) { - account.cooldownExpiresAt = result.expiresAt.toISOString(); - } - else if (!result.isActive) { - account.cooldownExpiresAt = undefined; - } - } - catch (e) { - console.error('[Auth] Initial scrape failed:', e); + account.cooldownExpiresAt = result.isActive && result.expiresAt ? result.expiresAt.toISOString() : undefined; } + catch (e) { } initBackend(); if (backend) await backend.shareAccount(account); store.set('accounts', accounts); if (mainWindow) mainWindow.webContents.send('accounts-updated', accounts); + updateTrayMenu(); loginWindow.close(); resolve(true); } @@ -553,11 +542,6 @@ electron_1.ipcMain.handle('open-steam-login', async (event, expectedSteamId) => } catch (error) { } }, 1000); - loginWindow.on('closed', () => { - clearInterval(checkCookie); - resolve(false); - }); + loginWindow.on('closed', () => { clearInterval(checkCookie); resolve(false); }); }); }); -electron_1.app.on('window-all-closed', () => { if (process.platform !== 'darwin') - electron_1.app.quit(); }); diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index 44b92e8..6850990 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -1,9 +1,8 @@ -import { app, BrowserWindow, ipcMain, shell, session, protocol, net } from 'electron'; +import { app, BrowserWindow, ipcMain, shell, session, protocol, net, Tray, Menu, nativeImage } from 'electron'; import path from 'path'; import Store from 'electron-store'; import { exec } from 'child_process'; import dotenv from 'dotenv'; -import cron from 'node-cron'; import axios from 'axios'; import fs from 'fs'; import { pathToFileURL } from 'url'; @@ -20,20 +19,7 @@ app.name = "Ultimate Ban Tracker"; // Load environment variables dotenv.config({ path: path.join(app.getAppPath(), '..', '.env') }); -// --- Server Configuration --- -let backend: BackendService | null = null; - -const initBackend = () => { - const config = store.get('serverConfig'); - if (config && config.enabled && config.url) { - console.log(`[Backend] Initializing with URL: ${config.url}`); - backend = new BackendService(config.url, config.token); - } else { - backend = null; - } -}; - -// --- Local Data Store --- +// --- Types & Interfaces --- interface Account { _id: string; steamId: string; @@ -61,8 +47,15 @@ interface ServerConfig { token?: string; serverSteamId?: string; enabled: boolean; + theme?: string; } +// --- App State --- +let mainWindow: BrowserWindow | null = null; +let tray: Tray | null = null; +let backend: BackendService | null = null; +(app as any).isQuitting = false; + const store = new Store<{ accounts: Account[], serverConfig: ServerConfig }>({ defaults: { accounts: [], @@ -86,403 +79,63 @@ const downloadAvatar = async (steamId: string, url: string): Promise { + const config = store.get('serverConfig'); + if (config && config.enabled && config.url) { + backend = new BackendService(config.url, config.token); } else { - mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); + backend = null; } -} +}; -// --- Sync Logic --- - -const syncAccounts = async () => { - initBackend(); - let accounts = store.get('accounts') as Account[]; - let hasChanges = false; +// --- System Tray --- +const createTray = () => { + const iconPath = path.join(app.getAppPath(), isDev ? 'assets-build/icon.png' : '../assets-build/icon.png'); + if (!fs.existsSync(iconPath)) return; - // 1. PULL SHARED ACCOUNTS FROM SERVER - if (backend) { - console.log('[Sync] Phase 1: Pulling from server...'); - try { - const shared = await backend.getSharedAccounts(); - for (const s of shared) { - const exists = accounts.find(a => a.steamId === s.steamId); - if (!exists) { - console.log(`[Sync] Discovered new account on server: ${s.personaName}`); - accounts.push({ - _id: `shared_${s.steamId}`, - steamId: s.steamId, - personaName: s.personaName, - avatar: s.avatar, - profileUrl: s.profileUrl, - vacBanned: s.vacBanned, - gameBans: s.gameBans, - cooldownExpiresAt: s.cooldownExpiresAt, - loginName: s.loginName || '', - steamLoginSecure: s.steamLoginSecure, - loginConfig: s.loginConfig, - sessionUpdatedAt: s.sessionUpdatedAt, - autoCheckCooldown: s.steamLoginSecure ? true : false, - status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', - lastBanCheck: new Date().toISOString() - }); - hasChanges = true; - } else { - const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0); - const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0); - - if (sDate > lDate) { - console.log(`[Sync] Updating session for ${exists.personaName} (Server is newer)`); - if (s.loginName) exists.loginName = s.loginName; - if (s.loginConfig) exists.loginConfig = s.loginConfig; - if (s.steamLoginSecure) { - exists.steamLoginSecure = s.steamLoginSecure; - exists.autoCheckCooldown = true; - exists.authError = false; - } - exists.sessionUpdatedAt = s.sessionUpdatedAt; - hasChanges = true; - } - - if (s.cooldownExpiresAt && (!exists.cooldownExpiresAt || new Date(s.cooldownExpiresAt) > new Date(exists.cooldownExpiresAt))) { - exists.cooldownExpiresAt = s.cooldownExpiresAt; - hasChanges = true; - } - } - } - } catch (e) { - console.error('[Sync] Pull failed'); + const icon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 }); + tray = new Tray(icon); + tray.setToolTip('Ultimate Ban Tracker'); + tray.on('click', () => { + if (mainWindow) { + mainWindow.show(); + mainWindow.focus(); } - } - - // BROADCAST PULL RESULTS IMMEDIATELY - if (hasChanges) { - store.set('accounts', accounts); - if (mainWindow) mainWindow.webContents.send('accounts-updated', accounts); - } - - if (accounts.length === 0) return; - - // 2. BACKGROUND STEALTH CHECKS - console.log(`[Sync] Phase 2: Starting background checks for ${accounts.length} accounts...`); - const updatedAccounts = [...accounts]; - let scrapeChanges = false; - - for (const account of updatedAccounts) { - try { - const now = new Date(); - - const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0); - const hoursSinceCheck = (now.getTime() - lastCheck.getTime()) / 3600000; - - if (hoursSinceCheck > 6 || !account.personaName) { - const profile = await fetchProfileData(account.steamId, account.steamLoginSecure); - const bans = await scrapeBanStatus(profile.profileUrl, account.steamLoginSecure); - - account.personaName = profile.personaName; - account.profileUrl = profile.profileUrl; - account.vacBanned = bans.vacBanned; - account.gameBans = bans.gameBans; - account.status = (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none'; - account.lastBanCheck = now.toISOString(); - - if (profile.avatar && (!account.localAvatar || profile.avatar !== account.avatar)) { - account.avatar = profile.avatar; - const localPath = await downloadAvatar(account.steamId, profile.avatar); - if (localPath) account.localAvatar = localPath; - } - - if (account.loginName) { - const config = steamClient.extractAccountConfig(account.loginName); - if (config) { - account.loginConfig = config; - account.sessionUpdatedAt = new Date().toISOString(); - } - } - - if (backend) await backend.shareAccount(account); - scrapeChanges = true; - } - - if (account.autoCheckCooldown && account.steamLoginSecure) { - if (account.cooldownExpiresAt && new Date(account.cooldownExpiresAt) > now) continue; - - const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0); - const hoursSinceScrape = (now.getTime() - lastScrape.getTime()) / 3600000; - - if (hoursSinceScrape > 8) { - const jitter = Math.floor(Math.random() * 60000) + 5000; - await new Promise(r => setTimeout(r, jitter)); - - try { - const result = await scrapeCooldown(account.steamId, account.steamLoginSecure); - account.authError = false; - account.lastScrapeTime = new Date().toISOString(); - - if (result.isActive) { - if (result.expiresAt) { - account.cooldownExpiresAt = result.expiresAt.toISOString(); - } else if (!account.cooldownExpiresAt) { - const placeholder = new Date(); - placeholder.setHours(placeholder.getHours() + 24); - account.cooldownExpiresAt = placeholder.toISOString(); - } - if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt); - } else if (account.cooldownExpiresAt) { - account.cooldownExpiresAt = undefined; - if (backend) await backend.pushCooldown(account.steamId, undefined); - } - scrapeChanges = true; - } catch (e: any) { - if (e.message.includes('cookie') || e.message.includes('Sign In')) { - account.authError = true; - scrapeChanges = true; - } - } - } - } - } catch (error) { } - } - - if (scrapeChanges) { - store.set('accounts', updatedAccounts); - if (mainWindow) mainWindow.webContents.send('accounts-updated', updatedAccounts); - } - console.log('[Sync] Sync cycle finished.'); -}; - -const scheduleNextSync = () => { - const delay = isDev ? 120000 : (Math.random() * 30 * 60000) + 30 * 60000; - setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, delay); -}; - -// --- Steam Auto-Discovery --- -const addingAccounts = new Set(); - -const handleLocalAccountsFound = async (localAccounts: LocalSteamAccount[]) => { - const currentAccounts = store.get('accounts') as Account[]; - let hasChanges = false; - - for (const local of localAccounts) { - if (addingAccounts.has(local.steamId)) continue; - const exists = currentAccounts.find(a => a.steamId === local.steamId); - if (exists) { - if (!exists.loginName && local.accountName) { exists.loginName = local.accountName; hasChanges = true; } - } else { - addingAccounts.add(local.steamId); - try { - const profile = await fetchProfileData(local.steamId); - const bans = await scrapeBanStatus(profile.profileUrl); - const localPath = await downloadAvatar(profile.steamId, profile.avatar); - currentAccounts.push({ - _id: Date.now().toString() + Math.random().toString().slice(2, 5), - steamId: local.steamId, - personaName: profile.personaName || local.personaName || local.accountName, - loginName: local.accountName, - autoCheckCooldown: false, - avatar: profile.avatar, - localAvatar: localPath, - profileUrl: profile.profileUrl, - status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none', - vacBanned: bans.vacBanned, - gameBans: bans.gameBans, - lastBanCheck: new Date().toISOString() - }); - hasChanges = true; - } catch (e) { } - addingAccounts.delete(local.steamId); - } - } - if (hasChanges) { - store.set('accounts', currentAccounts); - if (mainWindow) mainWindow.webContents.send('accounts-updated', currentAccounts); - } -}; - -app.whenReady().then(() => { - protocol.handle('steam-resource', (request) => { - let rawPath = decodeURIComponent(request.url.replace('steam-resource://', '')); - if (process.platform !== 'win32' && !rawPath.startsWith('/')) rawPath = '/' + rawPath; - const absolutePath = path.isAbsolute(rawPath) ? rawPath : path.resolve(rawPath); - if (!fs.existsSync(absolutePath)) return new Response('Not Found', { status: 404 }); - try { return net.fetch(pathToFileURL(absolutePath).toString()); } catch (e) { return new Response('Error', { status: 500 }); } }); + updateTrayMenu(); +}; - createWindow(); - initBackend(); - setTimeout(syncAccounts, 5000); - scheduleNextSync(); - steamClient.startWatching(handleLocalAccountsFound); -}); - -// --- IPC Handlers --- -console.log('[Main] Registering IPC Handlers...'); - -ipcMain.handle('get-accounts', () => store.get('accounts')); -ipcMain.handle('get-server-config', () => store.get('serverConfig')); - -ipcMain.handle('update-server-config', (event, config: Partial) => { - const current = store.get('serverConfig'); - const updated = { ...current, ...config }; - store.set('serverConfig', updated); - initBackend(); - return updated; -}); - -ipcMain.handle('login-to-server', async () => { - initBackend(); - const config = store.get('serverConfig') as ServerConfig; - if (!config.url) return false; - - return new Promise((resolve) => { - const authWindow = new BrowserWindow({ - width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Ban Tracker Server', - webPreferences: { nodeIntegration: false, contextIsolation: true } - }); - authWindow.loadURL(`${config.url}/auth/steam`); - - let captured = false; - const saveServerAuth = (token: string) => { - if (captured) return; - captured = true; - console.log('[ServerAuth] Securely captured token'); - let serverSteamId = undefined; - try { - const payload = JSON.parse(Buffer.from(token.split('.')[1]!, 'base64').toString()); - serverSteamId = payload.steamId; - } catch (e) {} - - const current = store.get('serverConfig'); - store.set('serverConfig', { ...current, token, serverSteamId, enabled: true }); - initBackend(); - authWindow.close(); - resolve(true); - }; - - // METHOD 1: Sniff HTTP Headers - const filter = { urls: [`${config.url}/*`] }; - authWindow.webContents.session.webRequest.onHeadersReceived(filter, (details, callback) => { - const headers = details.responseHeaders || {}; - const authToken = headers['x-ubt-auth-token']?.[0] || headers['X-UBT-Auth-Token']?.[0]; - if (authToken) saveServerAuth(authToken); - callback({ cancel: false }); - }); - - // METHOD 2: Watch Window Title (Fallback) - authWindow.on('page-title-updated', (event, title) => { - if (title.includes('AUTH_TOKEN:')) { - const token = title.split('AUTH_TOKEN:')[1]; - if (token) saveServerAuth(token); - } - }); - - authWindow.on('closed', () => { resolve(false); }); - }); -}); - -ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId })); - -ipcMain.handle('sync-now', async () => { await syncAccounts(); return true; }); - -ipcMain.handle('add-account', async (event, { identifier }) => { - try { - initBackend(); - // OPTIMIZATION: Check community server first - if (backend) { - const shared = await backend.getCommunityAccounts(); - const existing = shared.find((s: any) => s.steamId === identifier || s.profileUrl.includes(identifier)); - if (existing) { - const accounts = store.get('accounts') as Account[]; - if (accounts.find(a => a.steamId === existing.steamId)) throw new Error('Account already tracked'); - - const newAccount: Account = { - _id: `shared_${existing.steamId}`, - steamId: existing.steamId, - personaName: existing.personaName, - avatar: existing.avatar, - profileUrl: existing.profileUrl, - vacBanned: existing.vacBanned, - gameBans: existing.gameBans, - cooldownExpiresAt: existing.cooldownExpiresAt, - loginName: existing.loginName || '', - steamLoginSecure: existing.steamLoginSecure, - loginConfig: existing.loginConfig, - sessionUpdatedAt: existing.sessionUpdatedAt, - autoCheckCooldown: existing.steamLoginSecure ? true : false, - status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none', - lastBanCheck: new Date().toISOString() - }; - store.set('accounts', [...accounts, newAccount]); - return newAccount; - } - } - - const profile = await fetchProfileData(identifier); - const bans = await scrapeBanStatus(profile.profileUrl); - const localAvatar = await downloadAvatar(profile.steamId, profile.avatar); - const accounts = store.get('accounts') as Account[]; - const newAccount: Account = { - _id: Date.now().toString(), - steamId: profile.steamId, personaName: profile.personaName, loginName: '', - avatar: profile.avatar, localAvatar: localAvatar, profileUrl: profile.profileUrl, - autoCheckCooldown: false, vacBanned: bans.vacBanned, gameBans: bans.gameBans, - status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString() - }; - store.set('accounts', [...accounts, newAccount]); - return newAccount; - } catch (error: any) { throw error; } -}); - -ipcMain.handle('update-account', (event, id: string, data: Partial) => { +const updateTrayMenu = () => { + if (!tray) return; const accounts = store.get('accounts') as Account[]; - const index = accounts.findIndex((a: Account) => a._id === id); - if (index !== -1) { accounts[index] = { ...accounts[index], ...data } as Account; store.set('accounts', accounts); return accounts[index]; } - return null; -}); + const config = store.get('serverConfig'); -ipcMain.handle('delete-account', (event, id: string) => { - const accounts = store.get('accounts') as Account[]; - store.set('accounts', accounts.filter((a: Account) => a._id !== id)); - return true; -}); + const contextMenu = Menu.buildFromTemplate([ + { label: `Ultimate Ban Tracker v${app.getVersion()}`, enabled: false }, + { type: 'separator' }, + { + label: 'Switch Account', + submenu: accounts.length > 0 ? accounts.map(acc => ({ + label: `${acc.personaName} ${acc.loginName ? `(${acc.loginName})` : ''}`, + enabled: !!acc.loginName, + click: () => handleSwitchAccount(acc.loginName) + })) : [{ label: 'No accounts found', enabled: false }] + }, + { + label: 'Sync Now', + enabled: !!config?.enabled, + click: () => syncAccounts() + }, + { type: 'separator' }, + { label: 'Show Dashboard', click: () => { if (mainWindow) mainWindow.show(); } }, + { label: 'Quit', click: () => { (app as any).isQuitting = true; app.quit(); } } + ]); -ipcMain.handle('share-account-with-user', async (event, steamId: string, targetSteamId: string) => { - initBackend(); - if (backend) { - const accounts = store.get('accounts') as Account[]; - const account = accounts.find(a => a.steamId === steamId); - if (account) await backend.shareAccount(account); - return await backend.shareWithUser(steamId, targetSteamId); - } - throw new Error('Backend not configured'); -}); - -ipcMain.handle('get-community-accounts', async () => { initBackend(); return backend ? await backend.getCommunityAccounts() : []; }); -ipcMain.handle('get-server-users', async () => { initBackend(); return backend ? await backend.getServerUsers() : []; }); + tray.setContextMenu(contextMenu); +}; +// --- Steam Logic --- const killSteam = async () => { return new Promise((resolve) => { const command = process.platform === 'win32' ? 'taskkill /f /im steam.exe' : 'pkill -9 steam'; @@ -495,7 +148,7 @@ const startSteam = () => { exec(command); }; -ipcMain.handle('switch-account', async (event, loginName: string) => { +const handleSwitchAccount = async (loginName: string) => { if (!loginName) return false; try { await killSteam(); @@ -512,30 +165,322 @@ ipcMain.handle('switch-account', async (event, loginName: string) => { startSteam(); return true; } catch (e) { return false; } +}; + +// --- Sync Worker --- +const syncAccounts = async () => { + initBackend(); + let accounts = store.get('accounts') as Account[]; + let hasChanges = false; + + if (backend) { + try { + const shared = await backend.getSharedAccounts(); + for (const s of shared) { + const exists = accounts.find(a => a.steamId === s.steamId); + if (!exists) { + accounts.push({ + _id: `shared_${s.steamId}`, + steamId: s.steamId, personaName: s.personaName, avatar: s.avatar, profileUrl: s.profileUrl, + vacBanned: s.vacBanned, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt, + loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginConfig: s.loginConfig, + sessionUpdatedAt: s.sessionUpdatedAt, autoCheckCooldown: !!s.steamLoginSecure, + status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString() + }); + hasChanges = true; + } else { + const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0); + const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0); + if (sDate > lDate) { + if (s.loginName) exists.loginName = s.loginName; + if (s.loginConfig) exists.loginConfig = s.loginConfig; + if (s.steamLoginSecure) { exists.steamLoginSecure = s.steamLoginSecure; exists.autoCheckCooldown = true; exists.authError = false; } + exists.sessionUpdatedAt = s.sessionUpdatedAt; + hasChanges = true; + } + if (s.cooldownExpiresAt && (!exists.cooldownExpiresAt || new Date(s.cooldownExpiresAt) > new Date(exists.cooldownExpiresAt))) { + exists.cooldownExpiresAt = s.cooldownExpiresAt; + hasChanges = true; + } + } + } + } catch (e) { } + } + + if (hasChanges) { + store.set('accounts', accounts); + if (mainWindow) mainWindow.webContents.send('accounts-updated', accounts); + updateTrayMenu(); + } + + if (accounts.length === 0) return; + + const updatedAccounts = [...accounts]; + let scrapeChanges = false; + + for (const account of updatedAccounts) { + try { + const now = new Date(); + const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0); + if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) { + const profile = await fetchProfileData(account.steamId, account.steamLoginSecure); + const bans = await scrapeBanStatus(profile.profileUrl, account.steamLoginSecure); + account.personaName = profile.personaName; account.profileUrl = profile.profileUrl; + account.vacBanned = bans.vacBanned; account.gameBans = bans.gameBans; + account.status = (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none'; + account.lastBanCheck = now.toISOString(); + if (profile.avatar && (!account.localAvatar || profile.avatar !== account.avatar)) { + account.avatar = profile.avatar; + const localPath = await downloadAvatar(account.steamId, profile.avatar); + if (localPath) account.localAvatar = localPath; + } + if (account.loginName) { + const config = steamClient.extractAccountConfig(account.loginName); + if (config) { account.loginConfig = config; account.sessionUpdatedAt = new Date().toISOString(); } + } + if (backend) await backend.shareAccount(account); + scrapeChanges = true; + } + + if (account.autoCheckCooldown && account.steamLoginSecure) { + if (account.cooldownExpiresAt && new Date(account.cooldownExpiresAt) > now) continue; + const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0); + if ((now.getTime() - lastScrape.getTime()) / 3600000 > 8) { + await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000)); + try { + const result = await scrapeCooldown(account.steamId, account.steamLoginSecure); + account.authError = false; account.lastScrapeTime = new Date().toISOString(); + if (result.isActive) { + account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString(); + if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt); + } else if (account.cooldownExpiresAt) { + account.cooldownExpiresAt = undefined; + if (backend) await backend.pushCooldown(account.steamId, undefined); + } + scrapeChanges = true; + } catch (e: any) { + if (e.message.includes('cookie') || e.message.includes('Sign In')) { account.authError = true; scrapeChanges = true; } + } + } + } + } catch (error) { } + } + + if (scrapeChanges) { + store.set('accounts', updatedAccounts); + if (mainWindow) mainWindow.webContents.send('accounts-updated', updatedAccounts); + updateTrayMenu(); + } +}; + +const scheduleNextSync = () => { + setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, isDev ? 120000 : 1800000); +}; + +// --- Discovery --- +const addingAccounts = new Set(); +const handleLocalAccountsFound = async (localAccounts: LocalSteamAccount[]) => { + const currentAccounts = store.get('accounts') as Account[]; + let hasChanges = false; + for (const local of localAccounts) { + if (addingAccounts.has(local.steamId)) continue; + const exists = currentAccounts.find(a => a.steamId === local.steamId); + if (exists) { + if (!exists.loginName && local.accountName) { exists.loginName = local.accountName; hasChanges = true; } + } else { + addingAccounts.add(local.steamId); + try { + const profile = await fetchProfileData(local.steamId); + const bans = await scrapeBanStatus(profile.profileUrl); + const localPath = await downloadAvatar(profile.steamId, profile.avatar); + currentAccounts.push({ + _id: Date.now().toString() + Math.random().toString().slice(2, 5), + steamId: local.steamId, personaName: profile.personaName || local.accountName, + loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar, + localAvatar: localPath, profileUrl: profile.profileUrl, + status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none', + vacBanned: bans.vacBanned, gameBans: bans.gameBans, lastBanCheck: new Date().toISOString() + }); + hasChanges = true; + } catch (e) { } + addingAccounts.delete(local.steamId); + } + } + if (hasChanges) { + store.set('accounts', currentAccounts); + if (mainWindow) mainWindow.webContents.send('accounts-updated', currentAccounts); + updateTrayMenu(); + } +}; + +// --- Main Window Creation --- +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true, + webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true } + }); + + mainWindow.setMenu(null); + + mainWindow.on('close', (event) => { + if (!(app as any).isQuitting) { + event.preventDefault(); + mainWindow?.hide(); + } + return false; + }); + + if (isDev) mainWindow.loadURL('http://localhost:5173'); + else mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); +} + +// --- App Lifecycle --- +app.whenReady().then(() => { + protocol.handle('steam-resource', (request) => { + let rawPath = decodeURIComponent(request.url.replace('steam-resource://', '')); + if (process.platform !== 'win32' && !rawPath.startsWith('/')) rawPath = '/' + rawPath; + const absolutePath = path.isAbsolute(rawPath) ? rawPath : path.resolve(rawPath); + if (!fs.existsSync(absolutePath)) return new Response('Not Found', { status: 404 }); + try { return net.fetch(pathToFileURL(absolutePath).toString()); } catch (e) { return new Response('Error', { status: 500 }); } + }); + + createWindow(); + createTray(); + initBackend(); + setTimeout(syncAccounts, 5000); + scheduleNextSync(); + steamClient.startWatching(handleLocalAccountsFound); }); +app.on('window-all-closed', () => { if (process.platform !== 'darwin' && (app as any).isQuitting) app.quit(); }); +app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); else mainWindow?.show(); }); + +// --- IPC Handlers --- +ipcMain.handle('get-accounts', () => store.get('accounts')); +ipcMain.handle('get-server-config', () => store.get('serverConfig')); +ipcMain.handle('update-server-config', (event, config: Partial) => { + const current = store.get('serverConfig'); + const updated = { ...current, ...config }; + store.set('serverConfig', updated); + initBackend(); + return updated; +}); + +ipcMain.handle('login-to-server', async () => { + initBackend(); + const config = store.get('serverConfig') as ServerConfig; + if (!config.url) return false; + return new Promise((resolve) => { + const authWindow = new BrowserWindow({ + width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Ban Tracker Server', + webPreferences: { nodeIntegration: false, contextIsolation: true } + }); + authWindow.loadURL(`${config.url}/auth/steam`); + let captured = false; + const saveServerAuth = (token: string) => { + if (captured) return; captured = true; + let serverSteamId = undefined; + try { const payload = JSON.parse(Buffer.from(token.split('.')[1]!, 'base64').toString()); serverSteamId = payload.steamId; } catch (e) {} + const current = store.get('serverConfig'); + store.set('serverConfig', { ...current, token, serverSteamId, enabled: true }); + initBackend(); + authWindow.close(); + resolve(true); + }; + const filter = { urls: [`${config.url}/*`] }; + authWindow.webContents.session.webRequest.onHeadersReceived(filter, (details, callback) => { + const headers = details.responseHeaders || {}; + const authToken = headers['x-ubt-auth-token']?.[0] || headers['X-UBT-Auth-Token']?.[0]; + if (authToken) saveServerAuth(authToken); + callback({ cancel: false }); + }); + authWindow.on('page-title-updated', (event, title) => { + if (title.includes('AUTH_TOKEN:')) { const token = title.split('AUTH_TOKEN:')[1]; if (token) saveServerAuth(token); } + }); + authWindow.on('closed', () => resolve(false)); + }); +}); + +ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId })); +ipcMain.handle('sync-now', async () => { await syncAccounts(); return true; }); +ipcMain.handle('add-account', async (event, { identifier }) => { + try { + initBackend(); + if (backend) { + const shared = await backend.getCommunityAccounts(); + const existing = shared.find((s: any) => s.steamId === identifier || s.profileUrl.includes(identifier)); + if (existing) { + const accounts = store.get('accounts') as Account[]; + if (accounts.find(a => a.steamId === existing.steamId)) throw new Error('Account already tracked'); + const newAccount: Account = { + _id: `shared_${existing.steamId}`, steamId: existing.steamId, personaName: existing.personaName, + avatar: existing.avatar, profileUrl: existing.profileUrl, vacBanned: existing.vacBanned, + gameBans: existing.gameBans, cooldownExpiresAt: existing.cooldownExpiresAt, + loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure, + loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt, + autoCheckCooldown: !!existing.steamLoginSecure, status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none', + lastBanCheck: new Date().toISOString() + }; + store.set('accounts', [...accounts, newAccount]); + updateTrayMenu(); + return newAccount; + } + } + const profile = await fetchProfileData(identifier); + const bans = await scrapeBanStatus(profile.profileUrl); + const localAvatar = await downloadAvatar(profile.steamId, profile.avatar); + const accounts = store.get('accounts') as Account[]; + const newAccount: Account = { + _id: Date.now().toString(), steamId: profile.steamId, personaName: profile.personaName, + loginName: '', avatar: profile.avatar, localAvatar: localAvatar, profileUrl: profile.profileUrl, + autoCheckCooldown: false, vacBanned: bans.vacBanned, gameBans: bans.gameBans, + status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString() + }; + store.set('accounts', [...accounts, newAccount]); + updateTrayMenu(); + return newAccount; + } catch (error: any) { throw error; } +}); + +ipcMain.handle('update-account', (event, id: string, data: Partial) => { + const accounts = store.get('accounts') as Account[]; + const index = accounts.findIndex((a: Account) => a._id === id); + if (index !== -1) { accounts[index] = { ...accounts[index], ...data } as Account; store.set('accounts', accounts); updateTrayMenu(); return accounts[index]; } + return null; +}); + +ipcMain.handle('delete-account', (event, id: string) => { + const accounts = store.get('accounts') as Account[]; + store.set('accounts', accounts.filter((a: Account) => a._id !== id)); + updateTrayMenu(); + return true; +}); + +ipcMain.handle('share-account-with-user', async (event, steamId: string, targetSteamId: string) => { + initBackend(); + if (backend) { + const accounts = store.get('accounts') as Account[]; + const account = accounts.find(a => a.steamId === steamId); + if (account) await backend.shareAccount(account); + return await backend.shareWithUser(steamId, targetSteamId); + } + throw new Error('Backend not configured'); +}); + +ipcMain.handle('get-community-accounts', async () => { initBackend(); return backend ? await backend.getCommunityAccounts() : []; }); +ipcMain.handle('get-server-users', async () => { initBackend(); return backend ? await backend.getServerUsers() : []; }); +ipcMain.handle('switch-account', async (event, loginName: string) => await handleSwitchAccount(loginName)); ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url)); ipcMain.handle('open-steam-login', async (event, expectedSteamId: string) => { const loginSession = session.fromPartition('persist:steam-login'); await loginSession.clearStorageData({ storages: ['cookies', 'localstorage', 'indexdb'] }); - return new Promise((resolve) => { const loginWindow = new BrowserWindow({ - width: 800, - height: 700, - parent: mainWindow || undefined, - modal: true, - title: 'Login to Steam (Ensure "Remember Me" is checked!)', - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - partition: 'persist:steam-login' - } + width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Steam', + webPreferences: { nodeIntegration: false, contextIsolation: true, partition: 'persist:steam-login' } }); - loginWindow.loadURL('https://steamcommunity.com/login/home/?goto=my/gcpd/730'); - const checkCookie = setInterval(async () => { try { const cookies = await loginSession.cookies.get({ domain: 'steamcommunity.com' }); @@ -543,45 +488,29 @@ ipcMain.handle('open-steam-login', async (event, expectedSteamId: string) => { if (secureCookie) { const steamId = decodeURIComponent(secureCookie.value).split('|')[0]; if (steamId) { - if (expectedSteamId && steamId !== expectedSteamId) { - console.error(`[Auth] ID Mismatch! Expected ${expectedSteamId}, got ${steamId}`); - return; - } + if (expectedSteamId && steamId !== expectedSteamId) return; clearInterval(checkCookie); const cookieString = cookies.map(c => `${c.name}=${c.value}`).join('; '); - console.log(`[Auth] Captured session for SteamID: ${steamId}`); const accounts = store.get('accounts') as Account[]; const accountIndex = accounts.findIndex(a => a.steamId === steamId); if (accountIndex !== -1) { const account = accounts[accountIndex]!; - account.steamLoginSecure = cookieString; - account.autoCheckCooldown = true; - account.authError = false; + account.steamLoginSecure = cookieString; account.autoCheckCooldown = true; account.authError = false; account.sessionUpdatedAt = new Date().toISOString(); - if (account.loginName) { const config = steamClient.extractAccountConfig(account.loginName); if (config) account.loginConfig = config; } - try { - console.log(`[Auth] Performing initial scrape for ${account.personaName}...`); const result = await scrapeCooldown(account.steamId, cookieString); account.lastScrapeTime = new Date().toISOString(); - if (result.isActive && result.expiresAt) { - account.cooldownExpiresAt = result.expiresAt.toISOString(); - } else if (!result.isActive) { - account.cooldownExpiresAt = undefined; - } - } catch (e) { - console.error('[Auth] Initial scrape failed:', e); - } - + account.cooldownExpiresAt = result.isActive && result.expiresAt ? result.expiresAt.toISOString() : undefined; + } catch (e) { } initBackend(); if (backend) await backend.shareAccount(account); - store.set('accounts', accounts); if (mainWindow) mainWindow.webContents.send('accounts-updated', accounts); + updateTrayMenu(); loginWindow.close(); resolve(true); } @@ -589,12 +518,6 @@ ipcMain.handle('open-steam-login', async (event, expectedSteamId: string) => { } } catch (error) { } }, 1000); - - loginWindow.on('closed', () => { - clearInterval(checkCookie); - resolve(false); - }); + loginWindow.on('closed', () => { clearInterval(checkCookie); resolve(false); }); }); }); - -app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); -- 2.49.1 From b7e22b33afd91f6cf6216b5cab253b92e24cad26 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 02:03:58 +0100 Subject: [PATCH 2/2] fix: implement robust multi-format icon resolution for system tray --- frontend/dist-electron/main.js | 42 ++++++++++++++++++++++--------- frontend/electron/main.ts | 45 +++++++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 24 deletions(-) diff --git a/frontend/dist-electron/main.js b/frontend/dist-electron/main.js index 8d35802..fbe30c0 100644 --- a/frontend/dist-electron/main.js +++ b/frontend/dist-electron/main.js @@ -60,19 +60,37 @@ const initBackend = () => { }; // --- System Tray --- const createTray = () => { - const iconPath = path_1.default.join(electron_1.app.getAppPath(), isDev ? 'assets-build/icon.png' : '../assets-build/icon.png'); - if (!fs_1.default.existsSync(iconPath)) - return; - const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 }); - tray = new electron_1.Tray(icon); - tray.setToolTip('Ultimate Ban Tracker'); - tray.on('click', () => { - if (mainWindow) { - mainWindow.show(); - mainWindow.focus(); + const assetsDir = path_1.default.join(__dirname, '..', 'assets-build'); + const possibleIcons = ['icon.svg', 'icon.png']; + let iconPath = ''; + for (const name of possibleIcons) { + const fullPath = path_1.default.join(assetsDir, name); + if (fs_1.default.existsSync(fullPath)) { + iconPath = fullPath; + break; } - }); - updateTrayMenu(); + } + console.log(`[Tray] Attempting to initialize with icon: ${iconPath || 'NONE FOUND'}`); + if (!iconPath) { + console.warn(`[Tray] FAILED: No valid icon found in ${assetsDir}`); + return; + } + try { + const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 }); + tray = new electron_1.Tray(icon); + tray.setToolTip('Ultimate Ban Tracker'); + tray.on('click', () => { + if (mainWindow) { + mainWindow.show(); + mainWindow.focus(); + } + }); + updateTrayMenu(); + console.log(`[Tray] Successfully initialized`); + } + catch (e) { + console.error(`[Tray] Critical error during initialization: ${e.message}`); + } }; const updateTrayMenu = () => { if (!tray) diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index 6850990..f61e378 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -91,19 +91,40 @@ const initBackend = () => { // --- System Tray --- const createTray = () => { - const iconPath = path.join(app.getAppPath(), isDev ? 'assets-build/icon.png' : '../assets-build/icon.png'); - if (!fs.existsSync(iconPath)) return; - - const icon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 }); - tray = new Tray(icon); - tray.setToolTip('Ultimate Ban Tracker'); - tray.on('click', () => { - if (mainWindow) { - mainWindow.show(); - mainWindow.focus(); + const assetsDir = path.join(__dirname, '..', 'assets-build'); + const possibleIcons = ['icon.svg', 'icon.png']; + let iconPath = ''; + + for (const name of possibleIcons) { + const fullPath = path.join(assetsDir, name); + if (fs.existsSync(fullPath)) { + iconPath = fullPath; + break; } - }); - updateTrayMenu(); + } + + console.log(`[Tray] Attempting to initialize with icon: ${iconPath || 'NONE FOUND'}`); + + if (!iconPath) { + console.warn(`[Tray] FAILED: No valid icon found in ${assetsDir}`); + return; + } + + try { + const icon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 }); + tray = new Tray(icon); + tray.setToolTip('Ultimate Ban Tracker'); + tray.on('click', () => { + if (mainWindow) { + mainWindow.show(); + mainWindow.focus(); + } + }); + updateTrayMenu(); + console.log(`[Tray] Successfully initialized`); + } catch (e: any) { + console.error(`[Tray] Critical error during initialization: ${e.message}`); + } }; const updateTrayMenu = () => { -- 2.49.1