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">
<rect width="512" height="512" rx="64" fill="#171A21"/>
<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"/>
<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>
<!-- 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>

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,18 +60,37 @@ const initBackend = () => {
};
// --- System Tray ---
const createTray = () => {
const assetsDir = path_1.default.join(__dirname, '..', 'assets-build');
const possibleIcons = ['icon.svg', 'icon.png'];
let iconPath = '';
for (const name of possibleIcons) {
const fullPath = path_1.default.join(assetsDir, name);
if (fs_1.default.existsSync(fullPath)) {
iconPath = fullPath;
// Try to find the icon in various standard locations
const possiblePaths = [
path_1.default.join(__dirname, '..', 'assets-build'), // Dev
path_1.default.join(process.resourcesPath, 'assets-build'), // Packaged (External)
path_1.default.join(electron_1.app.getAppPath(), 'dist', 'assets-build'), // Packaged (Internal dist)
path_1.default.join(electron_1.app.getAppPath(), 'assets-build') // Packaged (Internal root)
];
let assetsDir = '';
for (const p of possiblePaths) {
if (fs_1.default.existsSync(p)) {
assetsDir = p;
break;
}
}
if (!iconPath)
const possibleIcons = ['icon.png', 'icon.svg'];
let iconPath = '';
if (assetsDir) {
for (const name of possibleIcons) {
const fullPath = path_1.default.join(assetsDir, name);
if (fs_1.default.existsSync(fullPath)) {
iconPath = fullPath;
break;
}
}
}
console.log(`[Tray] Resolved assets directory: ${assetsDir || 'NOT FOUND'}`);
console.log(`[Tray] Attempting to initialize with icon: ${iconPath || 'NONE FOUND'}`);
if (!iconPath) {
console.warn(`[Tray] FAILED: No valid icon found in searched paths.`);
return;
}
try {
const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
tray = new electron_1.Tray(icon);
@@ -80,7 +99,14 @@ const createTray = () => {
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) { }
};
@@ -108,6 +134,21 @@ const updateTrayMenu = () => {
]);
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 ---
const killSteam = async () => {
return new Promise((resolve) => {
@@ -169,12 +210,12 @@ const scrapeAccountData = async (account) => {
if (result.isActive) {
account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString();
if (backend)
await backend.pushCooldown(account.steamId, account.cooldownExpiresAt);
await backend.pushCooldown(account.steamId, account.cooldownExpiresAt, now.toISOString());
}
else {
account.cooldownExpiresAt = undefined;
if (backend)
await backend.pushCooldown(account.steamId, undefined);
await backend.pushCooldown(account.steamId, undefined, now.toISOString());
}
}
catch (e) {
@@ -231,8 +272,24 @@ const syncAccounts = async (isManual = false) => {
exists.sessionUpdatedAt = s.sessionUpdatedAt;
hasChanges = true;
}
if (s.cooldownExpiresAt && (!exists.cooldownExpiresAt || new Date(s.cooldownExpiresAt) > new Date(exists.cooldownExpiresAt))) {
// Metadata Sync (Pull)
const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0);
const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0);
if (sMetaDate > lMetaDate) {
exists.personaName = s.personaName;
exists.avatar = s.avatar;
exists.vacBanned = s.vacBanned;
exists.gameBans = s.gameBans;
exists.status = (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none';
exists.lastBanCheck = s.lastMetadataCheck;
hasChanges = true;
}
// Cooldown Sync (Pull)
const sScrapeDate = s.lastScrapeTime ? new Date(s.lastScrapeTime) : new Date(0);
const lScrapeDate = exists.lastScrapeTime ? new Date(exists.lastScrapeTime) : new Date(0);
if (sScrapeDate > lScrapeDate) {
exists.cooldownExpiresAt = s.cooldownExpiresAt;
exists.lastScrapeTime = s.lastScrapeTime;
hasChanges = true;
}
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
@@ -310,11 +367,20 @@ const handleLocalAccountsFound = async (localAccounts) => {
const profile = await (0, steam_web_1.fetchProfileData)(local.steamId);
const bans = await (0, steam_web_1.scrapeBanStatus)(profile.profileUrl);
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({
_id: Date.now().toString() + Math.random().toString().slice(2, 5),
steamId: local.steamId, personaName: profile.personaName || local.accountName,
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
localAvatar: localPath, profileUrl: profile.profileUrl,
loginConfig, sessionUpdatedAt: loginConfig ? new Date().toISOString() : undefined,
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none',
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-remove-account', async (event, steamId) => { initBackend(); if (backend)
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-steam-app-login', async () => {
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),
revokeAllAccountAccess: (steamId) => electron_1.ipcRenderer.invoke('revoke-all-account-access', steamId),
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'),
openSteamLogin: (steamId) => electron_1.ipcRenderer.invoke('open-steam-login', steamId),
// Server Config & Auth

View File

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

View File

@@ -57,17 +57,23 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => {
let expirationDate = undefined;
$('table').each((_, table) => {
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration'));
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration') || h.includes('Cooldown Expiration'));
if (expirationIndex !== -1) {
const firstRow = $(table).find('tr').not(':has(th)').first();
const dateText = firstRow.find('td').eq(expirationIndex).text().trim();
if (dateText && dateText !== '') {
const cleanDateText = dateText.replace(' GMT', ' UTC');
const parsed = new Date(cleanDateText);
if (!isNaN(parsed.getTime())) {
expirationDate = parsed;
const rows = $(table).find('tr').not(':has(th)');
rows.each((_, row) => {
const dateText = $(row).find('td').eq(expirationIndex).text().trim();
if (dateText && dateText !== '') {
// Steam uses 'GMT' which some JS engines don't parse well, replace with 'UTC'
const cleanDateText = dateText.replace(' GMT', ' UTC');
const parsed = new Date(cleanDateText);
if (!isNaN(parsed.getTime())) {
// We want the newest expiration date found
if (!expirationDate || parsed > expirationDate) {
expirationDate = parsed;
}
}
}
}
});
}
});
if (expirationDate && expirationDate.getTime() > Date.now()) {

View File

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

View File

@@ -135,7 +135,14 @@ const createTray = () => {
tray = new Tray(icon);
tray.setToolTip('Ultimate Ban Tracker');
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) { }
};
@@ -162,6 +169,25 @@ const updateTrayMenu = () => {
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 ---
const killSteam = async () => {
return new Promise<void>((resolve) => {
@@ -215,10 +241,10 @@ const scrapeAccountData = async (account: Account) => {
account.authError = false; account.lastScrapeTime = now.toISOString();
if (result.isActive) {
account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString();
if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt);
if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt, now.toISOString());
} else {
account.cooldownExpiresAt = undefined;
if (backend) await backend.pushCooldown(account.steamId, undefined);
if (backend) await backend.pushCooldown(account.steamId, undefined, now.toISOString());
}
} catch (e: any) {
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;
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;
}
// 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)) {
exists.sharedWith = s.sharedWith;
hasChanges = true;
@@ -337,11 +382,21 @@ const handleLocalAccountsFound = async (localAccounts: LocalSteamAccount[]) => {
const profile = await fetchProfileData(local.steamId);
const bans = await scrapeBanStatus(profile.profileUrl);
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({
_id: Date.now().toString() + Math.random().toString().slice(2, 5),
steamId: local.steamId, personaName: profile.personaName || local.accountName,
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
localAvatar: localPath, profileUrl: profile.profileUrl,
loginConfig, sessionUpdatedAt: loginConfig ? new Date().toISOString() : undefined,
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none',
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-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-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),
revokeAllAccountAccess: (steamId: string) => ipcRenderer.invoke('revoke-all-account-access', steamId),
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
updateAppIcon: (theme: string) => ipcRenderer.invoke('update-app-icon', theme),
openSteamAppLogin: () => ipcRenderer.invoke('open-steam-app-login'),
openSteamLogin: (steamId: string) => ipcRenderer.invoke('open-steam-login', steamId),

View File

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

View File

@@ -29,20 +29,25 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
$('table').each((_, table) => {
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration'));
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration') || h.includes('Cooldown Expiration'));
if (expirationIndex !== -1) {
const firstRow = $(table).find('tr').not(':has(th)').first();
const dateText = firstRow.find('td').eq(expirationIndex).text().trim();
if (dateText && dateText !== '') {
const cleanDateText = dateText.replace(' GMT', ' UTC');
const parsed = new Date(cleanDateText);
if (!isNaN(parsed.getTime())) {
expirationDate = parsed;
const rows = $(table).find('tr').not(':has(th)');
rows.each((_, row) => {
const dateText = $(row).find('td').eq(expirationIndex).text().trim();
if (dateText && dateText !== '') {
// Steam uses 'GMT' which some JS engines don't parse well, replace with 'UTC'
const cleanDateText = dateText.replace(' GMT', ' UTC');
const parsed = new Date(cleanDateText);
if (!isNaN(parsed.getTime())) {
// We want the newest expiration date found
if (!expirationDate || parsed > (expirationDate as Date)) {
expirationDate = parsed;
}
}
}
}
});
}
});

View File

@@ -26,14 +26,16 @@ class SteamClientService {
if (platform === 'win32') {
const possiblePaths = [
'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;
} else if (platform === 'linux') {
const possiblePaths = [
path.join(home, '.steam/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;
}
@@ -53,13 +55,27 @@ class SteamClientService {
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) {
this.onAccountsChanged = callback;
const loginUsersPath = this.getLoginUsersPath();
if (loginUsersPath && fs.existsSync(loginUsersPath)) {
this.readLocalAccounts();
chokidar.watch(loginUsersPath, { persistent: true }).on('change', () => {
chokidar.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => {
this.readLocalAccounts();
});
}
@@ -68,79 +84,72 @@ class SteamClientService {
private readLocalAccounts() {
const filePath = this.getLoginUsersPath();
if (!filePath || !fs.existsSync(filePath)) return;
try {
const content = fs.readFileSync(filePath, 'utf-8');
if (!content.trim()) return;
const data = parse(content) as any;
if (!data || !data.users) return;
const accounts: LocalSteamAccount[] = [];
for (const [steamId64, userData] of Object.entries(data.users)) {
const user = userData as any;
if (!user || !user.AccountName) continue;
accounts.push({
steamId: steamId64,
accountName: user.AccountName,
personaName: user.PersonaName,
steamId: steamId64, accountName: user.AccountName,
personaName: user.PersonaName || user.AccountName,
timestamp: parseInt(user.Timestamp) || 0
});
}
if (this.onAccountsChanged) this.onAccountsChanged(accounts);
} catch (error) {
console.error('[SteamClient] Error parsing loginusers.vdf:', error);
}
} catch (error) { }
}
public extractAccountConfig(accountName: string): any | null {
const configPath = this.getConfigVdfPath();
if (!configPath || !fs.existsSync(configPath)) return null;
try {
const content = fs.readFileSync(configPath, 'utf-8');
const data = parse(content) as any;
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
if (accounts && accounts[accountName]) {
return accounts[accountName];
}
} catch (e) {
console.error('[SteamClient] Failed to extract config.vdf data');
}
return null;
return (accounts && accounts[accountName]) ? accounts[accountName] : null;
} catch (e) { return null; }
}
public injectAccountConfig(accountName: string, accountData: any) {
const configPath = this.getConfigVdfPath();
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: {} } } } } };
if (fs.existsSync(configPath)) {
try {
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) { }
}
// Ensure structure exists
if (!data.InstallConfigStore) data.InstallConfigStore = {};
if (!data.InstallConfigStore.Software) data.InstallConfigStore.Software = {};
if (!data.InstallConfigStore.Software.Valve) data.InstallConfigStore.Software.Valve = {};
if (!data.InstallConfigStore.Software.Valve.Steam) data.InstallConfigStore.Software.Valve.Steam = {};
if (!data.InstallConfigStore.Software.Valve.Steam.Accounts) data.InstallConfigStore.Software.Valve.Steam.Accounts = {};
const ensurePath = (obj: any, keys: string[]) => {
let curr = obj;
for (const key of keys) {
if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {};
curr = curr[key];
}
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 {
fs.writeFileSync(configPath, stringify(data));
console.log(`[SteamClient] Injected login config for ${accountName} into config.vdf`);
} catch (e) {
console.error('[SteamClient] Failed to write config.vdf');
}
this.safeWriteVdf(configPath, data);
console.log(`[SteamClient] Safely injected session for ${accountName}`);
} catch (e) { }
}
public async setAutoLoginUser(accountName: string, accountConfig?: any, steamId?: string): Promise<boolean> {
@@ -148,14 +157,12 @@ class SteamClientService {
const loginUsersPath = this.getLoginUsersPath();
if (loginUsersPath) {
const configDir = path.dirname(loginUsersPath);
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
let data: any = { users: {} };
if (fs.existsSync(loginUsersPath)) {
try {
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) { }
}
@@ -164,7 +171,7 @@ class SteamClientService {
let found = false;
for (const [id, user] of Object.entries(data.users)) {
const u = user as any;
if (u.AccountName.toLowerCase() === accountName.toLowerCase()) {
if (u.AccountName?.toLowerCase() === accountName.toLowerCase()) {
u.mostrecent = "1";
u.RememberPassword = "1";
u.AllowAutoLogin = "1";
@@ -177,8 +184,8 @@ class SteamClientService {
}
}
if (!found && steamId) {
console.log(`[SteamClient] Provisioning user ${accountName} into loginusers.vdf`);
if (!found && steamId && accountName) {
console.log(`[SteamClient] Provisioning new user profile for ${accountName}`);
data.users[steamId] = {
AccountName: accountName,
PersonaName: accountName,
@@ -193,16 +200,16 @@ class SteamClientService {
}
try {
fs.writeFileSync(loginUsersPath, stringify(data));
} catch (e) {
console.error('[SteamClient] Failed to write loginusers.vdf');
}
this.safeWriteVdf(loginUsersPath, data);
} catch (e) { }
}
if (accountConfig) {
// Injection of the actual authentication blob
if (accountConfig && accountName) {
this.injectAccountConfig(accountName, accountConfig);
}
// --- Linux Registry / Registry.vdf Hardening ---
if (platform === 'linux') {
const regLocations = [
path.join(os.homedir(), '.steam', 'registry.vdf'),
@@ -210,37 +217,28 @@ class SteamClientService {
];
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)) {
try {
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) { }
} 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;
for (let i = 0; i < keys.length - 1; i++) {
if (!curr[keys[i]!]) curr[keys[i]!] = {};
curr = curr[keys[i]!];
}
curr[keys[keys.length - 1]!] = val;
for (const key of keys) { if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {}; curr = curr[key]; }
return curr;
};
const steamReg = ['Registry', 'HKCU', 'Software', 'Valve', 'Steam'];
setPath(regData, [...steamReg, 'AutoLoginUser'], accountName);
setPath(regData, [...steamReg, 'RememberPassword'], "1");
setPath(regData, [...steamReg, 'AlreadyLoggedIn'], "1");
setPath(regData, [...steamReg, 'WantsOfflineMode'], "0");
try {
fs.writeFileSync(regPath, stringify(regData));
console.log(`[SteamClient] Registry updated: ${regPath}`);
} catch (e) { }
const steamKey = ensurePath(regData, ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']);
steamKey.AutoLoginUser = accountName;
steamKey.RememberPassword = "1";
steamKey.AlreadyLoggedIn = "1";
steamKey.WantsOfflineMode = "0";
try { this.safeWriteVdf(regPath, regData); } catch (e) { }
}
}

View File

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

View File

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

View File

@@ -31,8 +31,27 @@ export const AppThemeProvider: React.FC<{ children: React.ReactNode }> = ({ chil
if (api?.updateServerConfig) {
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]);
return (