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
8 changed files with 273 additions and 284 deletions

View File

@@ -70,19 +70,27 @@ const createTray = () => {
break;
}
}
if (!iconPath)
console.log(`[Tray] Attempting to initialize with icon: ${iconPath || 'NONE FOUND'}`);
if (!iconPath) {
console.warn(`[Tray] FAILED: No valid icon found in ${assetsDir}`);
return;
}
try {
const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
tray = new electron_1.Tray(icon);
tray.setToolTip('Ultimate Ban Tracker');
tray.on('click', () => { if (mainWindow) {
mainWindow.show();
mainWindow.focus();
} });
tray.on('click', () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
});
updateTrayMenu();
console.log(`[Tray] Successfully initialized`);
}
catch (e) {
console.error(`[Tray] Critical error during initialization: ${e.message}`);
}
catch (e) { }
};
const updateTrayMenu = () => {
if (!tray)
@@ -100,7 +108,11 @@ const updateTrayMenu = () => {
click: () => handleSwitchAccount(acc.loginName)
})) : [{ 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' },
{ label: 'Show Dashboard', click: () => { if (mainWindow)
mainWindow.show(); } },
@@ -143,58 +155,8 @@ const handleSwitchAccount = async (loginName) => {
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);
}
else {
account.cooldownExpiresAt = undefined;
if (backend)
await backend.pushCooldown(account.steamId, undefined);
}
}
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 ---
const syncAccounts = async (isManual = false) => {
console.log(`[Sync] Phase 1: Pulling from server...`);
const syncAccounts = async () => {
initBackend();
let accounts = store.get('accounts');
let hasChanges = false;
@@ -205,13 +167,12 @@ const syncAccounts = async (isManual = false) => {
const exists = accounts.find(a => a.steamId === s.steamId);
if (!exists) {
accounts.push({
_id: `shared_${s.steamId}`, steamId: s.steamId, personaName: s.personaName,
avatar: s.avatar, profileUrl: s.profileUrl, vacBanned: s.vacBanned,
gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure,
loginConfig: s.loginConfig, sessionUpdatedAt: s.sessionUpdatedAt,
autoCheckCooldown: !!s.steamLoginSecure, status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none',
lastBanCheck: new Date().toISOString(), sharedWith: s.sharedWith
_id: `shared_${s.steamId}`,
steamId: s.steamId, personaName: s.personaName, avatar: s.avatar, profileUrl: s.profileUrl,
vacBanned: s.vacBanned, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginConfig: s.loginConfig,
sessionUpdatedAt: s.sessionUpdatedAt, autoCheckCooldown: !!s.steamLoginSecure,
status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString()
});
hasChanges = true;
}
@@ -235,10 +196,6 @@ const syncAccounts = async (isManual = false) => {
exists.cooldownExpiresAt = s.cooldownExpiresAt;
hasChanges = true;
}
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
exists.sharedWith = s.sharedWith;
hasChanges = true;
}
}
}
}
@@ -250,44 +207,88 @@ const syncAccounts = async (isManual = false) => {
mainWindow.webContents.send('accounts-updated', accounts);
updateTrayMenu();
}
// Phase 2: Background Scrapes
const runScrapes = async () => {
console.log(`[Sync] Phase 2: Starting background checks for ${accounts.length} accounts...`);
const currentAccounts = [...store.get('accounts')];
let scrapeChanges = false;
for (const account of currentAccounts) {
try {
const now = new Date();
if (backend && !account._id.startsWith('shared_'))
if (accounts.length === 0)
return;
const updatedAccounts = [...accounts];
let scrapeChanges = false;
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 (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);
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 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) {
if (!isManual && needsCooldown)
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 30000) + 5000));
if (await scrapeAccountData(account))
if ((now.getTime() - lastScrape.getTime()) / 3600000 > 8) {
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000));
try {
const result = await (0, scraper_1.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) {
if (e.message.includes('cookie') || e.message.includes('Sign In')) {
account.authError = true;
scrapeChanges = true;
}
}
}
}
catch (error) { }
}
if (scrapeChanges) {
store.set('accounts', currentAccounts);
if (mainWindow)
mainWindow.webContents.send('accounts-updated', currentAccounts);
updateTrayMenu();
}
console.log('[Sync] Sync cycle finished.');
};
if (isManual)
await runScrapes();
else
runScrapes();
catch (error) { }
}
if (scrapeChanges) {
store.set('accounts', updatedAccounts);
if (mainWindow)
mainWindow.webContents.send('accounts-updated', updatedAccounts);
updateTrayMenu();
}
};
const scheduleNextSync = () => {
setTimeout(async () => { await syncAccounts(false); scheduleNextSync(); }, isDev ? 300000 : 1800000);
setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, isDev ? 120000 : 1800000);
};
// --- Discovery ---
const addingAccounts = new Set();
@@ -331,7 +332,7 @@ const handleLocalAccountsFound = async (localAccounts) => {
updateTrayMenu();
}
};
// --- Main Window ---
// --- Main Window Creation ---
function createWindow() {
mainWindow = new electron_1.BrowserWindow({
width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true,
@@ -350,6 +351,7 @@ function createWindow() {
else
mainWindow.loadFile(path_1.default.join(__dirname, '..', 'dist', 'index.html'));
}
// --- App Lifecycle ---
electron_1.app.whenReady().then(() => {
electron_1.protocol.handle('steam-resource', (request) => {
let rawPath = decodeURIComponent(request.url.replace('steam-resource://', ''));
@@ -368,7 +370,7 @@ electron_1.app.whenReady().then(() => {
createWindow();
createTray();
initBackend();
setTimeout(() => syncAccounts(false), 5000);
setTimeout(syncAccounts, 5000);
scheduleNextSync();
steam_client_1.steamClient.startWatching(handleLocalAccountsFound);
});
@@ -395,7 +397,7 @@ electron_1.ipcMain.handle('login-to-server', async () => {
return false;
return new Promise((resolve) => {
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 }
});
authWindow.loadURL(`${config.url}/auth/steam`);
@@ -437,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('sync-now', async () => { await syncAccounts(true); 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('sync-now', async () => { await syncAccounts(); return true; });
electron_1.ipcMain.handle('add-account', async (event, { identifier }) => {
try {
initBackend();
@@ -469,7 +457,7 @@ electron_1.ipcMain.handle('add-account', async (event, { identifier }) => {
loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure,
loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt,
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]);
updateTrayMenu();
@@ -547,12 +535,15 @@ electron_1.ipcMain.handle('admin-remove-account', async (event, steamId) => { in
electron_1.ipcMain.handle('switch-account', async (event, loginName) => await handleSwitchAccount(loginName));
electron_1.ipcMain.handle('open-external', (event, url) => electron_1.shell.openExternal(url));
electron_1.ipcMain.handle('open-steam-app-login', async () => {
console.log('[SteamClient] Preparing for fresh login...');
await killSteam();
if (process.platform === 'win32') {
// Clear auto-login registry
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()));
}
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, "");
}
const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login';
@@ -560,20 +551,34 @@ electron_1.ipcMain.handle('open-steam-app-login', async () => {
return true;
});
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 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'] });
}
// If we have an existing cookie string for this account, pre-inject it
if (expectedSteamId) {
const accounts = store.get('accounts');
const account = accounts.find(a => a.steamId === expectedSteamId);
if (account?.steamLoginSecure) {
console.log(`[Auth] Pre-injecting existing cookies for ${account.personaName}...`);
const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim());
for (const pair of cookiePairs) {
const [name, value] = pair.split('=');
if (name && value) {
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) { }
}

View File

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

View File

@@ -40,7 +40,6 @@ interface Account {
cooldownExpiresAt?: string;
authError?: boolean;
notes?: string;
sharedWith?: any[];
}
interface ServerConfig {
@@ -49,7 +48,6 @@ interface ServerConfig {
serverSteamId?: string;
enabled: boolean;
theme?: string;
isAdmin?: boolean;
}
// --- App State ---
@@ -93,40 +91,22 @@ const initBackend = () => {
// --- System Tray ---
const createTray = () => {
// Try to find the icon in various standard locations
const possiblePaths = [
path.join(__dirname, '..', 'assets-build'), // Dev
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)
];
const assetsDir = path.join(__dirname, '..', 'assets-build');
const possibleIcons = ['icon.svg', 'icon.png'];
let iconPath = '';
let assetsDir = '';
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
assetsDir = p;
for (const name of possibleIcons) {
const fullPath = path.join(assetsDir, name);
if (fs.existsSync(fullPath)) {
iconPath = fullPath;
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'}`);
if (!iconPath) {
console.warn(`[Tray] FAILED: No valid icon found in searched paths.`);
console.warn(`[Tray] FAILED: No valid icon found in ${assetsDir}`);
return;
}
@@ -134,15 +114,24 @@ const createTray = () => {
const icon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
tray = new Tray(icon);
tray.setToolTip('Ultimate Ban Tracker');
tray.on('click', () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } });
tray.on('click', () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
});
updateTrayMenu();
} catch (e) { }
console.log(`[Tray] Successfully initialized`);
} catch (e: any) {
console.error(`[Tray] Critical error during initialization: ${e.message}`);
}
};
const updateTrayMenu = () => {
if (!tray) return;
const accounts = store.get('accounts') as Account[];
const config = store.get('serverConfig');
const contextMenu = Menu.buildFromTemplate([
{ label: `Ultimate Ban Tracker v${app.getVersion()}`, enabled: false },
{ type: 'separator' },
@@ -154,11 +143,16 @@ const updateTrayMenu = () => {
click: () => handleSwitchAccount(acc.loginName)
})) : [{ 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' },
{ label: 'Show Dashboard', click: () => { if (mainWindow) mainWindow.show(); } },
{ label: 'Quit', click: () => { (app as any).isQuitting = true; app.quit(); } }
]);
tray.setContextMenu(contextMenu);
};
@@ -194,49 +188,8 @@ const handleSwitchAccount = async (loginName: string) => {
} 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);
} else {
account.cooldownExpiresAt = undefined;
if (backend) await backend.pushCooldown(account.steamId, undefined);
}
} 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 ---
const syncAccounts = async (isManual = false) => {
console.log(`[Sync] Phase 1: Pulling from server...`);
const syncAccounts = async () => {
initBackend();
let accounts = store.get('accounts') as Account[];
let hasChanges = false;
@@ -248,13 +201,12 @@ const syncAccounts = async (isManual = false) => {
const exists = accounts.find(a => a.steamId === s.steamId);
if (!exists) {
accounts.push({
_id: `shared_${s.steamId}`, steamId: s.steamId, personaName: s.personaName,
avatar: s.avatar, profileUrl: s.profileUrl, vacBanned: s.vacBanned,
gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure,
loginConfig: s.loginConfig, sessionUpdatedAt: s.sessionUpdatedAt,
autoCheckCooldown: !!s.steamLoginSecure, status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none',
lastBanCheck: new Date().toISOString(), sharedWith: s.sharedWith
_id: `shared_${s.steamId}`,
steamId: s.steamId, personaName: s.personaName, avatar: s.avatar, profileUrl: s.profileUrl,
vacBanned: s.vacBanned, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginConfig: s.loginConfig,
sessionUpdatedAt: s.sessionUpdatedAt, autoCheckCooldown: !!s.steamLoginSecure,
status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString()
});
hasChanges = true;
} else {
@@ -271,10 +223,6 @@ const syncAccounts = async (isManual = false) => {
exists.cooldownExpiresAt = s.cooldownExpiresAt;
hasChanges = true;
}
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
exists.sharedWith = s.sharedWith;
hasChanges = true;
}
}
}
} catch (e) { }
@@ -286,39 +234,76 @@ const syncAccounts = async (isManual = false) => {
updateTrayMenu();
}
// Phase 2: Background Scrapes
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 (accounts.length === 0) return;
if (needsMetadata || needsCooldown || isManual) {
if (!isManual && needsCooldown) await new Promise(r => setTimeout(r, Math.floor(Math.random() * 30000) + 5000));
if (await scrapeAccountData(account)) scrapeChanges = true;
const updatedAccounts = [...accounts];
let scrapeChanges = false;
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 (scrapeChanges) {
store.set('accounts', currentAccounts);
if (mainWindow) mainWindow.webContents.send('accounts-updated', currentAccounts);
updateTrayMenu();
}
console.log('[Sync] Sync cycle finished.');
};
if (account.loginName) {
const config = steamClient.extractAccountConfig(account.loginName);
if (config) { account.loginConfig = config; account.sessionUpdatedAt = new Date().toISOString(); }
}
if (backend) await backend.shareAccount(account);
scrapeChanges = true;
}
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 = () => {
setTimeout(async () => { await syncAccounts(false); scheduleNextSync(); }, isDev ? 300000 : 1800000);
setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, isDev ? 120000 : 1800000);
};
// --- Discovery ---
@@ -357,21 +342,28 @@ const handleLocalAccountsFound = async (localAccounts: LocalSteamAccount[]) => {
}
};
// --- Main Window ---
// --- Main Window Creation ---
function createWindow() {
mainWindow = new BrowserWindow({
width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true,
webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true }
});
mainWindow.setMenu(null);
mainWindow.on('close', (event) => {
if (!(app as any).isQuitting) { event.preventDefault(); mainWindow?.hide(); }
if (!(app as any).isQuitting) {
event.preventDefault();
mainWindow?.hide();
}
return false;
});
if (isDev) mainWindow.loadURL('http://localhost:5173');
else mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html'));
}
// --- App Lifecycle ---
app.whenReady().then(() => {
protocol.handle('steam-resource', (request) => {
let rawPath = decodeURIComponent(request.url.replace('steam-resource://', ''));
@@ -380,10 +372,11 @@ app.whenReady().then(() => {
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 }); }
});
createWindow();
createTray();
initBackend();
setTimeout(() => syncAccounts(false), 5000);
setTimeout(syncAccounts, 5000);
scheduleNextSync();
steamClient.startWatching(handleLocalAccountsFound);
});
@@ -408,17 +401,19 @@ ipcMain.handle('login-to-server', async () => {
if (!config.url) return false;
return new Promise<boolean>((resolve) => {
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 }
});
authWindow.loadURL(`${config.url}/auth/steam`);
let captured = false;
const saveServerAuth = (token: string) => {
if (captured) return; captured = true;
let serverSteamId = undefined; let isAdmin = false;
let serverSteamId = undefined;
let isAdmin = false;
try {
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) {}
const current = store.get('serverConfig');
store.set('serverConfig', { ...current, token, serverSteamId, isAdmin, enabled: true });
@@ -441,21 +436,7 @@ ipcMain.handle('login-to-server', async () => {
});
ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId }));
ipcMain.handle('sync-now', async () => { await syncAccounts(true); 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('sync-now', async () => { await syncAccounts(); return true; });
ipcMain.handle('add-account', async (event, { identifier }) => {
try {
initBackend();
@@ -472,7 +453,7 @@ ipcMain.handle('add-account', async (event, { identifier }) => {
loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure,
loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt,
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]);
updateTrayMenu();
@@ -546,35 +527,60 @@ ipcMain.handle('switch-account', async (event, loginName: string) => await handl
ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url));
ipcMain.handle('open-steam-app-login', async () => {
console.log('[SteamClient] Preparing for fresh login...');
await killSteam();
if (process.platform === 'win32') {
// Clear auto-login registry
const clearReg = 'reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "" /f';
await new Promise<void>((res) => exec(clearReg, () => res()));
} else if (process.platform === 'linux') {
// On Linux we can use the steamClient helper to set an empty user
await steamClient.setAutoLoginUser("", undefined, "");
}
const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login';
exec(command);
return true;
});
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 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) {
const accounts = store.get('accounts') as Account[];
const account = accounts.find(a => a.steamId === expectedSteamId);
if (account?.steamLoginSecure) {
console.log(`[Auth] Pre-injecting existing cookies for ${account.personaName}...`);
const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim());
for (const pair of cookiePairs) {
const [name, value] = pair.split('=');
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) => {
const loginWindow = new BrowserWindow({
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'),
getServerUserInfo: () => ipcRenderer.invoke('get-server-user-info'),
syncNow: () => ipcRenderer.invoke('sync-now'),
scrapeAccount: (steamId: string) => ipcRenderer.invoke('scrape-account', steamId),
getCommunityAccounts: () => ipcRenderer.invoke('get-community-accounts'),
getServerUsers: () => ipcRenderer.invoke('get-server-users'),

View File

@@ -1,12 +1,12 @@
{
"name": "ultimate-ban-tracker-desktop",
"version": "1.3.0",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ultimate-ban-tracker-desktop",
"version": "1.3.0",
"version": "1.2.0",
"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.2.0",
"author": "Nils Pukropp <nils@narl.io>",
"homepage": "https://narl.io",
"license": "SEE LICENSE IN LICENSE",
@@ -28,8 +28,7 @@
},
"files": [
"dist/**/*",
"dist-electron/**/*",
"assets-build/**/*"
"dist-electron/**/*"
],
"linux": {
"target": [

View File

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

View File

@@ -376,12 +376,11 @@ const AccountRow: React.FC<{
onSwitch: (login: string) => void,
onAuth: () => void
}> = ({ 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 [isShareOpen, setIsShareOpen] = useState(false);
const [targetUserId, setTargetUserId] = useState('');
const [isSharing, setIsSharing] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [serverUsers, setServerUsers] = useState<any[]>([]);
const cooldownDate = account?.cooldownExpiresAt ? new Date(account.cooldownExpiresAt) : null;
@@ -405,12 +404,6 @@ const AccountRow: React.FC<{
const [imgSrc, setImgSrc] = useState(avatarSrc);
useEffect(() => { setImgSrc(avatarSrc); }, [avatarSrc]);
const handleRefresh = async () => {
setIsRefreshing(true);
await scrapeAccount(account.steamId);
setIsRefreshing(false);
};
const handleOpenShare = async () => {
setIsShareOpen(true);
try {
@@ -529,12 +522,7 @@ const AccountRow: React.FC<{
{account.steamLoginSecure && !account.authError ? <VerifiedUserIcon fontSize="inherit" /> : (account.authError ? <LockResetIcon fontSize="inherit" /> : <BoltIcon fontSize="inherit" />)}
</IconButton>
{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>
<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>
<Typography variant="caption" sx={{ color: 'success.main', fontWeight: 'bold', fontSize: '0.6rem' }}>TRACKING</Typography>
)}
</Box>
</Tooltip>