6 Commits

Author SHA1 Message Date
eca3a728fc Merge pull request 'release/v1.3.2' (#9) from release/v1.3.2 into main
All checks were successful
Build and Release / build (push) Successful in 5m33s
Reviewed-on: #9
2026-02-21 04:39:16 +01:00
60b3dd1ca1 Merge pull request 'release/v1.3.1' (#8) from release/v1.3.1 into main
Some checks failed
Build and Release / build (push) Failing after 5m36s
Reviewed-on: #8
2026-02-21 04:25:16 +01:00
589acdebcb Merge pull request 'chore: bump version to 1.3.0' (#7) from release/v1.3.0 into main
All checks were successful
Build and Release / build (push) Successful in 5m39s
Reviewed-on: #7
2026-02-21 03:37:38 +01:00
4037d7bce3 Merge pull request 'chore: update application title in index.html from frontend to Ultimate Ban Tracker' (#6) from release/v1.3.0 into main
Some checks failed
Build and Release / build (push) Has been cancelled
Reviewed-on: #6
2026-02-21 03:36:37 +01:00
5d611fd8be Merge pull request 'feat: implement comprehensive admin dashboard for server management and user oversight' (#5) from release/v1.3.0 into main
Some checks failed
Build and Release / build (push) Has been cancelled
Reviewed-on: #5
2026-02-21 03:35:22 +01:00
88d2a2133c Merge pull request 'chore: bump version to 1.2.0 and commit recent fixes/features including tray and auth isolation' (#4) from release/v1.2.0 into main
All checks were successful
Build and Release / build (push) Successful in 5m37s
Reviewed-on: #4
2026-02-21 03:21:45 +01:00
17 changed files with 150 additions and 357 deletions

View File

@@ -1,33 +1,4 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs> <rect width="512" height="512" rx="64" fill="#171A21"/>
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse"> <path d="M256 64C150.13 64 64 150.13 64 256C64 361.87 150.13 448 256 448C361.87 448 448 361.87 448 256C448 150.13 375.73 64 256 64ZM256 405.33C173.6 405.33 106.67 338.4 106.67 256C106.67 221.33 118.4 189.33 138.13 164.27L347.73 373.87C322.67 393.6 290.67 405.33 256 405.33ZM373.87 347.73L164.27 138.13C189.33 118.4 221.33 106.67 256 106.67C338.4 106.67 405.33 173.6 405.33 256C405.33 290.67 393.6 322.67 373.87 347.73Z" fill="#66C0F4"/>
<stop offset="0%" stop-color="#1B2838"/>
<stop offset="100%" stop-color="#101419"/>
</linearGradient>
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#66C0F4"/>
<stop offset="100%" stop-color="#1A9FFF"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="15" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<!-- Outer Rounded Container -->
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
<rect x="2" y="2" width="508" height="508" rx="98" stroke="white" stroke-opacity="0.05" stroke-width="4"/>
<!-- Tracking Ring (Detailed) -->
<circle cx="256" cy="256" r="180" stroke="#66C0F4" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
<!-- Central Shield Symbol -->
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
<!-- "Ban" Intersect (Stylized Cross) -->
<path d="M210 220L302 312M302 220L210 312" stroke="#1B2838" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
<!-- Glass Highlight -->
<path d="M100 100C150 60 362 60 412 100" stroke="white" stroke-opacity="0.1" stroke-width="20" stroke-linecap="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 604 B

View File

@@ -1,21 +0,0 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#EFF1F5"/>
<stop offset="100%" stop-color="#DCE0E8"/>
</linearGradient>
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#1E66F5"/>
<stop offset="100%" stop-color="#179299"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="15" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
<circle cx="256" cy="256" r="180" stroke="#1E66F5" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
<path d="M210 220L302 312M302 220L210 312" stroke="#EFF1F5" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,21 +0,0 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#1E1E2E"/>
<stop offset="100%" stop-color="#11111B"/>
</linearGradient>
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#B4BEFE"/>
<stop offset="100%" stop-color="#89B4FA"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="15" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
<circle cx="256" cy="256" r="180" stroke="#B4BEFE" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
<path d="M210 220L302 312M302 220L210 312" stroke="#1E1E2E" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,21 +0,0 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#3B4252"/>
<stop offset="100%" stop-color="#2E3440"/>
</linearGradient>
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#88C0D0"/>
<stop offset="100%" stop-color="#81A1C1"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="15" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
<circle cx="256" cy="256" r="180" stroke="#88C0D0" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
<path d="M210 220L302 312M302 220L210 312" stroke="#3B4252" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,21 +0,0 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#1B2838"/>
<stop offset="100%" stop-color="#101419"/>
</linearGradient>
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#66C0F4"/>
<stop offset="100%" stop-color="#1A9FFF"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="15" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
<circle cx="256" cy="256" r="180" stroke="#66C0F4" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
<path d="M210 220L302 312M302 220L210 312" stroke="#1B2838" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,21 +0,0 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#1A1B26"/>
<stop offset="100%" stop-color="#10101A"/>
</linearGradient>
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#7AA2F7"/>
<stop offset="100%" stop-color="#3D59A1"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="15" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
<circle cx="256" cy="256" r="180" stroke="#7AA2F7" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
<path d="M210 220L302 312M302 220L210 312" stroke="#1A1B26" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -99,14 +99,7 @@ const createTray = () => {
mainWindow.show(); mainWindow.show();
mainWindow.focus(); mainWindow.focus();
} }); } });
// Load initial themed icon updateTrayMenu();
const config = store.get('serverConfig');
if (config?.theme) {
setAppIcon(config.theme);
}
else {
updateTrayMenu(); // Fallback to refresh menu
}
} }
catch (e) { } catch (e) { }
}; };
@@ -134,21 +127,6 @@ const updateTrayMenu = () => {
]); ]);
tray.setContextMenu(contextMenu); tray.setContextMenu(contextMenu);
}; };
const setAppIcon = (themeName = 'steam') => {
const assetsDir = path_1.default.join(__dirname, '..', 'assets-build', 'icons');
const iconPath = path_1.default.join(assetsDir, `${themeName}.svg`);
if (!fs_1.default.existsSync(iconPath))
return;
const icon = electron_1.nativeImage.createFromPath(iconPath);
// Update Tray
if (tray) {
tray.setImage(icon.resize({ width: 16, height: 16 }));
}
// Update Main Window
if (mainWindow) {
mainWindow.setIcon(icon);
}
};
// --- Steam Logic --- // --- Steam Logic ---
const killSteam = async () => { const killSteam = async () => {
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -219,13 +197,9 @@ const scrapeAccountData = async (account) => {
} }
} }
catch (e) { catch (e) {
if (e instanceof scraper_1.SteamAuthError) { if (e.message.includes('cookie') || e.message.includes('Sign In'))
account.authError = true; account.authError = true;
} }
else {
console.error(`[Scraper] Temporary error for ${account.personaName}: ${e.message}`);
}
}
} }
if (backend && !account._id.startsWith('shared_')) { if (backend && !account._id.startsWith('shared_')) {
await backend.shareAccount(account); await backend.shareAccount(account);
@@ -263,16 +237,7 @@ const syncAccounts = async (isManual = false) => {
else { else {
const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0); const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0); const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
// 1. SENSITIVE DATA SYNC (Credentials) if (sDate > lDate) {
const sSessionDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
const lSessionDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
const isLocalAccount = !exists._id.startsWith('shared_');
const isLocalSessionHealthy = exists.steamLoginSecure && !exists.authError;
// SMART OVERWRITE LOGIC:
// - If it's a remote shared account: Newest wins.
// - If it's a LOCAL account: Only overwrite if our local session is broken/missing.
const shouldOverwriteCredentials = !isLocalAccount ? (sSessionDate > lSessionDate) : (!isLocalSessionHealthy && sSessionDate > lSessionDate);
if (shouldOverwriteCredentials) {
if (s.loginName) if (s.loginName)
exists.loginName = s.loginName; exists.loginName = s.loginName;
if (s.loginConfig) if (s.loginConfig)
@@ -285,7 +250,7 @@ const syncAccounts = async (isManual = false) => {
exists.sessionUpdatedAt = s.sessionUpdatedAt; exists.sessionUpdatedAt = s.sessionUpdatedAt;
hasChanges = true; hasChanges = true;
} }
// 2. Metadata Sync (Pull) - Always "Newest Wins" // Metadata Sync (Pull)
const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0); const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0);
const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0); const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0);
if (sMetaDate > lMetaDate) { if (sMetaDate > lMetaDate) {
@@ -623,11 +588,6 @@ electron_1.ipcMain.handle('admin-delete-user', async (event, userId) => { initBa
electron_1.ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; }); electron_1.ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; });
electron_1.ipcMain.handle('admin-remove-account', async (event, steamId) => { initBackend(); if (backend) electron_1.ipcMain.handle('admin-remove-account', async (event, steamId) => { initBackend(); if (backend)
await backend.forceRemoveAccount(steamId); return true; }); await backend.forceRemoveAccount(steamId); return true; });
electron_1.ipcMain.handle('force-sync', async () => { await syncAccounts(true); return true; });
electron_1.ipcMain.handle('update-app-icon', (event, themeName) => {
setAppIcon(themeName);
return true;
});
electron_1.ipcMain.handle('switch-account', async (event, loginName) => { electron_1.ipcMain.handle('switch-account', async (event, loginName) => {
if (!loginName) if (!loginName)
return false; return false;

View File

@@ -11,7 +11,6 @@ electron_1.contextBridge.exposeInMainWorld('electronAPI', {
revokeAccountAccess: (steamId, targetSteamId) => electron_1.ipcRenderer.invoke('revoke-account-access', steamId, targetSteamId), revokeAccountAccess: (steamId, targetSteamId) => electron_1.ipcRenderer.invoke('revoke-account-access', steamId, targetSteamId),
revokeAllAccountAccess: (steamId) => electron_1.ipcRenderer.invoke('revoke-all-account-access', steamId), revokeAllAccountAccess: (steamId) => electron_1.ipcRenderer.invoke('revoke-all-account-access', steamId),
openExternal: (url) => electron_1.ipcRenderer.invoke('open-external', url), openExternal: (url) => electron_1.ipcRenderer.invoke('open-external', url),
updateAppIcon: (theme) => electron_1.ipcRenderer.invoke('update-app-icon', theme),
openSteamAppLogin: () => electron_1.ipcRenderer.invoke('open-steam-app-login'), openSteamAppLogin: () => electron_1.ipcRenderer.invoke('open-steam-app-login'),
openSteamLogin: (steamId) => electron_1.ipcRenderer.invoke('open-steam-login', steamId), openSteamLogin: (steamId) => electron_1.ipcRenderer.invoke('open-steam-login', steamId),
// Server Config & Auth // Server Config & Auth

View File

@@ -36,17 +36,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.scrapeCooldown = exports.SteamAuthError = void 0; exports.scrapeCooldown = void 0;
const axios_1 = __importDefault(require("axios")); const axios_1 = __importDefault(require("axios"));
const cheerio = __importStar(require("cheerio")); const cheerio = __importStar(require("cheerio"));
// Custom error to identify session death
class SteamAuthError extends Error {
constructor(message) {
super(message);
this.name = "SteamAuthError";
}
}
exports.SteamAuthError = SteamAuthError;
const scrapeCooldown = async (steamId, steamLoginSecure) => { const scrapeCooldown = async (steamId, steamLoginSecure) => {
const url = `https://steamcommunity.com/profiles/${steamId}/gcpd/730?tab=matchmaking`; const url = `https://steamcommunity.com/profiles/${steamId}/gcpd/730?tab=matchmaking`;
try { try {
@@ -55,17 +47,13 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => {
'Cookie': steamLoginSecure, 'Cookie': steamLoginSecure,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}, },
timeout: 10000, timeout: 10000
validateStatus: (status) => status < 500 // Allow redirects to handle them manually
}); });
// If Steam redirects us to the login page, the cookie is dead
if (response.data.includes('Sign In') || response.request.path.includes('/login')) {
throw new SteamAuthError('Invalid or expired steamLoginSecure cookie');
}
const $ = cheerio.load(response.data); const $ = cheerio.load(response.data);
if (!response.data.includes('Personal Game Data')) { if (response.data.includes('Sign In') || !response.data.includes('Personal Game Data')) {
throw new SteamAuthError('Session invalid: Personal Game Data not accessible'); throw new Error('Invalid or expired steamLoginSecure cookie');
} }
// 1. Locate the specific table containing cooldown info
let expirationDate = undefined; let expirationDate = undefined;
$('table').each((_, table) => { $('table').each((_, table) => {
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get(); const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
@@ -75,18 +63,25 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => {
rows.each((_, row) => { rows.each((_, row) => {
const dateText = $(row).find('td').eq(expirationIndex).text().trim(); const dateText = $(row).find('td').eq(expirationIndex).text().trim();
if (dateText && dateText !== '') { if (dateText && dateText !== '') {
// Steam uses 'GMT' which some JS engines don't parse well, replace with 'UTC'
const cleanDateText = dateText.replace(' GMT', ' UTC'); const cleanDateText = dateText.replace(' GMT', ' UTC');
const parsed = new Date(cleanDateText); const parsed = new Date(cleanDateText);
if (!isNaN(parsed.getTime())) { if (!isNaN(parsed.getTime())) {
if (!expirationDate || parsed > expirationDate) // We want the newest expiration date found
if (!expirationDate || parsed > expirationDate) {
expirationDate = parsed; expirationDate = parsed;
} }
} }
}
}); });
} }
}); });
if (expirationDate && expirationDate.getTime() > Date.now()) { if (expirationDate && expirationDate.getTime() > Date.now()) {
return { isActive: true, expiresAt: expirationDate }; console.log(`[Scraper] Found active cooldown until: ${expirationDate.toISOString()}`);
return {
isActive: true,
expiresAt: expirationDate
};
} }
const content = $('#personal_game_data_content').text(); const content = $('#personal_game_data_content').text();
if (content.includes('Competitive Cooldown') || content.includes('Your account is currently')) { if (content.includes('Competitive Cooldown') || content.includes('Your account is currently')) {
@@ -95,10 +90,8 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => {
return { isActive: false }; return { isActive: false };
} }
catch (error) { catch (error) {
if (error instanceof SteamAuthError) console.error(`[Scraper] Error for ${steamId}:`, error.message);
throw error; throw error;
console.error(`[Scraper] Network/Internal Error for ${steamId}:`, error.message);
throw error; // Generic errors don't trigger re-auth
} }
}; };
exports.scrapeCooldown = scrapeCooldown; exports.scrapeCooldown = scrapeCooldown;

View File

@@ -49,6 +49,10 @@ class SteamClientService {
return null; return null;
return path_1.default.join(this.steamPath, 'config', 'config.vdf'); 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) { safeWriteVdf(filePath, data) {
const tempPath = `${filePath}.tmp_${Date.now()}`; const tempPath = `${filePath}.tmp_${Date.now()}`;
const dir = path_1.default.dirname(filePath); const dir = path_1.default.dirname(filePath);
@@ -57,6 +61,7 @@ class SteamClientService {
fs_1.default.mkdirSync(dir, { recursive: true }); fs_1.default.mkdirSync(dir, { recursive: true });
const vdfContent = (0, simple_vdf_1.stringify)(data); const vdfContent = (0, simple_vdf_1.stringify)(data);
fs_1.default.writeFileSync(tempPath, vdfContent, 'utf-8'); fs_1.default.writeFileSync(tempPath, vdfContent, 'utf-8');
// Atomic rename
fs_1.default.renameSync(tempPath, filePath); fs_1.default.renameSync(tempPath, filePath);
} }
catch (e) { catch (e) {
@@ -72,6 +77,7 @@ class SteamClientService {
if (loginUsersPath && fs_1.default.existsSync(loginUsersPath)) { if (loginUsersPath && fs_1.default.existsSync(loginUsersPath)) {
this.readLocalAccounts(); this.readLocalAccounts();
chokidar_1.default.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => { chokidar_1.default.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => {
console.log(`[SteamClient] loginusers.vdf changed, re-scanning...`);
this.readLocalAccounts(); this.readLocalAccounts();
}); });
} }
@@ -83,7 +89,7 @@ class SteamClientService {
try { try {
const content = fs_1.default.readFileSync(filePath, 'utf-8'); const content = fs_1.default.readFileSync(filePath, 'utf-8');
if (!content.trim()) if (!content.trim())
return; return; // Empty file
const data = (0, simple_vdf_1.parse)(content); const data = (0, simple_vdf_1.parse)(content);
if (!data || !data.users) if (!data || !data.users)
return; return;
@@ -93,7 +99,8 @@ class SteamClientService {
if (!user || !user.AccountName) if (!user || !user.AccountName)
continue; continue;
accounts.push({ accounts.push({
steamId: steamId64, accountName: user.AccountName, steamId: steamId64,
accountName: user.AccountName,
personaName: user.PersonaName || user.AccountName, personaName: user.PersonaName || user.AccountName,
timestamp: parseInt(user.Timestamp) || 0 timestamp: parseInt(user.Timestamp) || 0
}); });
@@ -101,7 +108,9 @@ class SteamClientService {
if (this.onAccountsChanged) if (this.onAccountsChanged)
this.onAccountsChanged(accounts); this.onAccountsChanged(accounts);
} }
catch (error) { } catch (error) {
console.error('[SteamClient] Error parsing loginusers.vdf:', error);
}
} }
extractAccountConfig(accountName) { extractAccountConfig(accountName) {
const configPath = this.getConfigVdfPath(); const configPath = this.getConfigVdfPath();
@@ -114,6 +123,7 @@ class SteamClientService {
return (accounts && accounts[accountName]) ? accounts[accountName] : null; return (accounts && accounts[accountName]) ? accounts[accountName] : null;
} }
catch (e) { catch (e) {
console.error('[SteamClient] Failed to extract config.vdf data');
return null; return null;
} }
} }
@@ -121,7 +131,17 @@ class SteamClientService {
const configPath = this.getConfigVdfPath(); const configPath = this.getConfigVdfPath();
if (!configPath) if (!configPath)
return; return;
let data = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } }; let data = {
InstallConfigStore: {
Software: {
Valve: {
Steam: {
Accounts: {}
}
}
}
}
};
if (fs_1.default.existsSync(configPath)) { if (fs_1.default.existsSync(configPath)) {
try { try {
const content = fs_1.default.readFileSync(configPath, 'utf-8'); const content = fs_1.default.readFileSync(configPath, 'utf-8');
@@ -131,23 +151,18 @@ class SteamClientService {
} }
catch (e) { } catch (e) { }
} }
const ensurePath = (obj, keys) => { // Ensure safe nesting
let curr = obj; if (!data.InstallConfigStore)
for (const key of keys) { data.InstallConfigStore = {};
if (!curr[key] || typeof curr[key] !== 'object') if (!data.InstallConfigStore.Software)
curr[key] = {}; data.InstallConfigStore.Software = {};
curr = curr[key]; if (!data.InstallConfigStore.Software.Valve)
} data.InstallConfigStore.Software.Valve = {};
return curr; if (!data.InstallConfigStore.Software.Valve.Steam)
}; data.InstallConfigStore.Software.Valve.Steam = {};
const steamAccounts = ensurePath(data, ['InstallConfigStore', 'Software', 'Valve', 'Steam', 'Accounts']); if (!data.InstallConfigStore.Software.Valve.Steam.Accounts)
// FAILPROOF: Force crucial flags that Steam uses to decide session validity data.InstallConfigStore.Software.Valve.Steam.Accounts = {};
steamAccounts[accountName] = { data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData;
...accountData,
RememberPassword: "1",
AllowAutoLogin: "1",
Timestamp: Math.floor(Date.now() / 1000).toString()
};
try { try {
this.safeWriteVdf(configPath, data); this.safeWriteVdf(configPath, data);
console.log(`[SteamClient] Safely injected session for ${accountName}`); console.log(`[SteamClient] Safely injected session for ${accountName}`);
@@ -205,7 +220,6 @@ class SteamClientService {
} }
catch (e) { } catch (e) { }
} }
// Injection of the actual authentication blob
if (accountConfig && accountName) { if (accountConfig && accountName) {
this.injectAccountConfig(accountName, accountConfig); this.injectAccountConfig(accountName, accountConfig);
} }
@@ -218,7 +232,11 @@ class SteamClientService {
for (const regPath of regLocations) { for (const regPath of regLocations) {
if (!fs_1.default.existsSync(path_1.default.dirname(regPath))) if (!fs_1.default.existsSync(path_1.default.dirname(regPath)))
continue; continue;
let regData = { Registry: { HKCU: { Software: { Valve: { Steam: { AutoLoginUser: "", RememberPassword: "1", AlreadyLoggedIn: "1" } } } } } }; let regData = { Registry: { HKCU: { Software: { Valve: { Steam: {
AutoLoginUser: "",
RememberPassword: "1",
AlreadyLoggedIn: "1"
} } } } } };
if (fs_1.default.existsSync(regPath)) { if (fs_1.default.existsSync(regPath)) {
try { try {
const content = fs_1.default.readFileSync(regPath, 'utf-8'); const content = fs_1.default.readFileSync(regPath, 'utf-8');
@@ -228,6 +246,7 @@ class SteamClientService {
} }
catch (e) { } catch (e) { }
} }
// Deep merge helper
const ensurePath = (obj, keys) => { const ensurePath = (obj, keys) => {
let curr = obj; let curr = obj;
for (const key of keys) { for (const key of keys) {

View File

@@ -7,7 +7,7 @@ import axios from 'axios';
import fs from 'fs'; import fs from 'fs';
import { pathToFileURL } from 'url'; import { pathToFileURL } from 'url';
import { fetchProfileData, scrapeBanStatus } from './services/steam-web'; import { fetchProfileData, scrapeBanStatus } from './services/steam-web';
import { scrapeCooldown, SteamAuthError } from './services/scraper'; import { scrapeCooldown } from './services/scraper';
import { steamClient, LocalSteamAccount } from './services/steam-client'; import { steamClient, LocalSteamAccount } from './services/steam-client';
import { BackendService } from './services/backend'; import { BackendService } from './services/backend';
@@ -135,14 +135,7 @@ const createTray = () => {
tray = new Tray(icon); tray = new Tray(icon);
tray.setToolTip('Ultimate Ban Tracker'); tray.setToolTip('Ultimate Ban Tracker');
tray.on('click', () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } }); tray.on('click', () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } });
updateTrayMenu();
// Load initial themed icon
const config = store.get('serverConfig');
if (config?.theme) {
setAppIcon(config.theme);
} else {
updateTrayMenu(); // Fallback to refresh menu
}
} catch (e) { } } catch (e) { }
}; };
@@ -169,25 +162,6 @@ const updateTrayMenu = () => {
tray.setContextMenu(contextMenu); tray.setContextMenu(contextMenu);
}; };
const setAppIcon = (themeName: string = 'steam') => {
const assetsDir = path.join(__dirname, '..', 'assets-build', 'icons');
const iconPath = path.join(assetsDir, `${themeName}.svg`);
if (!fs.existsSync(iconPath)) return;
const icon = nativeImage.createFromPath(iconPath);
// Update Tray
if (tray) {
tray.setImage(icon.resize({ width: 16, height: 16 }));
}
// Update Main Window
if (mainWindow) {
mainWindow.setIcon(icon);
}
};
// --- Steam Logic --- // --- Steam Logic ---
const killSteam = async () => { const killSteam = async () => {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
@@ -247,11 +221,7 @@ const scrapeAccountData = async (account: Account) => {
if (backend) await backend.pushCooldown(account.steamId, undefined, now.toISOString()); if (backend) await backend.pushCooldown(account.steamId, undefined, now.toISOString());
} }
} catch (e: any) { } catch (e: any) {
if (e instanceof SteamAuthError) { if (e.message.includes('cookie') || e.message.includes('Sign In')) account.authError = true;
account.authError = true;
} else {
console.error(`[Scraper] Temporary error for ${account.personaName}: ${e.message}`);
}
} }
} }
if (backend && !account._id.startsWith('shared_')) { if (backend && !account._id.startsWith('shared_')) {
@@ -290,31 +260,15 @@ const syncAccounts = async (isManual = false) => {
} else { } else {
const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0); const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0); const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
// 1. SENSITIVE DATA SYNC (Credentials) if (sDate > lDate) {
const sSessionDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
const lSessionDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
const isLocalAccount = !exists._id.startsWith('shared_');
const isLocalSessionHealthy = exists.steamLoginSecure && !exists.authError;
// SMART OVERWRITE LOGIC:
// - If it's a remote shared account: Newest wins.
// - If it's a LOCAL account: Only overwrite if our local session is broken/missing.
const shouldOverwriteCredentials = !isLocalAccount ? (sSessionDate > lSessionDate) : (!isLocalSessionHealthy && sSessionDate > lSessionDate);
if (shouldOverwriteCredentials) {
if (s.loginName) exists.loginName = s.loginName; if (s.loginName) exists.loginName = s.loginName;
if (s.loginConfig) exists.loginConfig = s.loginConfig; if (s.loginConfig) exists.loginConfig = s.loginConfig;
if (s.steamLoginSecure) { if (s.steamLoginSecure) { exists.steamLoginSecure = s.steamLoginSecure; exists.autoCheckCooldown = true; exists.authError = false; }
exists.steamLoginSecure = s.steamLoginSecure;
exists.autoCheckCooldown = true;
exists.authError = false;
}
exists.sessionUpdatedAt = s.sessionUpdatedAt; exists.sessionUpdatedAt = s.sessionUpdatedAt;
hasChanges = true; hasChanges = true;
} }
// 2. Metadata Sync (Pull) - Always "Newest Wins" // Metadata Sync (Pull)
const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0); const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0);
const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0); const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0);
if (sMetaDate > lMetaDate) { if (sMetaDate > lMetaDate) {
@@ -617,13 +571,6 @@ ipcMain.handle('admin-delete-user', async (event, userId: string) => { initBacke
ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; }); ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; });
ipcMain.handle('admin-remove-account', async (event, steamId: string) => { initBackend(); if (backend) await backend.forceRemoveAccount(steamId); return true; }); ipcMain.handle('admin-remove-account', async (event, steamId: string) => { initBackend(); if (backend) await backend.forceRemoveAccount(steamId); return true; });
ipcMain.handle('force-sync', async () => { await syncAccounts(true); return true; });
ipcMain.handle('update-app-icon', (event, themeName: string) => {
setAppIcon(themeName);
return true;
});
ipcMain.handle('switch-account', async (event, loginName: string) => { ipcMain.handle('switch-account', async (event, loginName: string) => {
if (!loginName) return false; if (!loginName) return false;
try { try {

View File

@@ -10,7 +10,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
revokeAccountAccess: (steamId: string, targetSteamId: string) => ipcRenderer.invoke('revoke-account-access', steamId, targetSteamId), revokeAccountAccess: (steamId: string, targetSteamId: string) => ipcRenderer.invoke('revoke-account-access', steamId, targetSteamId),
revokeAllAccountAccess: (steamId: string) => ipcRenderer.invoke('revoke-all-account-access', steamId), revokeAllAccountAccess: (steamId: string) => ipcRenderer.invoke('revoke-all-account-access', steamId),
openExternal: (url: string) => ipcRenderer.invoke('open-external', url), openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
updateAppIcon: (theme: string) => ipcRenderer.invoke('update-app-icon', theme),
openSteamAppLogin: () => ipcRenderer.invoke('open-steam-app-login'), openSteamAppLogin: () => ipcRenderer.invoke('open-steam-app-login'),
openSteamLogin: (steamId: string) => ipcRenderer.invoke('open-steam-login', steamId), openSteamLogin: (steamId: string) => ipcRenderer.invoke('open-steam-login', steamId),

View File

@@ -6,14 +6,6 @@ export interface CooldownData {
expiresAt?: Date; expiresAt?: Date;
} }
// Custom error to identify session death
export class SteamAuthError extends Error {
constructor(message: string) {
super(message);
this.name = "SteamAuthError";
}
}
export const scrapeCooldown = async (steamId: string, steamLoginSecure: string): Promise<CooldownData> => { export const scrapeCooldown = async (steamId: string, steamLoginSecure: string): Promise<CooldownData> => {
const url = `https://steamcommunity.com/profiles/${steamId}/gcpd/730?tab=matchmaking`; const url = `https://steamcommunity.com/profiles/${steamId}/gcpd/730?tab=matchmaking`;
@@ -23,21 +15,16 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
'Cookie': steamLoginSecure, 'Cookie': steamLoginSecure,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}, },
timeout: 10000, timeout: 10000
validateStatus: (status) => status < 500 // Allow redirects to handle them manually
}); });
// If Steam redirects us to the login page, the cookie is dead
if (response.data.includes('Sign In') || response.request.path.includes('/login')) {
throw new SteamAuthError('Invalid or expired steamLoginSecure cookie');
}
const $ = cheerio.load(response.data); const $ = cheerio.load(response.data);
if (!response.data.includes('Personal Game Data')) { if (response.data.includes('Sign In') || !response.data.includes('Personal Game Data')) {
throw new SteamAuthError('Session invalid: Personal Game Data not accessible'); throw new Error('Invalid or expired steamLoginSecure cookie');
} }
// 1. Locate the specific table containing cooldown info
let expirationDate: Date | undefined = undefined; let expirationDate: Date | undefined = undefined;
$('table').each((_, table) => { $('table').each((_, table) => {
@@ -49,10 +36,15 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
rows.each((_, row) => { rows.each((_, row) => {
const dateText = $(row).find('td').eq(expirationIndex).text().trim(); const dateText = $(row).find('td').eq(expirationIndex).text().trim();
if (dateText && dateText !== '') { if (dateText && dateText !== '') {
// Steam uses 'GMT' which some JS engines don't parse well, replace with 'UTC'
const cleanDateText = dateText.replace(' GMT', ' UTC'); const cleanDateText = dateText.replace(' GMT', ' UTC');
const parsed = new Date(cleanDateText); const parsed = new Date(cleanDateText);
if (!isNaN(parsed.getTime())) { if (!isNaN(parsed.getTime())) {
if (!expirationDate || parsed > (expirationDate as Date)) expirationDate = parsed; // We want the newest expiration date found
if (!expirationDate || parsed > (expirationDate as Date)) {
expirationDate = parsed;
}
} }
} }
}); });
@@ -60,7 +52,11 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
}); });
if (expirationDate && (expirationDate as Date).getTime() > Date.now()) { if (expirationDate && (expirationDate as Date).getTime() > Date.now()) {
return { isActive: true, expiresAt: expirationDate }; console.log(`[Scraper] Found active cooldown until: ${(expirationDate as Date).toISOString()}`);
return {
isActive: true,
expiresAt: expirationDate
};
} }
const content = $('#personal_game_data_content').text(); const content = $('#personal_game_data_content').text();
@@ -70,8 +66,7 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
return { isActive: false }; return { isActive: false };
} catch (error: any) { } catch (error: any) {
if (error instanceof SteamAuthError) throw error; console.error(`[Scraper] Error for ${steamId}:`, error.message);
console.error(`[Scraper] Network/Internal Error for ${steamId}:`, error.message); throw error;
throw error; // Generic errors don't trigger re-auth
} }
}; };

View File

@@ -55,13 +55,20 @@ class SteamClientService {
return path.join(this.steamPath, 'config', 'config.vdf'); return path.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.
*/
private safeWriteVdf(filePath: string, data: any) { private safeWriteVdf(filePath: string, data: any) {
const tempPath = `${filePath}.tmp_${Date.now()}`; const tempPath = `${filePath}.tmp_${Date.now()}`;
const dir = path.dirname(filePath); const dir = path.dirname(filePath);
try { try {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const vdfContent = stringify(data); const vdfContent = stringify(data);
fs.writeFileSync(tempPath, vdfContent, 'utf-8'); fs.writeFileSync(tempPath, vdfContent, 'utf-8');
// Atomic rename
fs.renameSync(tempPath, filePath); fs.renameSync(tempPath, filePath);
} catch (e: any) { } catch (e: any) {
console.error(`[SteamClient] Atomic write failed for ${filePath}: ${e.message}`); console.error(`[SteamClient] Atomic write failed for ${filePath}: ${e.message}`);
@@ -73,9 +80,11 @@ class SteamClientService {
public startWatching(callback: (accounts: LocalSteamAccount[]) => void) { public startWatching(callback: (accounts: LocalSteamAccount[]) => void) {
this.onAccountsChanged = callback; this.onAccountsChanged = callback;
const loginUsersPath = this.getLoginUsersPath(); const loginUsersPath = this.getLoginUsersPath();
if (loginUsersPath && fs.existsSync(loginUsersPath)) { if (loginUsersPath && fs.existsSync(loginUsersPath)) {
this.readLocalAccounts(); this.readLocalAccounts();
chokidar.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => { chokidar.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => {
console.log(`[SteamClient] loginusers.vdf changed, re-scanning...`);
this.readLocalAccounts(); this.readLocalAccounts();
}); });
} }
@@ -84,41 +93,64 @@ class SteamClientService {
private readLocalAccounts() { private readLocalAccounts() {
const filePath = this.getLoginUsersPath(); const filePath = this.getLoginUsersPath();
if (!filePath || !fs.existsSync(filePath)) return; if (!filePath || !fs.existsSync(filePath)) return;
try { try {
const content = fs.readFileSync(filePath, 'utf-8'); const content = fs.readFileSync(filePath, 'utf-8');
if (!content.trim()) return; if (!content.trim()) return; // Empty file
const data = parse(content) as any; const data = parse(content) as any;
if (!data || !data.users) return; if (!data || !data.users) return;
const accounts: LocalSteamAccount[] = []; const accounts: LocalSteamAccount[] = [];
for (const [steamId64, userData] of Object.entries(data.users)) { for (const [steamId64, userData] of Object.entries(data.users)) {
const user = userData as any; const user = userData as any;
if (!user || !user.AccountName) continue; if (!user || !user.AccountName) continue;
accounts.push({ accounts.push({
steamId: steamId64, accountName: user.AccountName, steamId: steamId64,
accountName: user.AccountName,
personaName: user.PersonaName || user.AccountName, personaName: user.PersonaName || user.AccountName,
timestamp: parseInt(user.Timestamp) || 0 timestamp: parseInt(user.Timestamp) || 0
}); });
} }
if (this.onAccountsChanged) this.onAccountsChanged(accounts); if (this.onAccountsChanged) this.onAccountsChanged(accounts);
} catch (error) { } } catch (error) {
console.error('[SteamClient] Error parsing loginusers.vdf:', error);
}
} }
public extractAccountConfig(accountName: string): any | null { public extractAccountConfig(accountName: string): any | null {
const configPath = this.getConfigVdfPath(); const configPath = this.getConfigVdfPath();
if (!configPath || !fs.existsSync(configPath)) return null; if (!configPath || !fs.existsSync(configPath)) return null;
try { try {
const content = fs.readFileSync(configPath, 'utf-8'); const content = fs.readFileSync(configPath, 'utf-8');
const data = parse(content) as any; const data = parse(content) as any;
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts; const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
return (accounts && accounts[accountName]) ? accounts[accountName] : null; return (accounts && accounts[accountName]) ? accounts[accountName] : null;
} catch (e) { return null; } } catch (e) {
console.error('[SteamClient] Failed to extract config.vdf data');
return null;
}
} }
public injectAccountConfig(accountName: string, accountData: any) { public injectAccountConfig(accountName: string, accountData: any) {
const configPath = this.getConfigVdfPath(); const configPath = this.getConfigVdfPath();
if (!configPath) return; if (!configPath) return;
let data: any = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } }; let data: any = {
InstallConfigStore: {
Software: {
Valve: {
Steam: {
Accounts: {}
}
}
}
}
};
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
try { try {
const content = fs.readFileSync(configPath, 'utf-8'); const content = fs.readFileSync(configPath, 'utf-8');
@@ -127,24 +159,14 @@ class SteamClientService {
} catch (e) { } } catch (e) { }
} }
const ensurePath = (obj: any, keys: string[]) => { // Ensure safe nesting
let curr = obj; if (!data.InstallConfigStore) data.InstallConfigStore = {};
for (const key of keys) { if (!data.InstallConfigStore.Software) data.InstallConfigStore.Software = {};
if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {}; if (!data.InstallConfigStore.Software.Valve) data.InstallConfigStore.Software.Valve = {};
curr = curr[key]; if (!data.InstallConfigStore.Software.Valve.Steam) data.InstallConfigStore.Software.Valve.Steam = {};
} if (!data.InstallConfigStore.Software.Valve.Steam.Accounts) data.InstallConfigStore.Software.Valve.Steam.Accounts = {};
return curr;
};
const steamAccounts = ensurePath(data, ['InstallConfigStore', 'Software', 'Valve', 'Steam', 'Accounts']); data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData;
// FAILPROOF: Force crucial flags that Steam uses to decide session validity
steamAccounts[accountName] = {
...accountData,
RememberPassword: "1",
AllowAutoLogin: "1",
Timestamp: Math.floor(Date.now() / 1000).toString()
};
try { try {
this.safeWriteVdf(configPath, data); this.safeWriteVdf(configPath, data);
@@ -204,7 +226,6 @@ class SteamClientService {
} catch (e) { } } catch (e) { }
} }
// Injection of the actual authentication blob
if (accountConfig && accountName) { if (accountConfig && accountName) {
this.injectAccountConfig(accountName, accountConfig); this.injectAccountConfig(accountName, accountConfig);
} }
@@ -218,7 +239,13 @@ class SteamClientService {
for (const regPath of regLocations) { for (const regPath of regLocations) {
if (!fs.existsSync(path.dirname(regPath))) continue; if (!fs.existsSync(path.dirname(regPath))) continue;
let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: { AutoLoginUser: "", RememberPassword: "1", AlreadyLoggedIn: "1" } } } } } };
let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: {
AutoLoginUser: "",
RememberPassword: "1",
AlreadyLoggedIn: "1"
} } } } } };
if (fs.existsSync(regPath)) { if (fs.existsSync(regPath)) {
try { try {
const content = fs.readFileSync(regPath, 'utf-8'); const content = fs.readFileSync(regPath, 'utf-8');
@@ -227,9 +254,13 @@ class SteamClientService {
} catch (e) { } } catch (e) { }
} }
// Deep merge helper
const ensurePath = (obj: any, keys: string[]) => { const ensurePath = (obj: any, keys: string[]) => {
let curr = obj; let curr = obj;
for (const key of keys) { if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {}; curr = curr[key]; } for (const key of keys) {
if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {};
curr = curr[key];
}
return curr; return curr;
}; };
@@ -238,7 +269,10 @@ class SteamClientService {
steamKey.RememberPassword = "1"; steamKey.RememberPassword = "1";
steamKey.AlreadyLoggedIn = "1"; steamKey.AlreadyLoggedIn = "1";
steamKey.WantsOfflineMode = "0"; steamKey.WantsOfflineMode = "0";
try { this.safeWriteVdf(regPath, regData); } catch (e) { }
try {
this.safeWriteVdf(regPath, regData);
} catch (e) { }
} }
} }

View File

@@ -1,12 +1,12 @@
{ {
"name": "ultimate-ban-tracker-desktop", "name": "ultimate-ban-tracker-desktop",
"version": "1.3.3", "version": "1.3.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ultimate-ban-tracker-desktop", "name": "ultimate-ban-tracker-desktop",
"version": "1.3.3", "version": "1.3.2",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",

View File

@@ -1,7 +1,7 @@
{ {
"name": "ultimate-ban-tracker-desktop", "name": "ultimate-ban-tracker-desktop",
"description": "Professional Steam Account Manager & Ban Tracker", "description": "Professional Steam Account Manager & Ban Tracker",
"version": "1.3.3", "version": "1.3.2",
"author": "Nils Pukropp <nils@narl.io>", "author": "Nils Pukropp <nils@narl.io>",
"homepage": "https://narl.io", "homepage": "https://narl.io",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",

View File

@@ -31,27 +31,8 @@ export const AppThemeProvider: React.FC<{ children: React.ReactNode }> = ({ chil
if (api?.updateServerConfig) { if (api?.updateServerConfig) {
await api.updateServerConfig({ theme }); await api.updateServerConfig({ theme });
} }
if (api?.updateAppIcon) {
try {
await api.updateAppIcon(theme);
} catch (e) { }
}
}; };
useEffect(() => {
const updateIcon = async () => {
const api = (window as any).electronAPI;
if (api?.updateAppIcon && currentTheme) {
try {
await api.updateAppIcon(currentTheme);
} catch (e) {
console.warn("[ThemeContext] updateAppIcon failed (likely not registered yet)");
}
}
};
updateIcon();
}, [currentTheme]);
const theme = useMemo(() => getTheme(currentTheme), [currentTheme]); const theme = useMemo(() => getTheme(currentTheme), [currentTheme]);
return ( return (