diff --git a/frontend/dist-electron/main.js b/frontend/dist-electron/main.js index b3e288c..65c236f 100644 --- a/frontend/dist-electron/main.js +++ b/frontend/dist-electron/main.js @@ -60,18 +60,37 @@ const initBackend = () => { }; // --- System Tray --- const createTray = () => { - 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; + // Try to find the icon in various standard locations + const possiblePaths = [ + path_1.default.join(__dirname, '..', 'assets-build'), // Dev + path_1.default.join(process.resourcesPath, 'assets-build'), // Packaged (External) + path_1.default.join(electron_1.app.getAppPath(), 'dist', 'assets-build'), // Packaged (Internal dist) + path_1.default.join(electron_1.app.getAppPath(), 'assets-build') // Packaged (Internal root) + ]; + let assetsDir = ''; + for (const p of possiblePaths) { + if (fs_1.default.existsSync(p)) { + assetsDir = p; break; } } - if (!iconPath) + const possibleIcons = ['icon.png', 'icon.svg']; + let iconPath = ''; + if (assetsDir) { + for (const name of possibleIcons) { + const fullPath = path_1.default.join(assetsDir, name); + if (fs_1.default.existsSync(fullPath)) { + iconPath = fullPath; + break; + } + } + } + console.log(`[Tray] Resolved assets directory: ${assetsDir || 'NOT FOUND'}`); + console.log(`[Tray] Attempting to initialize with icon: ${iconPath || 'NONE FOUND'}`); + if (!iconPath) { + console.warn(`[Tray] FAILED: No valid icon found in searched paths.`); return; + } try { const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 }); tray = new electron_1.Tray(icon); @@ -169,12 +188,12 @@ const scrapeAccountData = async (account) => { 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); + await backend.pushCooldown(account.steamId, account.cooldownExpiresAt, now.toISOString()); } else { account.cooldownExpiresAt = undefined; if (backend) - await backend.pushCooldown(account.steamId, undefined); + await backend.pushCooldown(account.steamId, undefined, now.toISOString()); } } catch (e) { @@ -231,8 +250,24 @@ const syncAccounts = async (isManual = false) => { exists.sessionUpdatedAt = s.sessionUpdatedAt; hasChanges = true; } - if (s.cooldownExpiresAt && (!exists.cooldownExpiresAt || new Date(s.cooldownExpiresAt) > new Date(exists.cooldownExpiresAt))) { + // Metadata Sync (Pull) + const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0); + const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0); + if (sMetaDate > lMetaDate) { + exists.personaName = s.personaName; + exists.avatar = s.avatar; + exists.vacBanned = s.vacBanned; + exists.gameBans = s.gameBans; + exists.status = (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none'; + exists.lastBanCheck = s.lastMetadataCheck; + hasChanges = true; + } + // Cooldown Sync (Pull) + const sScrapeDate = s.lastScrapeTime ? new Date(s.lastScrapeTime) : new Date(0); + const lScrapeDate = exists.lastScrapeTime ? new Date(exists.lastScrapeTime) : new Date(0); + if (sScrapeDate > lScrapeDate) { exists.cooldownExpiresAt = s.cooldownExpiresAt; + exists.lastScrapeTime = s.lastScrapeTime; hasChanges = true; } if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) { diff --git a/frontend/dist-electron/services/backend.js b/frontend/dist-electron/services/backend.js index d7e23ba..5dbc56d 100644 --- a/frontend/dist-electron/services/backend.js +++ b/frontend/dist-electron/services/backend.js @@ -68,19 +68,23 @@ class BackendService { loginName: account.loginName, steamLoginSecure: account.steamLoginSecure, loginConfig: account.loginConfig, - sessionUpdatedAt: account.sessionUpdatedAt + sessionUpdatedAt: account.sessionUpdatedAt, + lastMetadataCheck: account.lastBanCheck, + lastScrapeTime: account.lastScrapeTime, + cooldownExpiresAt: account.cooldownExpiresAt }, { headers: this.headers }); } catch (e) { console.error('[Backend] Failed to share account'); } } - async pushCooldown(steamId, cooldownExpiresAt) { + async pushCooldown(steamId, cooldownExpiresAt, lastScrapeTime) { if (!this.token) return; try { await axios_1.default.patch(`${this.url}/api/sync/${steamId}/cooldown`, { - cooldownExpiresAt + cooldownExpiresAt, + lastScrapeTime }, { headers: this.headers }); } catch (e) { diff --git a/frontend/dist-electron/services/scraper.js b/frontend/dist-electron/services/scraper.js index 9bbaaa7..ec3db93 100644 --- a/frontend/dist-electron/services/scraper.js +++ b/frontend/dist-electron/services/scraper.js @@ -57,17 +57,23 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => { let expirationDate = undefined; $('table').each((_, table) => { const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get(); - const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration')); + const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration') || h.includes('Cooldown Expiration')); if (expirationIndex !== -1) { - const firstRow = $(table).find('tr').not(':has(th)').first(); - const dateText = firstRow.find('td').eq(expirationIndex).text().trim(); - if (dateText && dateText !== '') { - const cleanDateText = dateText.replace(' GMT', ' UTC'); - const parsed = new Date(cleanDateText); - if (!isNaN(parsed.getTime())) { - expirationDate = parsed; + const rows = $(table).find('tr').not(':has(th)'); + rows.each((_, row) => { + const dateText = $(row).find('td').eq(expirationIndex).text().trim(); + if (dateText && dateText !== '') { + // Steam uses 'GMT' which some JS engines don't parse well, replace with 'UTC' + const cleanDateText = dateText.replace(' GMT', ' UTC'); + const parsed = new Date(cleanDateText); + if (!isNaN(parsed.getTime())) { + // We want the newest expiration date found + if (!expirationDate || parsed > expirationDate) { + expirationDate = parsed; + } + } } - } + }); } }); if (expirationDate && expirationDate.getTime() > Date.now()) { diff --git a/frontend/dist-electron/services/steam-client.js b/frontend/dist-electron/services/steam-client.js index 61a97fb..a1f55e1 100644 --- a/frontend/dist-electron/services/steam-client.js +++ b/frontend/dist-electron/services/steam-client.js @@ -21,7 +21,8 @@ class SteamClientService { if (platform === 'win32') { const possiblePaths = [ 'C:\\Program Files (x86)\\Steam', - 'C:\\Program Files\\Steam' + 'C:\\Program Files\\Steam', + path_1.default.join(process.env.APPDATA || '', 'Steam'), ]; this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null; } @@ -29,7 +30,8 @@ class SteamClientService { const possiblePaths = [ path_1.default.join(home, '.steam/steam'), path_1.default.join(home, '.local/share/Steam'), - path_1.default.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam') + path_1.default.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam'), // Flatpak + path_1.default.join(home, 'snap/steam/common/.steam/steam'), // Snap ]; this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null; } @@ -47,12 +49,35 @@ class SteamClientService { return null; return path_1.default.join(this.steamPath, 'config', 'config.vdf'); } + /** + * Safe Atomic Write: Writes to a temp file and renames it. + * This prevents file corruption if the app crashes during write. + */ + safeWriteVdf(filePath, data) { + const tempPath = `${filePath}.tmp_${Date.now()}`; + const dir = path_1.default.dirname(filePath); + try { + if (!fs_1.default.existsSync(dir)) + fs_1.default.mkdirSync(dir, { recursive: true }); + const vdfContent = (0, simple_vdf_1.stringify)(data); + fs_1.default.writeFileSync(tempPath, vdfContent, 'utf-8'); + // Atomic rename + fs_1.default.renameSync(tempPath, filePath); + } + catch (e) { + console.error(`[SteamClient] Atomic write failed for ${filePath}: ${e.message}`); + if (fs_1.default.existsSync(tempPath)) + fs_1.default.unlinkSync(tempPath); + throw e; + } + } startWatching(callback) { this.onAccountsChanged = callback; const loginUsersPath = this.getLoginUsersPath(); if (loginUsersPath && fs_1.default.existsSync(loginUsersPath)) { this.readLocalAccounts(); - chokidar_1.default.watch(loginUsersPath, { persistent: true }).on('change', () => { + chokidar_1.default.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => { + console.log(`[SteamClient] loginusers.vdf changed, re-scanning...`); this.readLocalAccounts(); }); } @@ -63,16 +88,20 @@ class SteamClientService { return; try { const content = fs_1.default.readFileSync(filePath, 'utf-8'); + if (!content.trim()) + return; // Empty file const data = (0, simple_vdf_1.parse)(content); if (!data || !data.users) return; const accounts = []; for (const [steamId64, userData] of Object.entries(data.users)) { const user = userData; + if (!user || !user.AccountName) + continue; accounts.push({ steamId: steamId64, accountName: user.AccountName, - personaName: user.PersonaName, + personaName: user.PersonaName || user.AccountName, timestamp: parseInt(user.Timestamp) || 0 }); } @@ -91,32 +120,38 @@ class SteamClientService { const content = fs_1.default.readFileSync(configPath, 'utf-8'); const data = (0, simple_vdf_1.parse)(content); const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts; - if (accounts && accounts[accountName]) { - return accounts[accountName]; - } + return (accounts && accounts[accountName]) ? accounts[accountName] : null; } catch (e) { console.error('[SteamClient] Failed to extract config.vdf data'); + return null; } - return null; } injectAccountConfig(accountName, accountData) { const configPath = this.getConfigVdfPath(); if (!configPath) return; - // Create directory if it doesn't exist - const configDir = path_1.default.dirname(configPath); - if (!fs_1.default.existsSync(configDir)) - fs_1.default.mkdirSync(configDir, { recursive: true }); - let data = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } }; + let data = { + InstallConfigStore: { + Software: { + Valve: { + Steam: { + Accounts: {} + } + } + } + } + }; if (fs_1.default.existsSync(configPath)) { try { const content = fs_1.default.readFileSync(configPath, 'utf-8'); - data = (0, simple_vdf_1.parse)(content); + const parsed = (0, simple_vdf_1.parse)(content); + if (parsed && typeof parsed === 'object') + data = parsed; } catch (e) { } } - // Ensure structure exists + // Ensure safe nesting if (!data.InstallConfigStore) data.InstallConfigStore = {}; if (!data.InstallConfigStore.Software) @@ -129,25 +164,22 @@ class SteamClientService { data.InstallConfigStore.Software.Valve.Steam.Accounts = {}; data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData; try { - fs_1.default.writeFileSync(configPath, (0, simple_vdf_1.stringify)(data)); - console.log(`[SteamClient] Injected login config for ${accountName} into config.vdf`); - } - catch (e) { - console.error('[SteamClient] Failed to write config.vdf'); + this.safeWriteVdf(configPath, data); + console.log(`[SteamClient] Safely injected session for ${accountName}`); } + catch (e) { } } async setAutoLoginUser(accountName, accountConfig, steamId) { const platform = os_1.default.platform(); const loginUsersPath = this.getLoginUsersPath(); if (loginUsersPath) { - const configDir = path_1.default.dirname(loginUsersPath); - if (!fs_1.default.existsSync(configDir)) - fs_1.default.mkdirSync(configDir, { recursive: true }); let data = { users: {} }; if (fs_1.default.existsSync(loginUsersPath)) { try { const content = fs_1.default.readFileSync(loginUsersPath, 'utf-8'); - data = (0, simple_vdf_1.parse)(content); + const parsed = (0, simple_vdf_1.parse)(content); + if (parsed && parsed.users) + data = parsed; } catch (e) { } } @@ -156,7 +188,7 @@ class SteamClientService { let found = false; for (const [id, user] of Object.entries(data.users)) { const u = user; - if (u.AccountName.toLowerCase() === accountName.toLowerCase()) { + if (u.AccountName?.toLowerCase() === accountName.toLowerCase()) { u.mostrecent = "1"; u.RememberPassword = "1"; u.AllowAutoLogin = "1"; @@ -169,8 +201,8 @@ class SteamClientService { u.mostrecent = "0"; } } - if (!found && steamId) { - console.log(`[SteamClient] Provisioning user ${accountName} into loginusers.vdf`); + if (!found && steamId && accountName) { + console.log(`[SteamClient] Provisioning new user profile for ${accountName}`); data.users[steamId] = { AccountName: accountName, PersonaName: accountName, @@ -184,51 +216,53 @@ class SteamClientService { }; } try { - fs_1.default.writeFileSync(loginUsersPath, (0, simple_vdf_1.stringify)(data)); - } - catch (e) { - console.error('[SteamClient] Failed to write loginusers.vdf'); + this.safeWriteVdf(loginUsersPath, data); } + catch (e) { } } - if (accountConfig) { + if (accountConfig && accountName) { this.injectAccountConfig(accountName, accountConfig); } + // --- Linux Registry / Registry.vdf Hardening --- if (platform === 'linux') { const regLocations = [ path_1.default.join(os_1.default.homedir(), '.steam', 'registry.vdf'), path_1.default.join(os_1.default.homedir(), '.steam', 'steam', 'registry.vdf') ]; for (const regPath of regLocations) { - let regData = { Registry: { HKCU: { Software: { Valve: { Steam: {} } } } } }; + if (!fs_1.default.existsSync(path_1.default.dirname(regPath))) + continue; + let regData = { Registry: { HKCU: { Software: { Valve: { Steam: { + AutoLoginUser: "", + RememberPassword: "1", + AlreadyLoggedIn: "1" + } } } } } }; if (fs_1.default.existsSync(regPath)) { try { const content = fs_1.default.readFileSync(regPath, 'utf-8'); - regData = (0, simple_vdf_1.parse)(content); + const parsed = (0, simple_vdf_1.parse)(content); + if (parsed && typeof parsed === 'object') + regData = parsed; } catch (e) { } } - else { - const regDir = path_1.default.dirname(regPath); - if (!fs_1.default.existsSync(regDir)) - fs_1.default.mkdirSync(regDir, { recursive: true }); - } - const setPath = (obj, keys, val) => { + // Deep merge helper + const ensurePath = (obj, keys) => { let curr = obj; - for (let i = 0; i < keys.length - 1; i++) { - if (!curr[keys[i]]) - curr[keys[i]] = {}; - curr = curr[keys[i]]; + for (const key of keys) { + if (!curr[key] || typeof curr[key] !== 'object') + curr[key] = {}; + curr = curr[key]; } - curr[keys[keys.length - 1]] = val; + return curr; }; - const steamReg = ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']; - setPath(regData, [...steamReg, 'AutoLoginUser'], accountName); - setPath(regData, [...steamReg, 'RememberPassword'], "1"); - setPath(regData, [...steamReg, 'AlreadyLoggedIn'], "1"); - setPath(regData, [...steamReg, 'WantsOfflineMode'], "0"); + const steamKey = ensurePath(regData, ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']); + steamKey.AutoLoginUser = accountName; + steamKey.RememberPassword = "1"; + steamKey.AlreadyLoggedIn = "1"; + steamKey.WantsOfflineMode = "0"; try { - fs_1.default.writeFileSync(regPath, (0, simple_vdf_1.stringify)(regData)); - console.log(`[SteamClient] Registry updated: ${regPath}`); + this.safeWriteVdf(regPath, regData); } catch (e) { } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fd20399..985ed93 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ultimate-ban-tracker-desktop", - "version": "1.3.0", + "version": "1.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ultimate-ban-tracker-desktop", - "version": "1.3.0", + "version": "1.3.2", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@emotion/react": "^11.14.0", diff --git a/frontend/package.json b/frontend/package.json index 0ddea69..dd33e3b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "ultimate-ban-tracker-desktop", "description": "Professional Steam Account Manager & Ban Tracker", - "version": "1.3.0", + "version": "1.3.2", "author": "Nils Pukropp ", "homepage": "https://narl.io", "license": "SEE LICENSE IN LICENSE",