16 Commits

Author SHA1 Message Date
2124845848 Merge pull request 'release/v1.3.3' (#10) from release/v1.3.3 into main
Some checks failed
Build and Release / build (push) Failing after 5m26s
Reviewed-on: #10
2026-02-21 04:50:07 +01:00
4ad4e1c9de fix: implement failproof cross-platform session injection with forced VDF flags and registry synchronization 2026-02-21 04:49:52 +01:00
3f7c325604 design: implement modern high-detail professional app icons with themed gradients and depth 2026-02-21 04:46:01 +01:00
776e05fb52 fix: add safety checks for updateAppIcon IPC calls to prevent early startup race conditions 2026-02-21 04:44:10 +01:00
fc19f66ace design: implement dynamic theme-based app icons for system tray and taskbar 2026-02-21 04:43:05 +01:00
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
b64ddafab9 fix: implement proactive session capture and harden Steam registry/VDF injection for cross-machine reliability 2026-02-21 04:38:43 +01:00
9174bcfca2 chore: bump version to 1.3.2 2026-02-21 04:36:21 +01:00
d30005acbd fix: implement atomic writes and hardened VDF provisioning for robust Steam integration on fresh installs 2026-02-21 04:33:43 +01:00
a5cc155ffc fix: implement robust multi-timestamp synchronization logic to prevent data regression 2026-02-21 04:31:50 +01:00
276d3bd4de fix: implement non-blocking two-phase sync and hardened GCPD scraper for reliable cooldown tracking 2026-02-21 04:27:55 +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
19 changed files with 589 additions and 176 deletions

View File

@@ -1,4 +1,33 @@
<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">
<rect width="512" height="512" rx="64" fill="#171A21"/> <defs>
<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"/> <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>
<!-- 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: 604 B

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,21 @@
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,21 @@
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,21 @@
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,21 @@
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,21 @@
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -60,9 +60,23 @@ const initBackend = () => {
}; };
// --- System Tray --- // --- System Tray ---
const createTray = () => { const createTray = () => {
const assetsDir = path_1.default.join(__dirname, '..', 'assets-build'); // Try to find the icon in various standard locations
const possibleIcons = ['icon.svg', 'icon.png']; 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;
}
}
const possibleIcons = ['icon.png', 'icon.svg'];
let iconPath = ''; let iconPath = '';
if (assetsDir) {
for (const name of possibleIcons) { for (const name of possibleIcons) {
const fullPath = path_1.default.join(assetsDir, name); const fullPath = path_1.default.join(assetsDir, name);
if (fs_1.default.existsSync(fullPath)) { if (fs_1.default.existsSync(fullPath)) {
@@ -70,8 +84,13 @@ const createTray = () => {
break; break;
} }
} }
if (!iconPath) }
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; return;
}
try { try {
const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 }); const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
tray = new electron_1.Tray(icon); tray = new electron_1.Tray(icon);
@@ -80,7 +99,14 @@ const createTray = () => {
mainWindow.show(); mainWindow.show();
mainWindow.focus(); 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) { }
}; };
@@ -108,6 +134,21 @@ 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) => {
@@ -169,12 +210,12 @@ const scrapeAccountData = async (account) => {
if (result.isActive) { if (result.isActive) {
account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString(); account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString();
if (backend) if (backend)
await backend.pushCooldown(account.steamId, account.cooldownExpiresAt); await backend.pushCooldown(account.steamId, account.cooldownExpiresAt, now.toISOString());
} }
else { else {
account.cooldownExpiresAt = undefined; account.cooldownExpiresAt = undefined;
if (backend) if (backend)
await backend.pushCooldown(account.steamId, undefined); await backend.pushCooldown(account.steamId, undefined, now.toISOString());
} }
} }
catch (e) { catch (e) {
@@ -231,8 +272,24 @@ const syncAccounts = async (isManual = false) => {
exists.sessionUpdatedAt = s.sessionUpdatedAt; exists.sessionUpdatedAt = s.sessionUpdatedAt;
hasChanges = true; 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.cooldownExpiresAt = s.cooldownExpiresAt;
exists.lastScrapeTime = s.lastScrapeTime;
hasChanges = true; hasChanges = true;
} }
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) { if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
@@ -310,11 +367,20 @@ const handleLocalAccountsFound = async (localAccounts) => {
const profile = await (0, steam_web_1.fetchProfileData)(local.steamId); const profile = await (0, steam_web_1.fetchProfileData)(local.steamId);
const bans = await (0, steam_web_1.scrapeBanStatus)(profile.profileUrl); const bans = await (0, steam_web_1.scrapeBanStatus)(profile.profileUrl);
const localPath = await downloadAvatar(profile.steamId, profile.avatar); const localPath = await downloadAvatar(profile.steamId, profile.avatar);
// Wait and retry snagging the config (Steam takes time to write it)
let loginConfig = undefined;
for (let i = 0; i < 3; i++) {
await new Promise(r => setTimeout(r, 2000));
loginConfig = steam_client_1.steamClient.extractAccountConfig(local.accountName);
if (loginConfig)
break;
}
currentAccounts.push({ currentAccounts.push({
_id: Date.now().toString() + Math.random().toString().slice(2, 5), _id: Date.now().toString() + Math.random().toString().slice(2, 5),
steamId: local.steamId, personaName: profile.personaName || local.accountName, steamId: local.steamId, personaName: profile.personaName || local.accountName,
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar, loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
localAvatar: localPath, profileUrl: profile.profileUrl, localAvatar: localPath, profileUrl: profile.profileUrl,
loginConfig, sessionUpdatedAt: loginConfig ? new Date().toISOString() : undefined,
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none', 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()
}); });
@@ -544,7 +610,51 @@ 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('switch-account', async (event, loginName) => await handleSwitchAccount(loginName)); 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) => {
if (!loginName)
return false;
try {
// PROACTIVE SYNC: Try to snag the freshest token before we kill Steam
const accounts = store.get('accounts');
const account = accounts.find(a => a.loginName === loginName);
if (account && !account._id.startsWith('shared_')) {
const freshConfig = steam_client_1.steamClient.extractAccountConfig(loginName);
if (freshConfig) {
account.loginConfig = freshConfig;
account.sessionUpdatedAt = new Date().toISOString();
if (backend)
await backend.shareAccount(account);
store.set('accounts', accounts);
}
}
await killSteam();
if (process.platform === 'win32') {
const regBase = 'reg add "HKCU\\Software\\Valve\\Steam"';
const commands = [
`${regBase} /v AutoLoginUser /t REG_SZ /d "${loginName}" /f`,
`${regBase} /v RememberPassword /t REG_DWORD /d 1 /f`,
`${regBase} /v AlreadyLoggedIn /t REG_DWORD /d 1 /f`,
`${regBase} /v WantsOfflineMode /t REG_DWORD /d 0 /f`
];
await new Promise((res, rej) => (0, child_process_1.exec)(commands.join(' && '), (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('open-external', (event, url) => electron_1.shell.openExternal(url)); electron_1.ipcMain.handle('open-external', (event, url) => electron_1.shell.openExternal(url));
electron_1.ipcMain.handle('open-steam-app-login', async () => { electron_1.ipcMain.handle('open-steam-app-login', async () => {
await killSteam(); await killSteam();

View File

@@ -11,6 +11,7 @@ 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

@@ -68,19 +68,23 @@ class BackendService {
loginName: account.loginName, loginName: account.loginName,
steamLoginSecure: account.steamLoginSecure, steamLoginSecure: account.steamLoginSecure,
loginConfig: account.loginConfig, loginConfig: account.loginConfig,
sessionUpdatedAt: account.sessionUpdatedAt sessionUpdatedAt: account.sessionUpdatedAt,
lastMetadataCheck: account.lastBanCheck,
lastScrapeTime: account.lastScrapeTime,
cooldownExpiresAt: account.cooldownExpiresAt
}, { headers: this.headers }); }, { headers: this.headers });
} }
catch (e) { catch (e) {
console.error('[Backend] Failed to share account'); console.error('[Backend] Failed to share account');
} }
} }
async pushCooldown(steamId, cooldownExpiresAt) { async pushCooldown(steamId, cooldownExpiresAt, lastScrapeTime) {
if (!this.token) if (!this.token)
return; return;
try { try {
await axios_1.default.patch(`${this.url}/api/sync/${steamId}/cooldown`, { await axios_1.default.patch(`${this.url}/api/sync/${steamId}/cooldown`, {
cooldownExpiresAt cooldownExpiresAt,
lastScrapeTime
}, { headers: this.headers }); }, { headers: this.headers });
} }
catch (e) { catch (e) {

View File

@@ -57,19 +57,25 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => {
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();
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) { if (expirationIndex !== -1) {
const firstRow = $(table).find('tr').not(':has(th)').first(); const rows = $(table).find('tr').not(':has(th)');
const dateText = firstRow.find('td').eq(expirationIndex).text().trim(); rows.each((_, row) => {
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())) {
// 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()) {
console.log(`[Scraper] Found active cooldown until: ${expirationDate.toISOString()}`); console.log(`[Scraper] Found active cooldown until: ${expirationDate.toISOString()}`);
return { return {

View File

@@ -21,7 +21,8 @@ class SteamClientService {
if (platform === 'win32') { if (platform === 'win32') {
const possiblePaths = [ const possiblePaths = [
'C:\\Program Files (x86)\\Steam', '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; this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null;
} }
@@ -29,7 +30,8 @@ class SteamClientService {
const possiblePaths = [ const possiblePaths = [
path_1.default.join(home, '.steam/steam'), path_1.default.join(home, '.steam/steam'),
path_1.default.join(home, '.local/share/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; this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null;
} }
@@ -47,12 +49,35 @@ 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) {
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) { startWatching(callback) {
this.onAccountsChanged = callback; this.onAccountsChanged = callback;
const loginUsersPath = this.getLoginUsersPath(); const loginUsersPath = this.getLoginUsersPath();
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 }).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();
}); });
} }
@@ -63,16 +88,20 @@ class SteamClientService {
return; return;
try { try {
const content = fs_1.default.readFileSync(filePath, 'utf-8'); const content = fs_1.default.readFileSync(filePath, 'utf-8');
if (!content.trim())
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;
const accounts = []; const accounts = [];
for (const [steamId64, userData] of Object.entries(data.users)) { for (const [steamId64, userData] of Object.entries(data.users)) {
const user = userData; const user = userData;
if (!user || !user.AccountName)
continue;
accounts.push({ accounts.push({
steamId: steamId64, steamId: steamId64,
accountName: user.AccountName, accountName: user.AccountName,
personaName: user.PersonaName, personaName: user.PersonaName || user.AccountName,
timestamp: parseInt(user.Timestamp) || 0 timestamp: parseInt(user.Timestamp) || 0
}); });
} }
@@ -91,32 +120,38 @@ class SteamClientService {
const content = fs_1.default.readFileSync(configPath, 'utf-8'); const content = fs_1.default.readFileSync(configPath, 'utf-8');
const data = (0, simple_vdf_1.parse)(content); const data = (0, simple_vdf_1.parse)(content);
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts; const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
if (accounts && accounts[accountName]) { return (accounts && accounts[accountName]) ? accounts[accountName] : null;
return accounts[accountName];
}
} }
catch (e) { catch (e) {
console.error('[SteamClient] Failed to extract config.vdf data'); console.error('[SteamClient] Failed to extract config.vdf data');
}
return null; return null;
} }
}
injectAccountConfig(accountName, accountData) { injectAccountConfig(accountName, accountData) {
const configPath = this.getConfigVdfPath(); const configPath = this.getConfigVdfPath();
if (!configPath) if (!configPath)
return; return;
// Create directory if it doesn't exist let data = {
const configDir = path_1.default.dirname(configPath); InstallConfigStore: {
if (!fs_1.default.existsSync(configDir)) Software: {
fs_1.default.mkdirSync(configDir, { recursive: true }); Valve: {
let data = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } }; 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');
data = (0, simple_vdf_1.parse)(content); const parsed = (0, simple_vdf_1.parse)(content);
if (parsed && typeof parsed === 'object')
data = parsed;
} }
catch (e) { } catch (e) { }
} }
// Ensure structure exists // Ensure safe nesting
if (!data.InstallConfigStore) if (!data.InstallConfigStore)
data.InstallConfigStore = {}; data.InstallConfigStore = {};
if (!data.InstallConfigStore.Software) if (!data.InstallConfigStore.Software)
@@ -129,25 +164,22 @@ class SteamClientService {
data.InstallConfigStore.Software.Valve.Steam.Accounts = {}; data.InstallConfigStore.Software.Valve.Steam.Accounts = {};
data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData; data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData;
try { try {
fs_1.default.writeFileSync(configPath, (0, simple_vdf_1.stringify)(data)); this.safeWriteVdf(configPath, data);
console.log(`[SteamClient] Injected login config for ${accountName} into config.vdf`); console.log(`[SteamClient] Safely injected session for ${accountName}`);
}
catch (e) {
console.error('[SteamClient] Failed to write config.vdf');
} }
catch (e) { }
} }
async setAutoLoginUser(accountName, accountConfig, steamId) { async setAutoLoginUser(accountName, accountConfig, steamId) {
const platform = os_1.default.platform(); const platform = os_1.default.platform();
const loginUsersPath = this.getLoginUsersPath(); const loginUsersPath = this.getLoginUsersPath();
if (loginUsersPath) { 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: {} }; let data = { users: {} };
if (fs_1.default.existsSync(loginUsersPath)) { if (fs_1.default.existsSync(loginUsersPath)) {
try { try {
const content = fs_1.default.readFileSync(loginUsersPath, 'utf-8'); 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) { } catch (e) { }
} }
@@ -156,7 +188,7 @@ class SteamClientService {
let found = false; let found = false;
for (const [id, user] of Object.entries(data.users)) { for (const [id, user] of Object.entries(data.users)) {
const u = user; const u = user;
if (u.AccountName.toLowerCase() === accountName.toLowerCase()) { if (u.AccountName?.toLowerCase() === accountName.toLowerCase()) {
u.mostrecent = "1"; u.mostrecent = "1";
u.RememberPassword = "1"; u.RememberPassword = "1";
u.AllowAutoLogin = "1"; u.AllowAutoLogin = "1";
@@ -169,8 +201,8 @@ class SteamClientService {
u.mostrecent = "0"; u.mostrecent = "0";
} }
} }
if (!found && steamId) { if (!found && steamId && accountName) {
console.log(`[SteamClient] Provisioning user ${accountName} into loginusers.vdf`); console.log(`[SteamClient] Provisioning new user profile for ${accountName}`);
data.users[steamId] = { data.users[steamId] = {
AccountName: accountName, AccountName: accountName,
PersonaName: accountName, PersonaName: accountName,
@@ -184,51 +216,53 @@ class SteamClientService {
}; };
} }
try { try {
fs_1.default.writeFileSync(loginUsersPath, (0, simple_vdf_1.stringify)(data)); this.safeWriteVdf(loginUsersPath, data);
} }
catch (e) { catch (e) { }
console.error('[SteamClient] Failed to write loginusers.vdf');
} }
} if (accountConfig && accountName) {
if (accountConfig) {
this.injectAccountConfig(accountName, accountConfig); this.injectAccountConfig(accountName, accountConfig);
} }
// --- Linux Registry / Registry.vdf Hardening ---
if (platform === 'linux') { if (platform === 'linux') {
const regLocations = [ const regLocations = [
path_1.default.join(os_1.default.homedir(), '.steam', 'registry.vdf'), path_1.default.join(os_1.default.homedir(), '.steam', 'registry.vdf'),
path_1.default.join(os_1.default.homedir(), '.steam', 'steam', 'registry.vdf') path_1.default.join(os_1.default.homedir(), '.steam', 'steam', 'registry.vdf')
]; ];
for (const regPath of regLocations) { 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)) { 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');
regData = (0, simple_vdf_1.parse)(content); const parsed = (0, simple_vdf_1.parse)(content);
if (parsed && typeof parsed === 'object')
regData = parsed;
} }
catch (e) { } catch (e) { }
} }
else { // Deep merge helper
const regDir = path_1.default.dirname(regPath); const ensurePath = (obj, keys) => {
if (!fs_1.default.existsSync(regDir))
fs_1.default.mkdirSync(regDir, { recursive: true });
}
const setPath = (obj, keys, val) => {
let curr = obj; let curr = obj;
for (let i = 0; i < keys.length - 1; i++) { for (const key of keys) {
if (!curr[keys[i]]) if (!curr[key] || typeof curr[key] !== 'object')
curr[keys[i]] = {}; curr[key] = {};
curr = curr[keys[i]]; curr = curr[key];
} }
curr[keys[keys.length - 1]] = val; return curr;
}; };
const steamReg = ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']; const steamKey = ensurePath(regData, ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']);
setPath(regData, [...steamReg, 'AutoLoginUser'], accountName); steamKey.AutoLoginUser = accountName;
setPath(regData, [...steamReg, 'RememberPassword'], "1"); steamKey.RememberPassword = "1";
setPath(regData, [...steamReg, 'AlreadyLoggedIn'], "1"); steamKey.AlreadyLoggedIn = "1";
setPath(regData, [...steamReg, 'WantsOfflineMode'], "0"); steamKey.WantsOfflineMode = "0";
try { try {
fs_1.default.writeFileSync(regPath, (0, simple_vdf_1.stringify)(regData)); this.safeWriteVdf(regPath, regData);
console.log(`[SteamClient] Registry updated: ${regPath}`);
} }
catch (e) { } catch (e) { }
} }

View File

@@ -135,7 +135,14 @@ 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) { }
}; };
@@ -162,6 +169,25 @@ 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) => {
@@ -215,10 +241,10 @@ const scrapeAccountData = async (account: Account) => {
account.authError = false; account.lastScrapeTime = now.toISOString(); account.authError = false; account.lastScrapeTime = now.toISOString();
if (result.isActive) { if (result.isActive) {
account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString(); account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString();
if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt); if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt, now.toISOString());
} else { } else {
account.cooldownExpiresAt = undefined; account.cooldownExpiresAt = undefined;
if (backend) await backend.pushCooldown(account.steamId, undefined); if (backend) await backend.pushCooldown(account.steamId, undefined, now.toISOString());
} }
} catch (e: any) { } catch (e: any) {
if (e.message.includes('cookie') || e.message.includes('Sign In')) account.authError = true; if (e.message.includes('cookie') || e.message.includes('Sign In')) account.authError = true;
@@ -267,10 +293,29 @@ const syncAccounts = async (isManual = false) => {
exists.sessionUpdatedAt = s.sessionUpdatedAt; exists.sessionUpdatedAt = s.sessionUpdatedAt;
hasChanges = true; hasChanges = true;
} }
if (s.cooldownExpiresAt && (!exists.cooldownExpiresAt || new Date(s.cooldownExpiresAt) > new Date(exists.cooldownExpiresAt))) {
exists.cooldownExpiresAt = s.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; 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)) { if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
exists.sharedWith = s.sharedWith; exists.sharedWith = s.sharedWith;
hasChanges = true; hasChanges = true;
@@ -337,11 +382,21 @@ const handleLocalAccountsFound = async (localAccounts: LocalSteamAccount[]) => {
const profile = await fetchProfileData(local.steamId); const profile = await fetchProfileData(local.steamId);
const bans = await scrapeBanStatus(profile.profileUrl); const bans = await scrapeBanStatus(profile.profileUrl);
const localPath = await downloadAvatar(profile.steamId, profile.avatar); const localPath = await downloadAvatar(profile.steamId, profile.avatar);
// Wait and retry snagging the config (Steam takes time to write it)
let loginConfig = undefined;
for (let i = 0; i < 3; i++) {
await new Promise(r => setTimeout(r, 2000));
loginConfig = steamClient.extractAccountConfig(local.accountName);
if (loginConfig) break;
}
currentAccounts.push({ currentAccounts.push({
_id: Date.now().toString() + Math.random().toString().slice(2, 5), _id: Date.now().toString() + Math.random().toString().slice(2, 5),
steamId: local.steamId, personaName: profile.personaName || local.accountName, steamId: local.steamId, personaName: profile.personaName || local.accountName,
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar, loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
localAvatar: localPath, profileUrl: profile.profileUrl, localAvatar: localPath, profileUrl: profile.profileUrl,
loginConfig, sessionUpdatedAt: loginConfig ? new Date().toISOString() : undefined,
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none', 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()
}); });
@@ -542,7 +597,49 @@ 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('switch-account', async (event, loginName: string) => await handleSwitchAccount(loginName)); 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) => {
if (!loginName) return false;
try {
// PROACTIVE SYNC: Try to snag the freshest token before we kill Steam
const accounts = store.get('accounts') as Account[];
const account = accounts.find(a => a.loginName === loginName);
if (account && !account._id.startsWith('shared_')) {
const freshConfig = steamClient.extractAccountConfig(loginName);
if (freshConfig) {
account.loginConfig = freshConfig;
account.sessionUpdatedAt = new Date().toISOString();
if (backend) await backend.shareAccount(account);
store.set('accounts', accounts);
}
}
await killSteam();
if (process.platform === 'win32') {
const regBase = 'reg add "HKCU\\Software\\Valve\\Steam"';
const commands = [
`${regBase} /v AutoLoginUser /t REG_SZ /d "${loginName}" /f`,
`${regBase} /v RememberPassword /t REG_DWORD /d 1 /f`,
`${regBase} /v AlreadyLoggedIn /t REG_DWORD /d 1 /f`,
`${regBase} /v WantsOfflineMode /t REG_DWORD /d 0 /f`
];
await new Promise<void>((res, rej) => exec(commands.join(' && '), (e) => e ? rej(e) : res()));
if (account && account.loginConfig) steamClient.injectAccountConfig(loginName, account.loginConfig);
} else if (process.platform === 'linux') {
await steamClient.setAutoLoginUser(loginName, account?.loginConfig, account?.steamId);
}
startSteam();
return true;
} catch (e) { return false; }
});
ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url)); ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url));
ipcMain.handle('open-steam-app-login', async () => { ipcMain.handle('open-steam-app-login', async () => {

View File

@@ -10,6 +10,7 @@ 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

@@ -62,18 +62,22 @@ export class BackendService {
loginName: account.loginName, loginName: account.loginName,
steamLoginSecure: account.steamLoginSecure, steamLoginSecure: account.steamLoginSecure,
loginConfig: account.loginConfig, loginConfig: account.loginConfig,
sessionUpdatedAt: account.sessionUpdatedAt sessionUpdatedAt: account.sessionUpdatedAt,
lastMetadataCheck: account.lastBanCheck,
lastScrapeTime: account.lastScrapeTime,
cooldownExpiresAt: account.cooldownExpiresAt
}, { headers: this.headers }); }, { headers: this.headers });
} catch (e) { } catch (e) {
console.error('[Backend] Failed to share account'); console.error('[Backend] Failed to share account');
} }
} }
public async pushCooldown(steamId: string, cooldownExpiresAt?: string) { public async pushCooldown(steamId: string, cooldownExpiresAt?: string, lastScrapeTime?: string) {
if (!this.token) return; if (!this.token) return;
try { try {
await axios.patch(`${this.url}/api/sync/${steamId}/cooldown`, { await axios.patch(`${this.url}/api/sync/${steamId}/cooldown`, {
cooldownExpiresAt cooldownExpiresAt,
lastScrapeTime
}, { headers: this.headers }); }, { headers: this.headers });
} catch (e) { } catch (e) {
console.error(`[Backend] Failed to push cooldown for ${steamId}`); console.error(`[Backend] Failed to push cooldown for ${steamId}`);

View File

@@ -29,22 +29,27 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
$('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();
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) { if (expirationIndex !== -1) {
const firstRow = $(table).find('tr').not(':has(th)').first(); const rows = $(table).find('tr').not(':has(th)');
const dateText = firstRow.find('td').eq(expirationIndex).text().trim(); rows.each((_, row) => {
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())) {
// We want the newest expiration date found
if (!expirationDate || parsed > (expirationDate as Date)) {
expirationDate = parsed; expirationDate = parsed;
} }
} }
} }
}); });
}
});
if (expirationDate && (expirationDate as Date).getTime() > Date.now()) { if (expirationDate && (expirationDate as Date).getTime() > Date.now()) {
console.log(`[Scraper] Found active cooldown until: ${(expirationDate as Date).toISOString()}`); console.log(`[Scraper] Found active cooldown until: ${(expirationDate as Date).toISOString()}`);

View File

@@ -26,14 +26,16 @@ class SteamClientService {
if (platform === 'win32') { if (platform === 'win32') {
const possiblePaths = [ const possiblePaths = [
'C:\\Program Files (x86)\\Steam', 'C:\\Program Files (x86)\\Steam',
'C:\\Program Files\\Steam' 'C:\\Program Files\\Steam',
path.join(process.env.APPDATA || '', 'Steam'),
]; ];
this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null; this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null;
} else if (platform === 'linux') { } else if (platform === 'linux') {
const possiblePaths = [ const possiblePaths = [
path.join(home, '.steam/steam'), path.join(home, '.steam/steam'),
path.join(home, '.local/share/Steam'), path.join(home, '.local/share/Steam'),
path.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam') path.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam'), // Flatpak
path.join(home, 'snap/steam/common/.steam/steam'), // Snap
]; ];
this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null; this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null;
} }
@@ -53,13 +55,27 @@ class SteamClientService {
return path.join(this.steamPath, 'config', 'config.vdf'); return path.join(this.steamPath, 'config', 'config.vdf');
} }
private safeWriteVdf(filePath: string, data: any) {
const tempPath = `${filePath}.tmp_${Date.now()}`;
const dir = path.dirname(filePath);
try {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const vdfContent = stringify(data);
fs.writeFileSync(tempPath, vdfContent, 'utf-8');
fs.renameSync(tempPath, filePath);
} catch (e: any) {
console.error(`[SteamClient] Atomic write failed for ${filePath}: ${e.message}`);
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
throw e;
}
}
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 }).on('change', () => { chokidar.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => {
this.readLocalAccounts(); this.readLocalAccounts();
}); });
} }
@@ -68,79 +84,72 @@ 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;
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;
accounts.push({ accounts.push({
steamId: steamId64, steamId: steamId64, accountName: user.AccountName,
accountName: user.AccountName, personaName: user.PersonaName || user.AccountName,
personaName: user.PersonaName,
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;
if (accounts && accounts[accountName]) { return (accounts && accounts[accountName]) ? accounts[accountName] : null;
return accounts[accountName]; } 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;
// Create directory if it doesn't exist
const configDir = path.dirname(configPath);
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
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');
data = parse(content) as any; const parsed = parse(content) as any;
if (parsed && typeof parsed === 'object') data = parsed;
} catch (e) { } } catch (e) { }
} }
// Ensure structure exists const ensurePath = (obj: any, keys: string[]) => {
if (!data.InstallConfigStore) data.InstallConfigStore = {}; let curr = obj;
if (!data.InstallConfigStore.Software) data.InstallConfigStore.Software = {}; for (const key of keys) {
if (!data.InstallConfigStore.Software.Valve) data.InstallConfigStore.Software.Valve = {}; if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {};
if (!data.InstallConfigStore.Software.Valve.Steam) data.InstallConfigStore.Software.Valve.Steam = {}; curr = curr[key];
if (!data.InstallConfigStore.Software.Valve.Steam.Accounts) data.InstallConfigStore.Software.Valve.Steam.Accounts = {}; }
return curr;
};
data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData; const steamAccounts = ensurePath(data, ['InstallConfigStore', 'Software', 'Valve', 'Steam', 'Accounts']);
// 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 {
fs.writeFileSync(configPath, stringify(data)); this.safeWriteVdf(configPath, data);
console.log(`[SteamClient] Injected login config for ${accountName} into config.vdf`); console.log(`[SteamClient] Safely injected session for ${accountName}`);
} catch (e) { } catch (e) { }
console.error('[SteamClient] Failed to write config.vdf');
}
} }
public async setAutoLoginUser(accountName: string, accountConfig?: any, steamId?: string): Promise<boolean> { public async setAutoLoginUser(accountName: string, accountConfig?: any, steamId?: string): Promise<boolean> {
@@ -148,14 +157,12 @@ class SteamClientService {
const loginUsersPath = this.getLoginUsersPath(); const loginUsersPath = this.getLoginUsersPath();
if (loginUsersPath) { if (loginUsersPath) {
const configDir = path.dirname(loginUsersPath);
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
let data: any = { users: {} }; let data: any = { users: {} };
if (fs.existsSync(loginUsersPath)) { if (fs.existsSync(loginUsersPath)) {
try { try {
const content = fs.readFileSync(loginUsersPath, 'utf-8'); const content = fs.readFileSync(loginUsersPath, 'utf-8');
data = parse(content) as any; const parsed = parse(content) as any;
if (parsed && parsed.users) data = parsed;
} catch (e) { } } catch (e) { }
} }
@@ -164,7 +171,7 @@ class SteamClientService {
let found = false; let found = false;
for (const [id, user] of Object.entries(data.users)) { for (const [id, user] of Object.entries(data.users)) {
const u = user as any; const u = user as any;
if (u.AccountName.toLowerCase() === accountName.toLowerCase()) { if (u.AccountName?.toLowerCase() === accountName.toLowerCase()) {
u.mostrecent = "1"; u.mostrecent = "1";
u.RememberPassword = "1"; u.RememberPassword = "1";
u.AllowAutoLogin = "1"; u.AllowAutoLogin = "1";
@@ -177,8 +184,8 @@ class SteamClientService {
} }
} }
if (!found && steamId) { if (!found && steamId && accountName) {
console.log(`[SteamClient] Provisioning user ${accountName} into loginusers.vdf`); console.log(`[SteamClient] Provisioning new user profile for ${accountName}`);
data.users[steamId] = { data.users[steamId] = {
AccountName: accountName, AccountName: accountName,
PersonaName: accountName, PersonaName: accountName,
@@ -193,16 +200,16 @@ class SteamClientService {
} }
try { try {
fs.writeFileSync(loginUsersPath, stringify(data)); this.safeWriteVdf(loginUsersPath, data);
} catch (e) { } catch (e) { }
console.error('[SteamClient] Failed to write loginusers.vdf');
}
} }
if (accountConfig) { // Injection of the actual authentication blob
if (accountConfig && accountName) {
this.injectAccountConfig(accountName, accountConfig); this.injectAccountConfig(accountName, accountConfig);
} }
// --- Linux Registry / Registry.vdf Hardening ---
if (platform === 'linux') { if (platform === 'linux') {
const regLocations = [ const regLocations = [
path.join(os.homedir(), '.steam', 'registry.vdf'), path.join(os.homedir(), '.steam', 'registry.vdf'),
@@ -210,37 +217,28 @@ class SteamClientService {
]; ];
for (const regPath of regLocations) { for (const regPath of regLocations) {
let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: {} } } } } }; if (!fs.existsSync(path.dirname(regPath))) continue;
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');
regData = parse(content) as any; const parsed = parse(content) as any;
if (parsed && typeof parsed === 'object') regData = parsed;
} catch (e) { } } catch (e) { }
} else {
const regDir = path.dirname(regPath);
if (!fs.existsSync(regDir)) fs.mkdirSync(regDir, { recursive: true });
} }
const setPath = (obj: any, keys: string[], val: string) => { const ensurePath = (obj: any, keys: string[]) => {
let curr = obj; let curr = obj;
for (let i = 0; i < keys.length - 1; i++) { for (const key of keys) { if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {}; curr = curr[key]; }
if (!curr[keys[i]!]) curr[keys[i]!] = {}; return curr;
curr = curr[keys[i]!];
}
curr[keys[keys.length - 1]!] = val;
}; };
const steamReg = ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']; const steamKey = ensurePath(regData, ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']);
setPath(regData, [...steamReg, 'AutoLoginUser'], accountName); steamKey.AutoLoginUser = accountName;
setPath(regData, [...steamReg, 'RememberPassword'], "1"); steamKey.RememberPassword = "1";
setPath(regData, [...steamReg, 'AlreadyLoggedIn'], "1"); steamKey.AlreadyLoggedIn = "1";
setPath(regData, [...steamReg, 'WantsOfflineMode'], "0"); steamKey.WantsOfflineMode = "0";
try { this.safeWriteVdf(regPath, regData); } catch (e) { }
try {
fs.writeFileSync(regPath, stringify(regData));
console.log(`[SteamClient] Registry updated: ${regPath}`);
} catch (e) { }
} }
} }

View File

@@ -1,12 +1,12 @@
{ {
"name": "ultimate-ban-tracker-desktop", "name": "ultimate-ban-tracker-desktop",
"version": "1.3.0", "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.0", "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.0", "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,8 +31,27 @@ 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 (