3 Commits

Author SHA1 Message Date
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
14 changed files with 409 additions and 652 deletions

View File

@@ -60,48 +60,37 @@ const initBackend = () => {
}; };
// --- System Tray --- // --- System Tray ---
const createTray = () => { const createTray = () => {
// Try to find the icon in various standard locations const assetsDir = path_1.default.join(__dirname, '..', 'assets-build');
const possiblePaths = [ const possibleIcons = ['icon.svg', 'icon.png'];
path_1.default.join(__dirname, '..', 'assets-build'), // Dev let iconPath = '';
path_1.default.join(process.resourcesPath, 'assets-build'), // Packaged (External) for (const name of possibleIcons) {
path_1.default.join(electron_1.app.getAppPath(), 'dist', 'assets-build'), // Packaged (Internal dist) const fullPath = path_1.default.join(assetsDir, name);
path_1.default.join(electron_1.app.getAppPath(), 'assets-build') // Packaged (Internal root) if (fs_1.default.existsSync(fullPath)) {
]; iconPath = fullPath;
let assetsDir = '';
for (const p of possiblePaths) {
if (fs_1.default.existsSync(p)) {
assetsDir = p;
break; break;
} }
} }
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'}`); console.log(`[Tray] Attempting to initialize with icon: ${iconPath || 'NONE FOUND'}`);
if (!iconPath) { if (!iconPath) {
console.warn(`[Tray] FAILED: No valid icon found in searched paths.`); console.warn(`[Tray] FAILED: No valid icon found in ${assetsDir}`);
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);
tray.setToolTip('Ultimate Ban Tracker'); tray.setToolTip('Ultimate Ban Tracker');
tray.on('click', () => { if (mainWindow) { tray.on('click', () => {
mainWindow.show(); if (mainWindow) {
mainWindow.focus(); mainWindow.show();
} }); mainWindow.focus();
}
});
updateTrayMenu(); updateTrayMenu();
console.log(`[Tray] Successfully initialized`);
}
catch (e) {
console.error(`[Tray] Critical error during initialization: ${e.message}`);
} }
catch (e) { }
}; };
const updateTrayMenu = () => { const updateTrayMenu = () => {
if (!tray) if (!tray)
@@ -119,7 +108,11 @@ const updateTrayMenu = () => {
click: () => handleSwitchAccount(acc.loginName) click: () => handleSwitchAccount(acc.loginName)
})) : [{ label: 'No accounts found', enabled: false }] })) : [{ label: 'No accounts found', enabled: false }]
}, },
{ label: 'Sync Now', enabled: !!config?.enabled, click: () => syncAccounts(true) }, {
label: 'Sync Now',
enabled: !!config?.enabled,
click: () => syncAccounts()
},
{ type: 'separator' }, { type: 'separator' },
{ label: 'Show Dashboard', click: () => { if (mainWindow) { label: 'Show Dashboard', click: () => { if (mainWindow)
mainWindow.show(); } }, mainWindow.show(); } },
@@ -162,58 +155,8 @@ const handleSwitchAccount = async (loginName) => {
return false; return false;
} }
}; };
// --- Scraper Helper ---
const scrapeAccountData = async (account) => {
const now = new Date();
try {
const profile = await (0, steam_web_1.fetchProfileData)(account.steamId, account.steamLoginSecure);
const bans = await (0, steam_web_1.scrapeBanStatus)(profile.profileUrl, account.steamLoginSecure);
account.personaName = profile.personaName;
account.profileUrl = profile.profileUrl;
account.vacBanned = bans.vacBanned;
account.gameBans = bans.gameBans;
account.status = (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none';
account.lastBanCheck = now.toISOString();
if (profile.avatar && (!account.localAvatar || profile.avatar !== account.avatar)) {
account.avatar = profile.avatar;
const localPath = await downloadAvatar(account.steamId, profile.avatar);
if (localPath)
account.localAvatar = localPath;
}
if (account.steamLoginSecure) {
try {
const result = await (0, scraper_1.scrapeCooldown)(account.steamId, account.steamLoginSecure);
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, now.toISOString());
}
else {
account.cooldownExpiresAt = undefined;
if (backend)
await backend.pushCooldown(account.steamId, undefined, now.toISOString());
}
}
catch (e) {
if (e.message.includes('cookie') || e.message.includes('Sign In'))
account.authError = true;
}
}
if (backend && !account._id.startsWith('shared_')) {
await backend.shareAccount(account);
}
return true;
}
catch (e) {
console.error(`[Scraper] Failed to scrape ${account.personaName}:`, e);
return false;
}
};
// --- Sync Worker --- // --- Sync Worker ---
const syncAccounts = async (isManual = false) => { const syncAccounts = async () => {
console.log(`[Sync] Phase 1: Pulling from server...`);
initBackend(); initBackend();
let accounts = store.get('accounts'); let accounts = store.get('accounts');
let hasChanges = false; let hasChanges = false;
@@ -224,13 +167,12 @@ const syncAccounts = async (isManual = false) => {
const exists = accounts.find(a => a.steamId === s.steamId); const exists = accounts.find(a => a.steamId === s.steamId);
if (!exists) { if (!exists) {
accounts.push({ accounts.push({
_id: `shared_${s.steamId}`, steamId: s.steamId, personaName: s.personaName, _id: `shared_${s.steamId}`,
avatar: s.avatar, profileUrl: s.profileUrl, vacBanned: s.vacBanned, steamId: s.steamId, personaName: s.personaName, avatar: s.avatar, profileUrl: s.profileUrl,
gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt, vacBanned: s.vacBanned, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginConfig: s.loginConfig,
loginConfig: s.loginConfig, sessionUpdatedAt: s.sessionUpdatedAt, sessionUpdatedAt: s.sessionUpdatedAt, autoCheckCooldown: !!s.steamLoginSecure,
autoCheckCooldown: !!s.steamLoginSecure, status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString()
lastBanCheck: new Date().toISOString(), sharedWith: s.sharedWith
}); });
hasChanges = true; hasChanges = true;
} }
@@ -250,28 +192,8 @@ const syncAccounts = async (isManual = false) => {
exists.sessionUpdatedAt = s.sessionUpdatedAt; exists.sessionUpdatedAt = s.sessionUpdatedAt;
hasChanges = true; hasChanges = true;
} }
// Metadata Sync (Pull) if (s.cooldownExpiresAt && (!exists.cooldownExpiresAt || new Date(s.cooldownExpiresAt) > new Date(exists.cooldownExpiresAt))) {
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;
}
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
exists.sharedWith = s.sharedWith;
hasChanges = true; hasChanges = true;
} }
} }
@@ -285,44 +207,88 @@ const syncAccounts = async (isManual = false) => {
mainWindow.webContents.send('accounts-updated', accounts); mainWindow.webContents.send('accounts-updated', accounts);
updateTrayMenu(); updateTrayMenu();
} }
// Phase 2: Background Scrapes if (accounts.length === 0)
const runScrapes = async () => { return;
console.log(`[Sync] Phase 2: Starting background checks for ${accounts.length} accounts...`); const updatedAccounts = [...accounts];
const currentAccounts = [...store.get('accounts')]; let scrapeChanges = false;
let scrapeChanges = false; for (const account of updatedAccounts) {
for (const account of currentAccounts) { try {
try { const now = new Date();
const now = new Date(); // OPTIMIZATION: Ensure ALL authenticated accounts are shared with the server on every sync cycle
if (backend && !account._id.startsWith('shared_')) // this guarantees that even if a push failed previously, it will be reconciled now.
if (backend && !account._id.startsWith('shared_')) {
console.log(`[Sync] Reconciling account with server: ${account.personaName}`);
await backend.shareAccount(account);
}
const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) {
const profile = await (0, steam_web_1.fetchProfileData)(account.steamId, account.steamLoginSecure);
const bans = await (0, steam_web_1.scrapeBanStatus)(profile.profileUrl, account.steamLoginSecure);
account.personaName = profile.personaName;
account.profileUrl = profile.profileUrl;
account.vacBanned = bans.vacBanned;
account.gameBans = bans.gameBans;
account.status = (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none';
account.lastBanCheck = now.toISOString();
if (profile.avatar && (!account.localAvatar || profile.avatar !== account.avatar)) {
account.avatar = profile.avatar;
const localPath = await downloadAvatar(account.steamId, profile.avatar);
if (localPath)
account.localAvatar = localPath;
}
if (account.loginName) {
const config = steam_client_1.steamClient.extractAccountConfig(account.loginName);
if (config) {
account.loginConfig = config;
account.sessionUpdatedAt = new Date().toISOString();
}
}
if (backend)
await backend.shareAccount(account); await backend.shareAccount(account);
const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0); scrapeChanges = true;
}
if (account.autoCheckCooldown && account.steamLoginSecure) {
if (account.cooldownExpiresAt && new Date(account.cooldownExpiresAt) > now)
continue;
const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0); const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0);
const needsMetadata = (now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName; if ((now.getTime() - lastScrape.getTime()) / 3600000 > 8) {
const needsCooldown = account.autoCheckCooldown && account.steamLoginSecure && (now.getTime() - lastScrape.getTime()) / 3600000 > 8; await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000));
if (needsMetadata || needsCooldown || isManual) { try {
if (!isManual && needsCooldown) const result = await (0, scraper_1.scrapeCooldown)(account.steamId, account.steamLoginSecure);
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 30000) + 5000)); account.authError = false;
if (await scrapeAccountData(account)) account.lastScrapeTime = new Date().toISOString();
if (result.isActive) {
account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString();
if (backend)
await backend.pushCooldown(account.steamId, account.cooldownExpiresAt);
}
else if (account.cooldownExpiresAt) {
account.cooldownExpiresAt = undefined;
if (backend)
await backend.pushCooldown(account.steamId, undefined);
}
scrapeChanges = true; scrapeChanges = true;
}
catch (e) {
if (e.message.includes('cookie') || e.message.includes('Sign In')) {
account.authError = true;
scrapeChanges = true;
}
}
} }
} }
catch (error) { }
} }
if (scrapeChanges) { catch (error) { }
store.set('accounts', currentAccounts); }
if (mainWindow) if (scrapeChanges) {
mainWindow.webContents.send('accounts-updated', currentAccounts); store.set('accounts', updatedAccounts);
updateTrayMenu(); if (mainWindow)
} mainWindow.webContents.send('accounts-updated', updatedAccounts);
console.log('[Sync] Sync cycle finished.'); updateTrayMenu();
}; }
if (isManual)
await runScrapes();
else
runScrapes();
}; };
const scheduleNextSync = () => { const scheduleNextSync = () => {
setTimeout(async () => { await syncAccounts(false); scheduleNextSync(); }, isDev ? 300000 : 1800000); setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, isDev ? 120000 : 1800000);
}; };
// --- Discovery --- // --- Discovery ---
const addingAccounts = new Set(); const addingAccounts = new Set();
@@ -345,20 +311,11 @@ 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()
}); });
@@ -375,7 +332,7 @@ const handleLocalAccountsFound = async (localAccounts) => {
updateTrayMenu(); updateTrayMenu();
} }
}; };
// --- Main Window --- // --- Main Window Creation ---
function createWindow() { function createWindow() {
mainWindow = new electron_1.BrowserWindow({ mainWindow = new electron_1.BrowserWindow({
width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true, width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true,
@@ -394,6 +351,7 @@ function createWindow() {
else else
mainWindow.loadFile(path_1.default.join(__dirname, '..', 'dist', 'index.html')); mainWindow.loadFile(path_1.default.join(__dirname, '..', 'dist', 'index.html'));
} }
// --- App Lifecycle ---
electron_1.app.whenReady().then(() => { electron_1.app.whenReady().then(() => {
electron_1.protocol.handle('steam-resource', (request) => { electron_1.protocol.handle('steam-resource', (request) => {
let rawPath = decodeURIComponent(request.url.replace('steam-resource://', '')); let rawPath = decodeURIComponent(request.url.replace('steam-resource://', ''));
@@ -412,7 +370,7 @@ electron_1.app.whenReady().then(() => {
createWindow(); createWindow();
createTray(); createTray();
initBackend(); initBackend();
setTimeout(() => syncAccounts(false), 5000); setTimeout(syncAccounts, 5000);
scheduleNextSync(); scheduleNextSync();
steam_client_1.steamClient.startWatching(handleLocalAccountsFound); steam_client_1.steamClient.startWatching(handleLocalAccountsFound);
}); });
@@ -439,7 +397,7 @@ electron_1.ipcMain.handle('login-to-server', async () => {
return false; return false;
return new Promise((resolve) => { return new Promise((resolve) => {
const authWindow = new electron_1.BrowserWindow({ const authWindow = new electron_1.BrowserWindow({
width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Server', width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Ban Tracker Server',
webPreferences: { nodeIntegration: false, contextIsolation: true } webPreferences: { nodeIntegration: false, contextIsolation: true }
}); });
authWindow.loadURL(`${config.url}/auth/steam`); authWindow.loadURL(`${config.url}/auth/steam`);
@@ -481,21 +439,7 @@ electron_1.ipcMain.handle('login-to-server', async () => {
}); });
}); });
electron_1.ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId })); electron_1.ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId }));
electron_1.ipcMain.handle('sync-now', async () => { await syncAccounts(true); return true; }); electron_1.ipcMain.handle('sync-now', async () => { await syncAccounts(); return true; });
electron_1.ipcMain.handle('scrape-account', async (event, steamId) => {
const accounts = store.get('accounts');
const account = accounts.find(a => a.steamId === steamId);
if (!account)
return false;
const success = await scrapeAccountData(account);
if (success) {
store.set('accounts', accounts);
if (mainWindow)
mainWindow.webContents.send('accounts-updated', accounts);
updateTrayMenu();
}
return success;
});
electron_1.ipcMain.handle('add-account', async (event, { identifier }) => { electron_1.ipcMain.handle('add-account', async (event, { identifier }) => {
try { try {
initBackend(); initBackend();
@@ -513,7 +457,7 @@ electron_1.ipcMain.handle('add-account', async (event, { identifier }) => {
loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure, loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure,
loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt, loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt,
autoCheckCooldown: !!existing.steamLoginSecure, status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none', autoCheckCooldown: !!existing.steamLoginSecure, status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none',
lastBanCheck: new Date().toISOString(), sharedWith: existing.sharedWith lastBanCheck: new Date().toISOString()
}; };
store.set('accounts', [...accounts, newAccount]); store.set('accounts', [...accounts, newAccount]);
updateTrayMenu(); updateTrayMenu();
@@ -588,54 +532,18 @@ 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) => { electron_1.ipcMain.handle('switch-account', async (event, loginName) => await handleSwitchAccount(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 () => {
console.log('[SteamClient] Preparing for fresh login...');
await killSteam(); await killSteam();
if (process.platform === 'win32') { if (process.platform === 'win32') {
// Clear auto-login registry
const clearReg = 'reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "" /f'; const clearReg = 'reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "" /f';
await new Promise((res) => (0, child_process_1.exec)(clearReg, () => res())); await new Promise((res) => (0, child_process_1.exec)(clearReg, () => res()));
} }
else if (process.platform === 'linux') { else if (process.platform === 'linux') {
// On Linux we can use the steamClient helper to set an empty user
await steam_client_1.steamClient.setAutoLoginUser("", undefined, ""); await steam_client_1.steamClient.setAutoLoginUser("", undefined, "");
} }
const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login'; const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login';
@@ -643,20 +551,34 @@ electron_1.ipcMain.handle('open-steam-app-login', async () => {
return true; return true;
}); });
electron_1.ipcMain.handle('open-steam-login', async (event, expectedSteamId) => { electron_1.ipcMain.handle('open-steam-login', async (event, expectedSteamId) => {
// Use a unique partition per account to prevent session bleeding
const partitionId = expectedSteamId ? `persist:steam-login-${expectedSteamId}` : 'persist:steam-login-new'; const partitionId = expectedSteamId ? `persist:steam-login-${expectedSteamId}` : 'persist:steam-login-new';
const loginSession = electron_1.session.fromPartition(partitionId); const loginSession = electron_1.session.fromPartition(partitionId);
if (!expectedSteamId) // If adding a brand new account, explicitly clear previous trash
if (!expectedSteamId) {
console.log('[Auth] Clearing session for new account login...');
await loginSession.clearStorageData({ storages: ['cookies', 'localstorage', 'indexdb'] }); await loginSession.clearStorageData({ storages: ['cookies', 'localstorage', 'indexdb'] });
}
// If we have an existing cookie string for this account, pre-inject it
if (expectedSteamId) { if (expectedSteamId) {
const accounts = store.get('accounts'); const accounts = store.get('accounts');
const account = accounts.find(a => a.steamId === expectedSteamId); const account = accounts.find(a => a.steamId === expectedSteamId);
if (account?.steamLoginSecure) { if (account?.steamLoginSecure) {
console.log(`[Auth] Pre-injecting existing cookies for ${account.personaName}...`);
const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim()); const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim());
for (const pair of cookiePairs) { for (const pair of cookiePairs) {
const [name, value] = pair.split('='); const [name, value] = pair.split('=');
if (name && value) { if (name && value) {
try { try {
await loginSession.cookies.set({ url: 'https://steamcommunity.com', domain: 'steamcommunity.com', name, value, path: '/', secure: true, httpOnly: name.includes('Secure') }); await loginSession.cookies.set({
url: 'https://steamcommunity.com',
domain: 'steamcommunity.com',
name: name,
value: value,
path: '/',
secure: true,
httpOnly: name.includes('Secure')
});
} }
catch (e) { } catch (e) { }
} }

View File

@@ -19,7 +19,6 @@ electron_1.contextBridge.exposeInMainWorld('electronAPI', {
loginToServer: () => electron_1.ipcRenderer.invoke('login-to-server'), loginToServer: () => electron_1.ipcRenderer.invoke('login-to-server'),
getServerUserInfo: () => electron_1.ipcRenderer.invoke('get-server-user-info'), getServerUserInfo: () => electron_1.ipcRenderer.invoke('get-server-user-info'),
syncNow: () => electron_1.ipcRenderer.invoke('sync-now'), syncNow: () => electron_1.ipcRenderer.invoke('sync-now'),
scrapeAccount: (steamId) => electron_1.ipcRenderer.invoke('scrape-account', steamId),
getCommunityAccounts: () => electron_1.ipcRenderer.invoke('get-community-accounts'), getCommunityAccounts: () => electron_1.ipcRenderer.invoke('get-community-accounts'),
getServerUsers: () => electron_1.ipcRenderer.invoke('get-server-users'), getServerUsers: () => electron_1.ipcRenderer.invoke('get-server-users'),
// Admin API // Admin API

View File

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

View File

@@ -21,8 +21,7 @@ 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;
} }
@@ -30,8 +29,7 @@ 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'), // Flatpak path_1.default.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam')
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;
} }
@@ -49,35 +47,12 @@ 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, ignoreInitial: true }).on('change', () => { chokidar_1.default.watch(loginUsersPath, { persistent: true }).on('change', () => {
console.log(`[SteamClient] loginusers.vdf changed, re-scanning...`);
this.readLocalAccounts(); this.readLocalAccounts();
}); });
} }
@@ -88,20 +63,16 @@ 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 || user.AccountName, personaName: user.PersonaName,
timestamp: parseInt(user.Timestamp) || 0 timestamp: parseInt(user.Timestamp) || 0
}); });
} }
@@ -120,38 +91,32 @@ 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;
return (accounts && accounts[accountName]) ? accounts[accountName] : null; if (accounts && accounts[accountName]) {
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;
let data = { // Create directory if it doesn't exist
InstallConfigStore: { const configDir = path_1.default.dirname(configPath);
Software: { if (!fs_1.default.existsSync(configDir))
Valve: { fs_1.default.mkdirSync(configDir, { recursive: true });
Steam: { let data = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } };
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');
const parsed = (0, simple_vdf_1.parse)(content); data = (0, simple_vdf_1.parse)(content);
if (parsed && typeof parsed === 'object')
data = parsed;
} }
catch (e) { } catch (e) { }
} }
// Ensure safe nesting // Ensure structure exists
if (!data.InstallConfigStore) if (!data.InstallConfigStore)
data.InstallConfigStore = {}; data.InstallConfigStore = {};
if (!data.InstallConfigStore.Software) if (!data.InstallConfigStore.Software)
@@ -164,22 +129,25 @@ 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 {
this.safeWriteVdf(configPath, data); fs_1.default.writeFileSync(configPath, (0, simple_vdf_1.stringify)(data));
console.log(`[SteamClient] Safely injected session for ${accountName}`); console.log(`[SteamClient] Injected login config for ${accountName} into config.vdf`);
}
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');
const parsed = (0, simple_vdf_1.parse)(content); data = (0, simple_vdf_1.parse)(content);
if (parsed && parsed.users)
data = parsed;
} }
catch (e) { } catch (e) { }
} }
@@ -188,7 +156,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";
@@ -201,8 +169,8 @@ class SteamClientService {
u.mostrecent = "0"; u.mostrecent = "0";
} }
} }
if (!found && steamId && accountName) { if (!found && steamId) {
console.log(`[SteamClient] Provisioning new user profile for ${accountName}`); console.log(`[SteamClient] Provisioning user ${accountName} into loginusers.vdf`);
data.users[steamId] = { data.users[steamId] = {
AccountName: accountName, AccountName: accountName,
PersonaName: accountName, PersonaName: accountName,
@@ -216,53 +184,51 @@ class SteamClientService {
}; };
} }
try { try {
this.safeWriteVdf(loginUsersPath, data); fs_1.default.writeFileSync(loginUsersPath, (0, simple_vdf_1.stringify)(data));
}
catch (e) {
console.error('[SteamClient] Failed to write loginusers.vdf');
} }
catch (e) { }
} }
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) {
if (!fs_1.default.existsSync(path_1.default.dirname(regPath))) let regData = { Registry: { HKCU: { Software: { Valve: { Steam: {} } } } } };
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');
const parsed = (0, simple_vdf_1.parse)(content); regData = (0, simple_vdf_1.parse)(content);
if (parsed && typeof parsed === 'object')
regData = parsed;
} }
catch (e) { } catch (e) { }
} }
// Deep merge helper else {
const ensurePath = (obj, keys) => { 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) => {
let curr = obj; let curr = obj;
for (const key of keys) { for (let i = 0; i < keys.length - 1; i++) {
if (!curr[key] || typeof curr[key] !== 'object') if (!curr[keys[i]])
curr[key] = {}; curr[keys[i]] = {};
curr = curr[key]; curr = curr[keys[i]];
} }
return curr; curr[keys[keys.length - 1]] = val;
}; };
const steamKey = ensurePath(regData, ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']); const steamReg = ['Registry', 'HKCU', 'Software', 'Valve', 'Steam'];
steamKey.AutoLoginUser = accountName; setPath(regData, [...steamReg, 'AutoLoginUser'], accountName);
steamKey.RememberPassword = "1"; setPath(regData, [...steamReg, 'RememberPassword'], "1");
steamKey.AlreadyLoggedIn = "1"; setPath(regData, [...steamReg, 'AlreadyLoggedIn'], "1");
steamKey.WantsOfflineMode = "0"; setPath(regData, [...steamReg, 'WantsOfflineMode'], "0");
try { try {
this.safeWriteVdf(regPath, regData); fs_1.default.writeFileSync(regPath, (0, simple_vdf_1.stringify)(regData));
console.log(`[SteamClient] Registry updated: ${regPath}`);
} }
catch (e) { } catch (e) { }
} }

View File

@@ -40,7 +40,6 @@ interface Account {
cooldownExpiresAt?: string; cooldownExpiresAt?: string;
authError?: boolean; authError?: boolean;
notes?: string; notes?: string;
sharedWith?: any[];
} }
interface ServerConfig { interface ServerConfig {
@@ -49,7 +48,6 @@ interface ServerConfig {
serverSteamId?: string; serverSteamId?: string;
enabled: boolean; enabled: boolean;
theme?: string; theme?: string;
isAdmin?: boolean;
} }
// --- App State --- // --- App State ---
@@ -93,40 +91,22 @@ const initBackend = () => {
// --- System Tray --- // --- System Tray ---
const createTray = () => { const createTray = () => {
// Try to find the icon in various standard locations const assetsDir = path.join(__dirname, '..', 'assets-build');
const possiblePaths = [ const possibleIcons = ['icon.svg', 'icon.png'];
path.join(__dirname, '..', 'assets-build'), // Dev let iconPath = '';
path.join(process.resourcesPath, 'assets-build'), // Packaged (External)
path.join(app.getAppPath(), 'dist', 'assets-build'), // Packaged (Internal dist)
path.join(app.getAppPath(), 'assets-build') // Packaged (Internal root)
];
let assetsDir = ''; for (const name of possibleIcons) {
for (const p of possiblePaths) { const fullPath = path.join(assetsDir, name);
if (fs.existsSync(p)) { if (fs.existsSync(fullPath)) {
assetsDir = p; iconPath = fullPath;
break; break;
} }
} }
const possibleIcons = ['icon.png', 'icon.svg'];
let iconPath = '';
if (assetsDir) {
for (const name of possibleIcons) {
const fullPath = path.join(assetsDir, name);
if (fs.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'}`); console.log(`[Tray] Attempting to initialize with icon: ${iconPath || 'NONE FOUND'}`);
if (!iconPath) { if (!iconPath) {
console.warn(`[Tray] FAILED: No valid icon found in searched paths.`); console.warn(`[Tray] FAILED: No valid icon found in ${assetsDir}`);
return; return;
} }
@@ -134,15 +114,24 @@ const createTray = () => {
const icon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 }); const icon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
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(); updateTrayMenu();
} catch (e) { } console.log(`[Tray] Successfully initialized`);
} catch (e: any) {
console.error(`[Tray] Critical error during initialization: ${e.message}`);
}
}; };
const updateTrayMenu = () => { const updateTrayMenu = () => {
if (!tray) return; if (!tray) return;
const accounts = store.get('accounts') as Account[]; const accounts = store.get('accounts') as Account[];
const config = store.get('serverConfig'); const config = store.get('serverConfig');
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ label: `Ultimate Ban Tracker v${app.getVersion()}`, enabled: false }, { label: `Ultimate Ban Tracker v${app.getVersion()}`, enabled: false },
{ type: 'separator' }, { type: 'separator' },
@@ -154,11 +143,16 @@ const updateTrayMenu = () => {
click: () => handleSwitchAccount(acc.loginName) click: () => handleSwitchAccount(acc.loginName)
})) : [{ label: 'No accounts found', enabled: false }] })) : [{ label: 'No accounts found', enabled: false }]
}, },
{ label: 'Sync Now', enabled: !!config?.enabled, click: () => syncAccounts(true) }, {
label: 'Sync Now',
enabled: !!config?.enabled,
click: () => syncAccounts()
},
{ type: 'separator' }, { type: 'separator' },
{ label: 'Show Dashboard', click: () => { if (mainWindow) mainWindow.show(); } }, { label: 'Show Dashboard', click: () => { if (mainWindow) mainWindow.show(); } },
{ label: 'Quit', click: () => { (app as any).isQuitting = true; app.quit(); } } { label: 'Quit', click: () => { (app as any).isQuitting = true; app.quit(); } }
]); ]);
tray.setContextMenu(contextMenu); tray.setContextMenu(contextMenu);
}; };
@@ -194,49 +188,8 @@ const handleSwitchAccount = async (loginName: string) => {
} catch (e) { return false; } } catch (e) { return false; }
}; };
// --- Scraper Helper ---
const scrapeAccountData = async (account: Account) => {
const now = new Date();
try {
const profile = await fetchProfileData(account.steamId, account.steamLoginSecure);
const bans = await scrapeBanStatus(profile.profileUrl, account.steamLoginSecure);
account.personaName = profile.personaName; account.profileUrl = profile.profileUrl;
account.vacBanned = bans.vacBanned; account.gameBans = bans.gameBans;
account.status = (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none';
account.lastBanCheck = now.toISOString();
if (profile.avatar && (!account.localAvatar || profile.avatar !== account.avatar)) {
account.avatar = profile.avatar;
const localPath = await downloadAvatar(account.steamId, profile.avatar);
if (localPath) account.localAvatar = localPath;
}
if (account.steamLoginSecure) {
try {
const result = await scrapeCooldown(account.steamId, account.steamLoginSecure);
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, now.toISOString());
} else {
account.cooldownExpiresAt = 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;
}
}
if (backend && !account._id.startsWith('shared_')) {
await backend.shareAccount(account);
}
return true;
} catch (e) {
console.error(`[Scraper] Failed to scrape ${account.personaName}:`, e);
return false;
}
};
// --- Sync Worker --- // --- Sync Worker ---
const syncAccounts = async (isManual = false) => { const syncAccounts = async () => {
console.log(`[Sync] Phase 1: Pulling from server...`);
initBackend(); initBackend();
let accounts = store.get('accounts') as Account[]; let accounts = store.get('accounts') as Account[];
let hasChanges = false; let hasChanges = false;
@@ -248,13 +201,12 @@ const syncAccounts = async (isManual = false) => {
const exists = accounts.find(a => a.steamId === s.steamId); const exists = accounts.find(a => a.steamId === s.steamId);
if (!exists) { if (!exists) {
accounts.push({ accounts.push({
_id: `shared_${s.steamId}`, steamId: s.steamId, personaName: s.personaName, _id: `shared_${s.steamId}`,
avatar: s.avatar, profileUrl: s.profileUrl, vacBanned: s.vacBanned, steamId: s.steamId, personaName: s.personaName, avatar: s.avatar, profileUrl: s.profileUrl,
gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt, vacBanned: s.vacBanned, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginConfig: s.loginConfig,
loginConfig: s.loginConfig, sessionUpdatedAt: s.sessionUpdatedAt, sessionUpdatedAt: s.sessionUpdatedAt, autoCheckCooldown: !!s.steamLoginSecure,
autoCheckCooldown: !!s.steamLoginSecure, status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString()
lastBanCheck: new Date().toISOString(), sharedWith: s.sharedWith
}); });
hasChanges = true; hasChanges = true;
} else { } else {
@@ -267,31 +219,8 @@ 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;
}
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
exists.sharedWith = s.sharedWith;
hasChanges = true; hasChanges = true;
} }
} }
@@ -305,39 +234,76 @@ const syncAccounts = async (isManual = false) => {
updateTrayMenu(); updateTrayMenu();
} }
// Phase 2: Background Scrapes if (accounts.length === 0) return;
const runScrapes = async () => {
console.log(`[Sync] Phase 2: Starting background checks for ${accounts.length} accounts...`);
const currentAccounts = [...store.get('accounts') as Account[]];
let scrapeChanges = false;
for (const account of currentAccounts) {
try {
const now = new Date();
if (backend && !account._id.startsWith('shared_')) await backend.shareAccount(account);
const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0);
const needsMetadata = (now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName;
const needsCooldown = account.autoCheckCooldown && account.steamLoginSecure && (now.getTime() - lastScrape.getTime()) / 3600000 > 8;
if (needsMetadata || needsCooldown || isManual) { const updatedAccounts = [...accounts];
if (!isManual && needsCooldown) await new Promise(r => setTimeout(r, Math.floor(Math.random() * 30000) + 5000)); let scrapeChanges = false;
if (await scrapeAccountData(account)) scrapeChanges = true;
for (const account of updatedAccounts) {
try {
const now = new Date();
// OPTIMIZATION: Ensure ALL authenticated accounts are shared with the server on every sync cycle
// this guarantees that even if a push failed previously, it will be reconciled now.
if (backend && !account._id.startsWith('shared_')) {
console.log(`[Sync] Reconciling account with server: ${account.personaName}`);
await backend.shareAccount(account);
}
const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) {
const profile = await fetchProfileData(account.steamId, account.steamLoginSecure);
const bans = await scrapeBanStatus(profile.profileUrl, account.steamLoginSecure);
account.personaName = profile.personaName; account.profileUrl = profile.profileUrl;
account.vacBanned = bans.vacBanned; account.gameBans = bans.gameBans;
account.status = (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none';
account.lastBanCheck = now.toISOString();
if (profile.avatar && (!account.localAvatar || profile.avatar !== account.avatar)) {
account.avatar = profile.avatar;
const localPath = await downloadAvatar(account.steamId, profile.avatar);
if (localPath) account.localAvatar = localPath;
} }
} catch (error) { } if (account.loginName) {
} const config = steamClient.extractAccountConfig(account.loginName);
if (scrapeChanges) { if (config) { account.loginConfig = config; account.sessionUpdatedAt = new Date().toISOString(); }
store.set('accounts', currentAccounts); }
if (mainWindow) mainWindow.webContents.send('accounts-updated', currentAccounts); if (backend) await backend.shareAccount(account);
updateTrayMenu(); scrapeChanges = true;
} }
console.log('[Sync] Sync cycle finished.');
};
if (isManual) await runScrapes(); else runScrapes(); if (account.autoCheckCooldown && account.steamLoginSecure) {
if (account.cooldownExpiresAt && new Date(account.cooldownExpiresAt) > now) continue;
const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0);
if ((now.getTime() - lastScrape.getTime()) / 3600000 > 8) {
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000));
try {
const result = await scrapeCooldown(account.steamId, account.steamLoginSecure);
account.authError = false; account.lastScrapeTime = new Date().toISOString();
if (result.isActive) {
account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString();
if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt);
} else if (account.cooldownExpiresAt) {
account.cooldownExpiresAt = undefined;
if (backend) await backend.pushCooldown(account.steamId, undefined);
}
scrapeChanges = true;
} catch (e: any) {
if (e.message.includes('cookie') || e.message.includes('Sign In')) { account.authError = true; scrapeChanges = true; }
}
}
}
} catch (error) { }
}
if (scrapeChanges) {
store.set('accounts', updatedAccounts);
if (mainWindow) mainWindow.webContents.send('accounts-updated', updatedAccounts);
updateTrayMenu();
}
}; };
const scheduleNextSync = () => { const scheduleNextSync = () => {
setTimeout(async () => { await syncAccounts(false); scheduleNextSync(); }, isDev ? 300000 : 1800000); setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, isDev ? 120000 : 1800000);
}; };
// --- Discovery --- // --- Discovery ---
@@ -356,21 +322,11 @@ 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()
}); });
@@ -386,21 +342,28 @@ const handleLocalAccountsFound = async (localAccounts: LocalSteamAccount[]) => {
} }
}; };
// --- Main Window --- // --- Main Window Creation ---
function createWindow() { function createWindow() {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true, width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true,
webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true } webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true }
}); });
mainWindow.setMenu(null); mainWindow.setMenu(null);
mainWindow.on('close', (event) => { mainWindow.on('close', (event) => {
if (!(app as any).isQuitting) { event.preventDefault(); mainWindow?.hide(); } if (!(app as any).isQuitting) {
event.preventDefault();
mainWindow?.hide();
}
return false; return false;
}); });
if (isDev) mainWindow.loadURL('http://localhost:5173'); if (isDev) mainWindow.loadURL('http://localhost:5173');
else mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); else mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html'));
} }
// --- App Lifecycle ---
app.whenReady().then(() => { app.whenReady().then(() => {
protocol.handle('steam-resource', (request) => { protocol.handle('steam-resource', (request) => {
let rawPath = decodeURIComponent(request.url.replace('steam-resource://', '')); let rawPath = decodeURIComponent(request.url.replace('steam-resource://', ''));
@@ -409,10 +372,11 @@ app.whenReady().then(() => {
if (!fs.existsSync(absolutePath)) return new Response('Not Found', { status: 404 }); if (!fs.existsSync(absolutePath)) return new Response('Not Found', { status: 404 });
try { return net.fetch(pathToFileURL(absolutePath).toString()); } catch (e) { return new Response('Error', { status: 500 }); } try { return net.fetch(pathToFileURL(absolutePath).toString()); } catch (e) { return new Response('Error', { status: 500 }); }
}); });
createWindow(); createWindow();
createTray(); createTray();
initBackend(); initBackend();
setTimeout(() => syncAccounts(false), 5000); setTimeout(syncAccounts, 5000);
scheduleNextSync(); scheduleNextSync();
steamClient.startWatching(handleLocalAccountsFound); steamClient.startWatching(handleLocalAccountsFound);
}); });
@@ -437,17 +401,19 @@ ipcMain.handle('login-to-server', async () => {
if (!config.url) return false; if (!config.url) return false;
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
const authWindow = new BrowserWindow({ const authWindow = new BrowserWindow({
width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Server', width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Ban Tracker Server',
webPreferences: { nodeIntegration: false, contextIsolation: true } webPreferences: { nodeIntegration: false, contextIsolation: true }
}); });
authWindow.loadURL(`${config.url}/auth/steam`); authWindow.loadURL(`${config.url}/auth/steam`);
let captured = false; let captured = false;
const saveServerAuth = (token: string) => { const saveServerAuth = (token: string) => {
if (captured) return; captured = true; if (captured) return; captured = true;
let serverSteamId = undefined; let isAdmin = false; let serverSteamId = undefined;
let isAdmin = false;
try { try {
const payload = JSON.parse(Buffer.from(token.split('.')[1]!, 'base64').toString()); const payload = JSON.parse(Buffer.from(token.split('.')[1]!, 'base64').toString());
serverSteamId = payload.steamId; isAdmin = !!payload.isAdmin; serverSteamId = payload.steamId;
isAdmin = !!payload.isAdmin;
} catch (e) {} } catch (e) {}
const current = store.get('serverConfig'); const current = store.get('serverConfig');
store.set('serverConfig', { ...current, token, serverSteamId, isAdmin, enabled: true }); store.set('serverConfig', { ...current, token, serverSteamId, isAdmin, enabled: true });
@@ -470,21 +436,7 @@ ipcMain.handle('login-to-server', async () => {
}); });
ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId })); ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId }));
ipcMain.handle('sync-now', async () => { await syncAccounts(true); return true; }); ipcMain.handle('sync-now', async () => { await syncAccounts(); return true; });
ipcMain.handle('scrape-account', async (event, steamId: string) => {
const accounts = store.get('accounts') as Account[];
const account = accounts.find(a => a.steamId === steamId);
if (!account) return false;
const success = await scrapeAccountData(account);
if (success) {
store.set('accounts', accounts);
if (mainWindow) mainWindow.webContents.send('accounts-updated', accounts);
updateTrayMenu();
}
return success;
});
ipcMain.handle('add-account', async (event, { identifier }) => { ipcMain.handle('add-account', async (event, { identifier }) => {
try { try {
initBackend(); initBackend();
@@ -501,7 +453,7 @@ ipcMain.handle('add-account', async (event, { identifier }) => {
loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure, loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure,
loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt, loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt,
autoCheckCooldown: !!existing.steamLoginSecure, status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none', autoCheckCooldown: !!existing.steamLoginSecure, status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none',
lastBanCheck: new Date().toISOString(), sharedWith: existing.sharedWith lastBanCheck: new Date().toISOString()
}; };
store.set('accounts', [...accounts, newAccount]); store.set('accounts', [...accounts, newAccount]);
updateTrayMenu(); updateTrayMenu();
@@ -571,74 +523,64 @@ 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) => { ipcMain.handle('switch-account', async (event, loginName: string) => await handleSwitchAccount(loginName));
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 () => {
console.log('[SteamClient] Preparing for fresh login...');
await killSteam(); await killSteam();
if (process.platform === 'win32') { if (process.platform === 'win32') {
// Clear auto-login registry
const clearReg = 'reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "" /f'; const clearReg = 'reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "" /f';
await new Promise<void>((res) => exec(clearReg, () => res())); await new Promise<void>((res) => exec(clearReg, () => res()));
} else if (process.platform === 'linux') { } else if (process.platform === 'linux') {
// On Linux we can use the steamClient helper to set an empty user
await steamClient.setAutoLoginUser("", undefined, ""); await steamClient.setAutoLoginUser("", undefined, "");
} }
const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login'; const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login';
exec(command); exec(command);
return true; return true;
}); });
ipcMain.handle('open-steam-login', async (event, expectedSteamId: string) => { ipcMain.handle('open-steam-login', async (event, expectedSteamId: string) => {
// Use a unique partition per account to prevent session bleeding
const partitionId = expectedSteamId ? `persist:steam-login-${expectedSteamId}` : 'persist:steam-login-new'; const partitionId = expectedSteamId ? `persist:steam-login-${expectedSteamId}` : 'persist:steam-login-new';
const loginSession = session.fromPartition(partitionId); const loginSession = session.fromPartition(partitionId);
if (!expectedSteamId) await loginSession.clearStorageData({ storages: ['cookies', 'localstorage', 'indexdb'] });
// If adding a brand new account, explicitly clear previous trash
if (!expectedSteamId) {
console.log('[Auth] Clearing session for new account login...');
await loginSession.clearStorageData({ storages: ['cookies', 'localstorage', 'indexdb'] });
}
// If we have an existing cookie string for this account, pre-inject it
if (expectedSteamId) { if (expectedSteamId) {
const accounts = store.get('accounts') as Account[]; const accounts = store.get('accounts') as Account[];
const account = accounts.find(a => a.steamId === expectedSteamId); const account = accounts.find(a => a.steamId === expectedSteamId);
if (account?.steamLoginSecure) { if (account?.steamLoginSecure) {
console.log(`[Auth] Pre-injecting existing cookies for ${account.personaName}...`);
const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim()); const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim());
for (const pair of cookiePairs) { for (const pair of cookiePairs) {
const [name, value] = pair.split('='); const [name, value] = pair.split('=');
if (name && value) { if (name && value) {
try { await loginSession.cookies.set({ url: 'https://steamcommunity.com', domain: 'steamcommunity.com', name, value, path: '/', secure: true, httpOnly: name.includes('Secure') }); } catch (e) {} try {
await loginSession.cookies.set({
url: 'https://steamcommunity.com',
domain: 'steamcommunity.com',
name: name,
value: value,
path: '/',
secure: true,
httpOnly: name.includes('Secure')
});
} catch (e) {}
} }
} }
} }
} }
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
const loginWindow = new BrowserWindow({ const loginWindow = new BrowserWindow({
width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Steam', width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Steam',

View File

@@ -19,7 +19,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
loginToServer: () => ipcRenderer.invoke('login-to-server'), loginToServer: () => ipcRenderer.invoke('login-to-server'),
getServerUserInfo: () => ipcRenderer.invoke('get-server-user-info'), getServerUserInfo: () => ipcRenderer.invoke('get-server-user-info'),
syncNow: () => ipcRenderer.invoke('sync-now'), syncNow: () => ipcRenderer.invoke('sync-now'),
scrapeAccount: (steamId: string) => ipcRenderer.invoke('scrape-account', steamId),
getCommunityAccounts: () => ipcRenderer.invoke('get-community-accounts'), getCommunityAccounts: () => ipcRenderer.invoke('get-community-accounts'),
getServerUsers: () => ipcRenderer.invoke('get-server-users'), getServerUsers: () => ipcRenderer.invoke('get-server-users'),

View File

@@ -62,22 +62,18 @@ 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, lastScrapeTime?: string) { public async pushCooldown(steamId: string, cooldownExpiresAt?: 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,25 +29,20 @@ 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') || h.includes('Cooldown Expiration')); const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration'));
if (expirationIndex !== -1) { if (expirationIndex !== -1) {
const rows = $(table).find('tr').not(':has(th)'); const firstRow = $(table).find('tr').not(':has(th)').first();
rows.each((_, row) => { const dateText = firstRow.find('td').eq(expirationIndex).text().trim();
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())) { if (dateText && dateText !== '') {
// We want the newest expiration date found const cleanDateText = dateText.replace(' GMT', ' UTC');
if (!expirationDate || parsed > (expirationDate as Date)) { const parsed = new Date(cleanDateText);
expirationDate = parsed;
} if (!isNaN(parsed.getTime())) {
} expirationDate = parsed;
} }
}); }
} }
}); });

View File

@@ -26,16 +26,14 @@ 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'), // Flatpak path.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam')
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;
} }
@@ -55,36 +53,13 @@ class SteamClientService {
return path.join(this.steamPath, 'config', 'config.vdf'); return path.join(this.steamPath, 'config', 'config.vdf');
} }
/**
* Safe Atomic Write: Writes to a temp file and renames it.
* This prevents file corruption if the app crashes during write.
*/
private safeWriteVdf(filePath: string, data: any) {
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');
// Atomic rename
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, ignoreInitial: true }).on('change', () => { chokidar.watch(loginUsersPath, { persistent: true }).on('change', () => {
console.log(`[SteamClient] loginusers.vdf changed, re-scanning...`);
this.readLocalAccounts(); this.readLocalAccounts();
}); });
} }
@@ -96,20 +71,16 @@ class SteamClientService {
try { try {
const content = fs.readFileSync(filePath, 'utf-8'); const content = fs.readFileSync(filePath, 'utf-8');
if (!content.trim()) return; // Empty file
const data = parse(content) as any; const data = parse(content) as any;
if (!data || !data.users) return; if (!data || !data.users) return;
const accounts: LocalSteamAccount[] = []; const accounts: LocalSteamAccount[] = [];
for (const [steamId64, userData] of Object.entries(data.users)) { for (const [steamId64, userData] of Object.entries(data.users)) {
const user = userData as any; const user = userData as any;
if (!user || !user.AccountName) continue;
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
}); });
} }
@@ -127,39 +98,35 @@ class SteamClientService {
try { try {
const content = fs.readFileSync(configPath, 'utf-8'); const content = fs.readFileSync(configPath, 'utf-8');
const data = parse(content) as any; const data = parse(content) as any;
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts; const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
return (accounts && accounts[accountName]) ? accounts[accountName] : null; if (accounts && accounts[accountName]) {
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;
} }
public injectAccountConfig(accountName: string, accountData: any) { public injectAccountConfig(accountName: string, accountData: any) {
const configPath = this.getConfigVdfPath(); const configPath = this.getConfigVdfPath();
if (!configPath) return; if (!configPath) return;
let data: any = { // Create directory if it doesn't exist
InstallConfigStore: { const configDir = path.dirname(configPath);
Software: { if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
Valve: {
Steam: { let data: any = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } };
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');
const parsed = parse(content) as any; data = parse(content) as any;
if (parsed && typeof parsed === 'object') data = parsed;
} catch (e) { } } catch (e) { }
} }
// Ensure safe nesting // Ensure structure exists
if (!data.InstallConfigStore) data.InstallConfigStore = {}; if (!data.InstallConfigStore) data.InstallConfigStore = {};
if (!data.InstallConfigStore.Software) data.InstallConfigStore.Software = {}; if (!data.InstallConfigStore.Software) data.InstallConfigStore.Software = {};
if (!data.InstallConfigStore.Software.Valve) data.InstallConfigStore.Software.Valve = {}; if (!data.InstallConfigStore.Software.Valve) data.InstallConfigStore.Software.Valve = {};
@@ -169,9 +136,11 @@ class SteamClientService {
data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData; data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData;
try { try {
this.safeWriteVdf(configPath, data); fs.writeFileSync(configPath, stringify(data));
console.log(`[SteamClient] Safely injected session for ${accountName}`); console.log(`[SteamClient] Injected login config for ${accountName} into config.vdf`);
} 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> {
@@ -179,12 +148,14 @@ 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');
const parsed = parse(content) as any; data = parse(content) as any;
if (parsed && parsed.users) data = parsed;
} catch (e) { } } catch (e) { }
} }
@@ -193,7 +164,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";
@@ -206,8 +177,8 @@ class SteamClientService {
} }
} }
if (!found && steamId && accountName) { if (!found && steamId) {
console.log(`[SteamClient] Provisioning new user profile for ${accountName}`); console.log(`[SteamClient] Provisioning user ${accountName} into loginusers.vdf`);
data.users[steamId] = { data.users[steamId] = {
AccountName: accountName, AccountName: accountName,
PersonaName: accountName, PersonaName: accountName,
@@ -222,15 +193,16 @@ class SteamClientService {
} }
try { try {
this.safeWriteVdf(loginUsersPath, data); fs.writeFileSync(loginUsersPath, stringify(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.join(os.homedir(), '.steam', 'registry.vdf'), path.join(os.homedir(), '.steam', 'registry.vdf'),
@@ -238,40 +210,36 @@ class SteamClientService {
]; ];
for (const regPath of regLocations) { for (const regPath of regLocations) {
if (!fs.existsSync(path.dirname(regPath))) continue; let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: {} } } } } };
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');
const parsed = parse(content) as any; regData = 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 });
} }
// Deep merge helper const setPath = (obj: any, keys: string[], val: string) => {
const ensurePath = (obj: any, keys: string[]) => {
let curr = obj; let curr = obj;
for (const key of keys) { for (let i = 0; i < keys.length - 1; i++) {
if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {}; if (!curr[keys[i]!]) curr[keys[i]!] = {};
curr = curr[key]; curr = curr[keys[i]!];
} }
return curr; curr[keys[keys.length - 1]!] = val;
}; };
const steamKey = ensurePath(regData, ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']); const steamReg = ['Registry', 'HKCU', 'Software', 'Valve', 'Steam'];
steamKey.AutoLoginUser = accountName; setPath(regData, [...steamReg, 'AutoLoginUser'], accountName);
steamKey.RememberPassword = "1"; setPath(regData, [...steamReg, 'RememberPassword'], "1");
steamKey.AlreadyLoggedIn = "1"; setPath(regData, [...steamReg, 'AlreadyLoggedIn'], "1");
steamKey.WantsOfflineMode = "0"; setPath(regData, [...steamReg, 'WantsOfflineMode'], "0");
try { try {
this.safeWriteVdf(regPath, regData); fs.writeFileSync(regPath, stringify(regData));
console.log(`[SteamClient] Registry updated: ${regPath}`);
} catch (e) { } } catch (e) { }
} }
} }

View File

@@ -1,12 +1,12 @@
{ {
"name": "ultimate-ban-tracker-desktop", "name": "ultimate-ban-tracker-desktop",
"version": "1.3.2", "version": "1.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ultimate-ban-tracker-desktop", "name": "ultimate-ban-tracker-desktop",
"version": "1.3.2", "version": "1.2.0",
"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.2", "version": "1.2.0",
"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",
@@ -28,8 +28,7 @@
}, },
"files": [ "files": [
"dist/**/*", "dist/**/*",
"dist-electron/**/*", "dist-electron/**/*"
"assets-build/**/*"
], ],
"linux": { "linux": {
"target": [ "target": [

View File

@@ -49,7 +49,6 @@ interface AccountsContextType {
updateServerConfig: (config: Partial<ServerConfig>) => Promise<void>; updateServerConfig: (config: Partial<ServerConfig>) => Promise<void>;
loginToServer: () => Promise<void>; loginToServer: () => Promise<void>;
syncNow: () => Promise<void>; syncNow: () => Promise<void>;
scrapeAccount: (steamId: string) => Promise<boolean>;
getCommunityAccounts: () => Promise<any[]>; getCommunityAccounts: () => Promise<any[]>;
getServerUsers: () => Promise<any[]>; getServerUsers: () => Promise<any[]>;
refreshAccounts: (showLoading?: boolean) => Promise<void>; refreshAccounts: (showLoading?: boolean) => Promise<void>;
@@ -115,12 +114,6 @@ export const AccountsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
} }
}; };
const scrapeAccount = async (steamId: string) => {
const success = await (window as any).electronAPI.scrapeAccount(steamId);
if (success) await syncNow();
return success;
};
const addAccount = async (data: { identifier: string }) => { const addAccount = async (data: { identifier: string }) => {
await (window as any).electronAPI.addAccount(data); await (window as any).electronAPI.addAccount(data);
await refreshAccounts(); await refreshAccounts();
@@ -201,7 +194,7 @@ export const AccountsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
accounts, serverConfig, isLoading, isSyncing, addAccount, updateAccount, deleteAccount, accounts, serverConfig, isLoading, isSyncing, addAccount, updateAccount, deleteAccount,
switchAccount, openSteamAppLogin, openSteamLogin, updateServerConfig, loginToServer, switchAccount, openSteamAppLogin, openSteamLogin, updateServerConfig, loginToServer,
getCommunityAccounts, getServerUsers, shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, syncNow, refreshAccounts, getCommunityAccounts, getServerUsers, shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, syncNow, refreshAccounts,
scrapeAccount, adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount
}}> }}>
{children} {children}
</AccountsContext.Provider> </AccountsContext.Provider>

View File

@@ -376,12 +376,11 @@ const AccountRow: React.FC<{
onSwitch: (login: string) => void, onSwitch: (login: string) => void,
onAuth: () => void onAuth: () => void
}> = ({ account, onDelete, onSwitch, onAuth }) => { }> = ({ account, onDelete, onSwitch, onAuth }) => {
const { shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, getServerUsers, serverConfig, scrapeAccount } = useAccounts(); const { shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, getServerUsers, serverConfig } = useAccounts();
const [timeLeft, setTimeLeft] = useState<string | null>(null); const [timeLeft, setTimeLeft] = useState<string | null>(null);
const [isShareOpen, setIsShareOpen] = useState(false); const [isShareOpen, setIsShareOpen] = useState(false);
const [targetUserId, setTargetUserId] = useState(''); const [targetUserId, setTargetUserId] = useState('');
const [isSharing, setIsSharing] = useState(false); const [isSharing, setIsSharing] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [serverUsers, setServerUsers] = useState<any[]>([]); const [serverUsers, setServerUsers] = useState<any[]>([]);
const cooldownDate = account?.cooldownExpiresAt ? new Date(account.cooldownExpiresAt) : null; const cooldownDate = account?.cooldownExpiresAt ? new Date(account.cooldownExpiresAt) : null;
@@ -405,12 +404,6 @@ const AccountRow: React.FC<{
const [imgSrc, setImgSrc] = useState(avatarSrc); const [imgSrc, setImgSrc] = useState(avatarSrc);
useEffect(() => { setImgSrc(avatarSrc); }, [avatarSrc]); useEffect(() => { setImgSrc(avatarSrc); }, [avatarSrc]);
const handleRefresh = async () => {
setIsRefreshing(true);
await scrapeAccount(account.steamId);
setIsRefreshing(false);
};
const handleOpenShare = async () => { const handleOpenShare = async () => {
setIsShareOpen(true); setIsShareOpen(true);
try { try {
@@ -529,12 +522,7 @@ const AccountRow: React.FC<{
{account.steamLoginSecure && !account.authError ? <VerifiedUserIcon fontSize="inherit" /> : (account.authError ? <LockResetIcon fontSize="inherit" /> : <BoltIcon fontSize="inherit" />)} {account.steamLoginSecure && !account.authError ? <VerifiedUserIcon fontSize="inherit" /> : (account.authError ? <LockResetIcon fontSize="inherit" /> : <BoltIcon fontSize="inherit" />)}
</IconButton> </IconButton>
{account.steamLoginSecure && !account.authError && ( {account.steamLoginSecure && !account.authError && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> <Typography variant="caption" sx={{ color: 'success.main', fontWeight: 'bold', fontSize: '0.6rem' }}>TRACKING</Typography>
<Typography variant="caption" sx={{ color: 'success.main', fontWeight: 'bold', fontSize: '0.6rem' }}>TRACKING</Typography>
<IconButton size="small" onClick={handleRefresh} disabled={isRefreshing} sx={{ p: 0.2, color: 'text.secondary', '&:hover': { color: 'primary.main' } }}>
{isRefreshing ? <CircularProgress size={10} color="inherit" /> : <SyncIcon sx={{ fontSize: 12 }} />}
</IconButton>
</Box>
)} )}
</Box> </Box>
</Tooltip> </Tooltip>