Compare commits
22 Commits
4037d7bce3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4021e3cc42 | |||
| 1d4fb03104 | |||
| d6d87107f5 | |||
| 2ef8dd06e7 | |||
| 559c7bfdef | |||
| 2124845848 | |||
| 4ad4e1c9de | |||
| 3f7c325604 | |||
| 776e05fb52 | |||
| fc19f66ace | |||
| eca3a728fc | |||
| b64ddafab9 | |||
| 9174bcfca2 | |||
| d30005acbd | |||
| a5cc155ffc | |||
| 276d3bd4de | |||
| 60b3dd1ca1 | |||
| 34a71de2dc | |||
| 83dbfce8b2 | |||
| c208ecea95 | |||
| cf78e3c329 | |||
| 589acdebcb |
@@ -1,4 +1,33 @@
|
|||||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect width="512" height="512" rx="64" fill="#171A21"/>
|
<defs>
|
||||||
<path d="M256 64C150.13 64 64 150.13 64 256C64 361.87 150.13 448 256 448C361.87 448 448 361.87 448 256C448 150.13 375.73 64 256 64ZM256 405.33C173.6 405.33 106.67 338.4 106.67 256C106.67 221.33 118.4 189.33 138.13 164.27L347.73 373.87C322.67 393.6 290.67 405.33 256 405.33ZM373.87 347.73L164.27 138.13C189.33 118.4 221.33 106.67 256 106.67C338.4 106.67 405.33 173.6 405.33 256C405.33 290.67 393.6 322.67 373.87 347.73Z" fill="#66C0F4"/>
|
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#1B2838"/>
|
||||||
|
<stop offset="100%" stop-color="#101419"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#66C0F4"/>
|
||||||
|
<stop offset="100%" stop-color="#1A9FFF"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="15" result="blur"/>
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Outer Rounded Container -->
|
||||||
|
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
|
||||||
|
<rect x="2" y="2" width="508" height="508" rx="98" stroke="white" stroke-opacity="0.05" stroke-width="4"/>
|
||||||
|
|
||||||
|
<!-- Tracking Ring (Detailed) -->
|
||||||
|
<circle cx="256" cy="256" r="180" stroke="#66C0F4" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
|
||||||
|
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
|
||||||
|
|
||||||
|
<!-- Central Shield Symbol -->
|
||||||
|
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
|
||||||
|
|
||||||
|
<!-- "Ban" Intersect (Stylized Cross) -->
|
||||||
|
<path d="M210 220L302 312M302 220L210 312" stroke="#1B2838" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
|
||||||
|
|
||||||
|
<!-- Glass Highlight -->
|
||||||
|
<path d="M100 100C150 60 362 60 412 100" stroke="white" stroke-opacity="0.1" stroke-width="20" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 604 B After Width: | Height: | Size: 1.7 KiB |
21
frontend/assets-build/icons/latte.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#EFF1F5"/>
|
||||||
|
<stop offset="100%" stop-color="#DCE0E8"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#1E66F5"/>
|
||||||
|
<stop offset="100%" stop-color="#179299"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="15" result="blur"/>
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
|
||||||
|
<circle cx="256" cy="256" r="180" stroke="#1E66F5" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
|
||||||
|
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
|
||||||
|
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
|
||||||
|
<path d="M210 220L302 312M302 220L210 312" stroke="#EFF1F5" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
21
frontend/assets-build/icons/mocha.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#1E1E2E"/>
|
||||||
|
<stop offset="100%" stop-color="#11111B"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#B4BEFE"/>
|
||||||
|
<stop offset="100%" stop-color="#89B4FA"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="15" result="blur"/>
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
|
||||||
|
<circle cx="256" cy="256" r="180" stroke="#B4BEFE" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
|
||||||
|
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
|
||||||
|
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
|
||||||
|
<path d="M210 220L302 312M302 220L210 312" stroke="#1E1E2E" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
21
frontend/assets-build/icons/nord.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#3B4252"/>
|
||||||
|
<stop offset="100%" stop-color="#2E3440"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#88C0D0"/>
|
||||||
|
<stop offset="100%" stop-color="#81A1C1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="15" result="blur"/>
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
|
||||||
|
<circle cx="256" cy="256" r="180" stroke="#88C0D0" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
|
||||||
|
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
|
||||||
|
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
|
||||||
|
<path d="M210 220L302 312M302 220L210 312" stroke="#3B4252" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
21
frontend/assets-build/icons/steam.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#1B2838"/>
|
||||||
|
<stop offset="100%" stop-color="#101419"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#66C0F4"/>
|
||||||
|
<stop offset="100%" stop-color="#1A9FFF"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="15" result="blur"/>
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
|
||||||
|
<circle cx="256" cy="256" r="180" stroke="#66C0F4" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
|
||||||
|
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
|
||||||
|
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
|
||||||
|
<path d="M210 220L302 312M302 220L210 312" stroke="#1B2838" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
21
frontend/assets-build/icons/tokyo.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#1A1B26"/>
|
||||||
|
<stop offset="100%" stop-color="#10101A"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#7AA2F7"/>
|
||||||
|
<stop offset="100%" stop-color="#3D59A1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="15" result="blur"/>
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
|
||||||
|
<circle cx="256" cy="256" r="180" stroke="#7AA2F7" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
|
||||||
|
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
|
||||||
|
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
|
||||||
|
<path d="M210 220L302 312M302 220L210 312" stroke="#1A1B26" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -60,37 +60,55 @@ const initBackend = () => {
|
|||||||
};
|
};
|
||||||
// --- System Tray ---
|
// --- System Tray ---
|
||||||
const createTray = () => {
|
const createTray = () => {
|
||||||
const assetsDir = path_1.default.join(__dirname, '..', 'assets-build');
|
// Try to find the icon in various standard locations
|
||||||
const possibleIcons = ['icon.svg', 'icon.png'];
|
const possiblePaths = [
|
||||||
let iconPath = '';
|
path_1.default.join(__dirname, '..', 'assets-build'), // Dev
|
||||||
for (const name of possibleIcons) {
|
path_1.default.join(process.resourcesPath, 'assets-build'), // Packaged (External)
|
||||||
const fullPath = path_1.default.join(assetsDir, name);
|
path_1.default.join(electron_1.app.getAppPath(), 'dist', 'assets-build'), // Packaged (Internal dist)
|
||||||
if (fs_1.default.existsSync(fullPath)) {
|
path_1.default.join(electron_1.app.getAppPath(), 'assets-build') // Packaged (Internal root)
|
||||||
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 ${assetsDir}`);
|
console.warn(`[Tray] FAILED: No valid icon found in searched paths.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
|
const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
|
||||||
tray = new electron_1.Tray(icon);
|
tray = new electron_1.Tray(icon);
|
||||||
tray.setToolTip('Ultimate Ban Tracker');
|
tray.setToolTip('Ultimate Ban Tracker');
|
||||||
tray.on('click', () => {
|
tray.on('click', () => { if (mainWindow) {
|
||||||
if (mainWindow) {
|
mainWindow.show();
|
||||||
mainWindow.show();
|
mainWindow.focus();
|
||||||
mainWindow.focus();
|
} });
|
||||||
}
|
// Load initial themed icon
|
||||||
});
|
const config = store.get('serverConfig');
|
||||||
updateTrayMenu();
|
if (config?.theme) {
|
||||||
console.log(`[Tray] Successfully initialized`);
|
setAppIcon(config.theme);
|
||||||
}
|
}
|
||||||
catch (e) {
|
else {
|
||||||
console.error(`[Tray] Critical error during initialization: ${e.message}`);
|
updateTrayMenu(); // Fallback to refresh menu
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch (e) { }
|
||||||
};
|
};
|
||||||
const updateTrayMenu = () => {
|
const updateTrayMenu = () => {
|
||||||
if (!tray)
|
if (!tray)
|
||||||
@@ -108,11 +126,7 @@ 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(); } },
|
||||||
@@ -120,6 +134,21 @@ const updateTrayMenu = () => {
|
|||||||
]);
|
]);
|
||||||
tray.setContextMenu(contextMenu);
|
tray.setContextMenu(contextMenu);
|
||||||
};
|
};
|
||||||
|
const setAppIcon = (themeName = 'steam') => {
|
||||||
|
const assetsDir = path_1.default.join(__dirname, '..', 'assets-build', 'icons');
|
||||||
|
const iconPath = path_1.default.join(assetsDir, `${themeName}.svg`);
|
||||||
|
if (!fs_1.default.existsSync(iconPath))
|
||||||
|
return;
|
||||||
|
const icon = electron_1.nativeImage.createFromPath(iconPath);
|
||||||
|
// Update Tray
|
||||||
|
if (tray) {
|
||||||
|
tray.setImage(icon.resize({ width: 16, height: 16 }));
|
||||||
|
}
|
||||||
|
// Update Main Window
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.setIcon(icon);
|
||||||
|
}
|
||||||
|
};
|
||||||
// --- Steam Logic ---
|
// --- Steam Logic ---
|
||||||
const killSteam = async () => {
|
const killSteam = async () => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -155,8 +184,62 @@ 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 instanceof scraper_1.SteamAuthError) {
|
||||||
|
account.authError = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.error(`[Scraper] Temporary error for ${account.personaName}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 () => {
|
const syncAccounts = async (isManual = false) => {
|
||||||
|
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;
|
||||||
@@ -167,19 +250,29 @@ const syncAccounts = async () => {
|
|||||||
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}`,
|
_id: `shared_${s.steamId}`, steamId: s.steamId, personaName: s.personaName,
|
||||||
steamId: s.steamId, personaName: s.personaName, avatar: s.avatar, profileUrl: s.profileUrl,
|
avatar: s.avatar, profileUrl: s.profileUrl, vacBanned: s.vacBanned,
|
||||||
vacBanned: s.vacBanned, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
|
gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
|
||||||
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginConfig: s.loginConfig,
|
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure,
|
||||||
sessionUpdatedAt: s.sessionUpdatedAt, autoCheckCooldown: !!s.steamLoginSecure,
|
loginConfig: s.loginConfig, sessionUpdatedAt: s.sessionUpdatedAt,
|
||||||
status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString()
|
autoCheckCooldown: !!s.steamLoginSecure, status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none',
|
||||||
|
lastBanCheck: new Date().toISOString(), sharedWith: s.sharedWith
|
||||||
});
|
});
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
|
const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
|
||||||
const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
|
const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
|
||||||
if (sDate > lDate) {
|
// 1. SENSITIVE DATA SYNC (Credentials)
|
||||||
|
const sSessionDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
|
||||||
|
const lSessionDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
|
||||||
|
const isLocalAccount = !exists._id.startsWith('shared_');
|
||||||
|
const isLocalSessionHealthy = exists.steamLoginSecure && !exists.authError;
|
||||||
|
// SMART OVERWRITE LOGIC:
|
||||||
|
// - If it's a remote shared account: Newest wins.
|
||||||
|
// - If it's a LOCAL account: Only overwrite if our local session is broken/missing.
|
||||||
|
const shouldOverwriteCredentials = !isLocalAccount ? (sSessionDate > lSessionDate) : (!isLocalSessionHealthy && sSessionDate > lSessionDate);
|
||||||
|
if (shouldOverwriteCredentials) {
|
||||||
if (s.loginName)
|
if (s.loginName)
|
||||||
exists.loginName = s.loginName;
|
exists.loginName = s.loginName;
|
||||||
if (s.loginConfig)
|
if (s.loginConfig)
|
||||||
@@ -192,8 +285,28 @@ const syncAccounts = async () => {
|
|||||||
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))) {
|
// 2. Metadata Sync (Pull) - Always "Newest Wins"
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,88 +320,44 @@ const syncAccounts = async () => {
|
|||||||
mainWindow.webContents.send('accounts-updated', accounts);
|
mainWindow.webContents.send('accounts-updated', accounts);
|
||||||
updateTrayMenu();
|
updateTrayMenu();
|
||||||
}
|
}
|
||||||
if (accounts.length === 0)
|
// Phase 2: Background Scrapes
|
||||||
return;
|
const runScrapes = async () => {
|
||||||
const updatedAccounts = [...accounts];
|
console.log(`[Sync] Phase 2: Starting background checks for ${accounts.length} accounts...`);
|
||||||
let scrapeChanges = false;
|
const currentAccounts = [...store.get('accounts')];
|
||||||
for (const account of updatedAccounts) {
|
let scrapeChanges = false;
|
||||||
try {
|
for (const account of currentAccounts) {
|
||||||
const now = new Date();
|
try {
|
||||||
// OPTIMIZATION: Ensure ALL authenticated accounts are shared with the server on every sync cycle
|
const now = new Date();
|
||||||
// this guarantees that even if a push failed previously, it will be reconciled now.
|
if (backend && !account._id.startsWith('shared_'))
|
||||||
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);
|
||||||
scrapeChanges = true;
|
const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
|
||||||
}
|
|
||||||
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);
|
||||||
if ((now.getTime() - lastScrape.getTime()) / 3600000 > 8) {
|
const needsMetadata = (now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName;
|
||||||
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000));
|
const needsCooldown = account.autoCheckCooldown && account.steamLoginSecure && (now.getTime() - lastScrape.getTime()) / 3600000 > 8;
|
||||||
try {
|
if (needsMetadata || needsCooldown || isManual) {
|
||||||
const result = await (0, scraper_1.scrapeCooldown)(account.steamId, account.steamLoginSecure);
|
if (!isManual && needsCooldown)
|
||||||
account.authError = false;
|
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 30000) + 5000));
|
||||||
account.lastScrapeTime = new Date().toISOString();
|
if (await scrapeAccountData(account))
|
||||||
if (result.isActive) {
|
|
||||||
account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString();
|
|
||||||
if (backend)
|
|
||||||
await backend.pushCooldown(account.steamId, account.cooldownExpiresAt);
|
|
||||||
}
|
|
||||||
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) { }
|
||||||
}
|
}
|
||||||
catch (error) { }
|
if (scrapeChanges) {
|
||||||
}
|
store.set('accounts', currentAccounts);
|
||||||
if (scrapeChanges) {
|
if (mainWindow)
|
||||||
store.set('accounts', updatedAccounts);
|
mainWindow.webContents.send('accounts-updated', currentAccounts);
|
||||||
if (mainWindow)
|
updateTrayMenu();
|
||||||
mainWindow.webContents.send('accounts-updated', updatedAccounts);
|
}
|
||||||
updateTrayMenu();
|
console.log('[Sync] Sync cycle finished.');
|
||||||
}
|
};
|
||||||
|
if (isManual)
|
||||||
|
await runScrapes();
|
||||||
|
else
|
||||||
|
runScrapes();
|
||||||
};
|
};
|
||||||
const scheduleNextSync = () => {
|
const scheduleNextSync = () => {
|
||||||
setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, isDev ? 120000 : 1800000);
|
setTimeout(async () => { await syncAccounts(false); scheduleNextSync(); }, isDev ? 300000 : 1800000);
|
||||||
};
|
};
|
||||||
// --- Discovery ---
|
// --- Discovery ---
|
||||||
const addingAccounts = new Set();
|
const addingAccounts = new Set();
|
||||||
@@ -311,11 +380,20 @@ const handleLocalAccountsFound = async (localAccounts) => {
|
|||||||
const profile = await (0, steam_web_1.fetchProfileData)(local.steamId);
|
const profile = await (0, steam_web_1.fetchProfileData)(local.steamId);
|
||||||
const bans = await (0, steam_web_1.scrapeBanStatus)(profile.profileUrl);
|
const bans = await (0, steam_web_1.scrapeBanStatus)(profile.profileUrl);
|
||||||
const localPath = await downloadAvatar(profile.steamId, profile.avatar);
|
const localPath = await downloadAvatar(profile.steamId, profile.avatar);
|
||||||
|
// Wait and retry snagging the config (Steam takes time to write it)
|
||||||
|
let loginConfig = undefined;
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
loginConfig = steam_client_1.steamClient.extractAccountConfig(local.accountName);
|
||||||
|
if (loginConfig)
|
||||||
|
break;
|
||||||
|
}
|
||||||
currentAccounts.push({
|
currentAccounts.push({
|
||||||
_id: Date.now().toString() + Math.random().toString().slice(2, 5),
|
_id: Date.now().toString() + Math.random().toString().slice(2, 5),
|
||||||
steamId: local.steamId, personaName: profile.personaName || local.accountName,
|
steamId: local.steamId, personaName: profile.personaName || local.accountName,
|
||||||
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
|
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
|
||||||
localAvatar: localPath, profileUrl: profile.profileUrl,
|
localAvatar: localPath, profileUrl: profile.profileUrl,
|
||||||
|
loginConfig, sessionUpdatedAt: loginConfig ? new Date().toISOString() : undefined,
|
||||||
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none',
|
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none',
|
||||||
vacBanned: bans.vacBanned, gameBans: bans.gameBans, lastBanCheck: new Date().toISOString()
|
vacBanned: bans.vacBanned, gameBans: bans.gameBans, lastBanCheck: new Date().toISOString()
|
||||||
});
|
});
|
||||||
@@ -332,7 +410,7 @@ const handleLocalAccountsFound = async (localAccounts) => {
|
|||||||
updateTrayMenu();
|
updateTrayMenu();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// --- Main Window Creation ---
|
// --- Main Window ---
|
||||||
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,
|
||||||
@@ -351,7 +429,6 @@ 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://', ''));
|
||||||
@@ -370,7 +447,7 @@ electron_1.app.whenReady().then(() => {
|
|||||||
createWindow();
|
createWindow();
|
||||||
createTray();
|
createTray();
|
||||||
initBackend();
|
initBackend();
|
||||||
setTimeout(syncAccounts, 5000);
|
setTimeout(() => syncAccounts(false), 5000);
|
||||||
scheduleNextSync();
|
scheduleNextSync();
|
||||||
steam_client_1.steamClient.startWatching(handleLocalAccountsFound);
|
steam_client_1.steamClient.startWatching(handleLocalAccountsFound);
|
||||||
});
|
});
|
||||||
@@ -397,7 +474,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 Ban Tracker Server',
|
width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Server',
|
||||||
webPreferences: { nodeIntegration: false, contextIsolation: true }
|
webPreferences: { nodeIntegration: false, contextIsolation: true }
|
||||||
});
|
});
|
||||||
authWindow.loadURL(`${config.url}/auth/steam`);
|
authWindow.loadURL(`${config.url}/auth/steam`);
|
||||||
@@ -439,7 +516,21 @@ 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(); return true; });
|
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('add-account', async (event, { identifier }) => {
|
electron_1.ipcMain.handle('add-account', async (event, { identifier }) => {
|
||||||
try {
|
try {
|
||||||
initBackend();
|
initBackend();
|
||||||
@@ -457,7 +548,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()
|
lastBanCheck: new Date().toISOString(), sharedWith: existing.sharedWith
|
||||||
};
|
};
|
||||||
store.set('accounts', [...accounts, newAccount]);
|
store.set('accounts', [...accounts, newAccount]);
|
||||||
updateTrayMenu();
|
updateTrayMenu();
|
||||||
@@ -532,18 +623,59 @@ electron_1.ipcMain.handle('admin-delete-user', async (event, userId) => { initBa
|
|||||||
electron_1.ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; });
|
electron_1.ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; });
|
||||||
electron_1.ipcMain.handle('admin-remove-account', async (event, steamId) => { initBackend(); if (backend)
|
electron_1.ipcMain.handle('admin-remove-account', async (event, steamId) => { initBackend(); if (backend)
|
||||||
await backend.forceRemoveAccount(steamId); return true; });
|
await backend.forceRemoveAccount(steamId); return true; });
|
||||||
electron_1.ipcMain.handle('switch-account', async (event, loginName) => await handleSwitchAccount(loginName));
|
electron_1.ipcMain.handle('force-sync', async () => { await syncAccounts(true); return true; });
|
||||||
|
electron_1.ipcMain.handle('update-app-icon', (event, themeName) => {
|
||||||
|
setAppIcon(themeName);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
electron_1.ipcMain.handle('switch-account', async (event, loginName) => {
|
||||||
|
if (!loginName)
|
||||||
|
return false;
|
||||||
|
try {
|
||||||
|
// PROACTIVE SYNC: Try to snag the freshest token before we kill Steam
|
||||||
|
const accounts = store.get('accounts');
|
||||||
|
const account = accounts.find(a => a.loginName === loginName);
|
||||||
|
if (account && !account._id.startsWith('shared_')) {
|
||||||
|
const freshConfig = steam_client_1.steamClient.extractAccountConfig(loginName);
|
||||||
|
if (freshConfig) {
|
||||||
|
account.loginConfig = freshConfig;
|
||||||
|
account.sessionUpdatedAt = new Date().toISOString();
|
||||||
|
if (backend)
|
||||||
|
await backend.shareAccount(account);
|
||||||
|
store.set('accounts', accounts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await killSteam();
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const regBase = 'reg add "HKCU\\Software\\Valve\\Steam"';
|
||||||
|
const commands = [
|
||||||
|
`${regBase} /v AutoLoginUser /t REG_SZ /d "${loginName}" /f`,
|
||||||
|
`${regBase} /v RememberPassword /t REG_DWORD /d 1 /f`,
|
||||||
|
`${regBase} /v AlreadyLoggedIn /t REG_DWORD /d 1 /f`,
|
||||||
|
`${regBase} /v WantsOfflineMode /t REG_DWORD /d 0 /f`
|
||||||
|
];
|
||||||
|
await new Promise((res, rej) => (0, child_process_1.exec)(commands.join(' && '), (e) => e ? rej(e) : res()));
|
||||||
|
if (account && account.loginConfig)
|
||||||
|
steam_client_1.steamClient.injectAccountConfig(loginName, account.loginConfig);
|
||||||
|
}
|
||||||
|
else if (process.platform === 'linux') {
|
||||||
|
await steam_client_1.steamClient.setAutoLoginUser(loginName, account?.loginConfig, account?.steamId);
|
||||||
|
}
|
||||||
|
startSteam();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
electron_1.ipcMain.handle('open-external', (event, url) => electron_1.shell.openExternal(url));
|
electron_1.ipcMain.handle('open-external', (event, url) => electron_1.shell.openExternal(url));
|
||||||
electron_1.ipcMain.handle('open-steam-app-login', async () => {
|
electron_1.ipcMain.handle('open-steam-app-login', async () => {
|
||||||
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';
|
||||||
@@ -551,34 +683,20 @@ 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 adding a brand new account, explicitly clear previous trash
|
if (!expectedSteamId)
|
||||||
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({
|
await loginSession.cookies.set({ url: 'https://steamcommunity.com', domain: 'steamcommunity.com', name, value, path: '/', secure: true, httpOnly: name.includes('Secure') });
|
||||||
url: 'https://steamcommunity.com',
|
|
||||||
domain: 'steamcommunity.com',
|
|
||||||
name: name,
|
|
||||||
value: value,
|
|
||||||
path: '/',
|
|
||||||
secure: true,
|
|
||||||
httpOnly: name.includes('Secure')
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (e) { }
|
catch (e) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ electron_1.contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
revokeAccountAccess: (steamId, targetSteamId) => electron_1.ipcRenderer.invoke('revoke-account-access', steamId, targetSteamId),
|
revokeAccountAccess: (steamId, targetSteamId) => electron_1.ipcRenderer.invoke('revoke-account-access', steamId, targetSteamId),
|
||||||
revokeAllAccountAccess: (steamId) => electron_1.ipcRenderer.invoke('revoke-all-account-access', steamId),
|
revokeAllAccountAccess: (steamId) => electron_1.ipcRenderer.invoke('revoke-all-account-access', steamId),
|
||||||
openExternal: (url) => electron_1.ipcRenderer.invoke('open-external', url),
|
openExternal: (url) => electron_1.ipcRenderer.invoke('open-external', url),
|
||||||
|
updateAppIcon: (theme) => electron_1.ipcRenderer.invoke('update-app-icon', theme),
|
||||||
openSteamAppLogin: () => electron_1.ipcRenderer.invoke('open-steam-app-login'),
|
openSteamAppLogin: () => electron_1.ipcRenderer.invoke('open-steam-app-login'),
|
||||||
openSteamLogin: (steamId) => electron_1.ipcRenderer.invoke('open-steam-login', steamId),
|
openSteamLogin: (steamId) => electron_1.ipcRenderer.invoke('open-steam-login', steamId),
|
||||||
// Server Config & Auth
|
// Server Config & Auth
|
||||||
@@ -19,6 +20,7 @@ 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
|
||||||
|
|||||||
@@ -68,19 +68,23 @@ class BackendService {
|
|||||||
loginName: account.loginName,
|
loginName: account.loginName,
|
||||||
steamLoginSecure: account.steamLoginSecure,
|
steamLoginSecure: account.steamLoginSecure,
|
||||||
loginConfig: account.loginConfig,
|
loginConfig: account.loginConfig,
|
||||||
sessionUpdatedAt: account.sessionUpdatedAt
|
sessionUpdatedAt: account.sessionUpdatedAt,
|
||||||
|
lastMetadataCheck: account.lastBanCheck,
|
||||||
|
lastScrapeTime: account.lastScrapeTime,
|
||||||
|
cooldownExpiresAt: account.cooldownExpiresAt
|
||||||
}, { headers: this.headers });
|
}, { headers: this.headers });
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error('[Backend] Failed to share account');
|
console.error('[Backend] Failed to share account');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async pushCooldown(steamId, cooldownExpiresAt) {
|
async pushCooldown(steamId, cooldownExpiresAt, lastScrapeTime) {
|
||||||
if (!this.token)
|
if (!this.token)
|
||||||
return;
|
return;
|
||||||
try {
|
try {
|
||||||
await axios_1.default.patch(`${this.url}/api/sync/${steamId}/cooldown`, {
|
await axios_1.default.patch(`${this.url}/api/sync/${steamId}/cooldown`, {
|
||||||
cooldownExpiresAt
|
cooldownExpiresAt,
|
||||||
|
lastScrapeTime
|
||||||
}, { headers: this.headers });
|
}, { headers: this.headers });
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
|
|||||||
@@ -36,9 +36,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.scrapeCooldown = void 0;
|
exports.scrapeCooldown = exports.SteamAuthError = void 0;
|
||||||
const axios_1 = __importDefault(require("axios"));
|
const axios_1 = __importDefault(require("axios"));
|
||||||
const cheerio = __importStar(require("cheerio"));
|
const cheerio = __importStar(require("cheerio"));
|
||||||
|
// Custom error to identify session death
|
||||||
|
class SteamAuthError extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message);
|
||||||
|
this.name = "SteamAuthError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.SteamAuthError = SteamAuthError;
|
||||||
const scrapeCooldown = async (steamId, steamLoginSecure) => {
|
const scrapeCooldown = async (steamId, steamLoginSecure) => {
|
||||||
const url = `https://steamcommunity.com/profiles/${steamId}/gcpd/730?tab=matchmaking`;
|
const url = `https://steamcommunity.com/profiles/${steamId}/gcpd/730?tab=matchmaking`;
|
||||||
try {
|
try {
|
||||||
@@ -47,35 +55,38 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => {
|
|||||||
'Cookie': steamLoginSecure,
|
'Cookie': steamLoginSecure,
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||||
},
|
},
|
||||||
timeout: 10000
|
timeout: 10000,
|
||||||
|
validateStatus: (status) => status < 500 // Allow redirects to handle them manually
|
||||||
});
|
});
|
||||||
const $ = cheerio.load(response.data);
|
// If Steam redirects us to the login page, the cookie is dead
|
||||||
if (response.data.includes('Sign In') || !response.data.includes('Personal Game Data')) {
|
if (response.data.includes('Sign In') || response.request.path.includes('/login')) {
|
||||||
throw new Error('Invalid or expired steamLoginSecure cookie');
|
throw new SteamAuthError('Invalid or expired steamLoginSecure cookie');
|
||||||
|
}
|
||||||
|
const $ = cheerio.load(response.data);
|
||||||
|
if (!response.data.includes('Personal Game Data')) {
|
||||||
|
throw new SteamAuthError('Session invalid: Personal Game Data not accessible');
|
||||||
}
|
}
|
||||||
// 1. Locate the specific table containing cooldown info
|
|
||||||
let expirationDate = undefined;
|
let expirationDate = undefined;
|
||||||
$('table').each((_, table) => {
|
$('table').each((_, table) => {
|
||||||
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
|
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
|
||||||
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration'));
|
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration') || h.includes('Cooldown Expiration'));
|
||||||
if (expirationIndex !== -1) {
|
if (expirationIndex !== -1) {
|
||||||
const firstRow = $(table).find('tr').not(':has(th)').first();
|
const rows = $(table).find('tr').not(':has(th)');
|
||||||
const dateText = firstRow.find('td').eq(expirationIndex).text().trim();
|
rows.each((_, row) => {
|
||||||
if (dateText && dateText !== '') {
|
const dateText = $(row).find('td').eq(expirationIndex).text().trim();
|
||||||
const cleanDateText = dateText.replace(' GMT', ' UTC');
|
if (dateText && dateText !== '') {
|
||||||
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())) {
|
||||||
|
if (!expirationDate || parsed > expirationDate)
|
||||||
|
expirationDate = parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (expirationDate && expirationDate.getTime() > Date.now()) {
|
if (expirationDate && expirationDate.getTime() > Date.now()) {
|
||||||
console.log(`[Scraper] Found active cooldown until: ${expirationDate.toISOString()}`);
|
return { isActive: true, expiresAt: expirationDate };
|
||||||
return {
|
|
||||||
isActive: true,
|
|
||||||
expiresAt: expirationDate
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
const content = $('#personal_game_data_content').text();
|
const content = $('#personal_game_data_content').text();
|
||||||
if (content.includes('Competitive Cooldown') || content.includes('Your account is currently')) {
|
if (content.includes('Competitive Cooldown') || content.includes('Your account is currently')) {
|
||||||
@@ -84,8 +95,10 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => {
|
|||||||
return { isActive: false };
|
return { isActive: false };
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(`[Scraper] Error for ${steamId}:`, error.message);
|
if (error instanceof SteamAuthError)
|
||||||
throw error;
|
throw error;
|
||||||
|
console.error(`[Scraper] Network/Internal Error for ${steamId}:`, error.message);
|
||||||
|
throw error; // Generic errors don't trigger re-auth
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
exports.scrapeCooldown = scrapeCooldown;
|
exports.scrapeCooldown = scrapeCooldown;
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ class SteamClientService {
|
|||||||
if (platform === 'win32') {
|
if (platform === 'win32') {
|
||||||
const possiblePaths = [
|
const possiblePaths = [
|
||||||
'C:\\Program Files (x86)\\Steam',
|
'C:\\Program Files (x86)\\Steam',
|
||||||
'C:\\Program Files\\Steam'
|
'C:\\Program Files\\Steam',
|
||||||
|
path_1.default.join(process.env.APPDATA || '', 'Steam'),
|
||||||
];
|
];
|
||||||
this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null;
|
this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null;
|
||||||
}
|
}
|
||||||
@@ -29,7 +30,8 @@ class SteamClientService {
|
|||||||
const possiblePaths = [
|
const possiblePaths = [
|
||||||
path_1.default.join(home, '.steam/steam'),
|
path_1.default.join(home, '.steam/steam'),
|
||||||
path_1.default.join(home, '.local/share/Steam'),
|
path_1.default.join(home, '.local/share/Steam'),
|
||||||
path_1.default.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam')
|
path_1.default.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam'), // Flatpak
|
||||||
|
path_1.default.join(home, 'snap/steam/common/.steam/steam'), // Snap
|
||||||
];
|
];
|
||||||
this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null;
|
this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null;
|
||||||
}
|
}
|
||||||
@@ -47,12 +49,29 @@ 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');
|
||||||
}
|
}
|
||||||
|
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');
|
||||||
|
fs_1.default.renameSync(tempPath, filePath);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(`[SteamClient] Atomic write failed for ${filePath}: ${e.message}`);
|
||||||
|
if (fs_1.default.existsSync(tempPath))
|
||||||
|
fs_1.default.unlinkSync(tempPath);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
startWatching(callback) {
|
startWatching(callback) {
|
||||||
this.onAccountsChanged = callback;
|
this.onAccountsChanged = callback;
|
||||||
const loginUsersPath = this.getLoginUsersPath();
|
const loginUsersPath = this.getLoginUsersPath();
|
||||||
if (loginUsersPath && fs_1.default.existsSync(loginUsersPath)) {
|
if (loginUsersPath && fs_1.default.existsSync(loginUsersPath)) {
|
||||||
this.readLocalAccounts();
|
this.readLocalAccounts();
|
||||||
chokidar_1.default.watch(loginUsersPath, { persistent: true }).on('change', () => {
|
chokidar_1.default.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => {
|
||||||
this.readLocalAccounts();
|
this.readLocalAccounts();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -63,25 +82,26 @@ 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;
|
||||||
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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this.onAccountsChanged)
|
if (this.onAccountsChanged)
|
||||||
this.onAccountsChanged(accounts);
|
this.onAccountsChanged(accounts);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) { }
|
||||||
console.error('[SteamClient] Error parsing loginusers.vdf:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
extractAccountConfig(accountName) {
|
extractAccountConfig(accountName) {
|
||||||
const configPath = this.getConfigVdfPath();
|
const configPath = this.getConfigVdfPath();
|
||||||
@@ -91,63 +111,60 @@ class SteamClientService {
|
|||||||
const content = fs_1.default.readFileSync(configPath, 'utf-8');
|
const content = fs_1.default.readFileSync(configPath, 'utf-8');
|
||||||
const data = (0, simple_vdf_1.parse)(content);
|
const data = (0, simple_vdf_1.parse)(content);
|
||||||
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
|
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
|
||||||
if (accounts && accounts[accountName]) {
|
return (accounts && accounts[accountName]) ? accounts[accountName] : null;
|
||||||
return accounts[accountName];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error('[SteamClient] Failed to extract config.vdf data');
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
injectAccountConfig(accountName, accountData) {
|
injectAccountConfig(accountName, accountData) {
|
||||||
const configPath = this.getConfigVdfPath();
|
const configPath = this.getConfigVdfPath();
|
||||||
if (!configPath)
|
if (!configPath)
|
||||||
return;
|
return;
|
||||||
// Create directory if it doesn't exist
|
|
||||||
const configDir = path_1.default.dirname(configPath);
|
|
||||||
if (!fs_1.default.existsSync(configDir))
|
|
||||||
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
||||||
let data = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } };
|
let data = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } };
|
||||||
if (fs_1.default.existsSync(configPath)) {
|
if (fs_1.default.existsSync(configPath)) {
|
||||||
try {
|
try {
|
||||||
const content = fs_1.default.readFileSync(configPath, 'utf-8');
|
const content = fs_1.default.readFileSync(configPath, 'utf-8');
|
||||||
data = (0, simple_vdf_1.parse)(content);
|
const parsed = (0, simple_vdf_1.parse)(content);
|
||||||
|
if (parsed && typeof parsed === 'object')
|
||||||
|
data = parsed;
|
||||||
}
|
}
|
||||||
catch (e) { }
|
catch (e) { }
|
||||||
}
|
}
|
||||||
// Ensure structure exists
|
const ensurePath = (obj, keys) => {
|
||||||
if (!data.InstallConfigStore)
|
let curr = obj;
|
||||||
data.InstallConfigStore = {};
|
for (const key of keys) {
|
||||||
if (!data.InstallConfigStore.Software)
|
if (!curr[key] || typeof curr[key] !== 'object')
|
||||||
data.InstallConfigStore.Software = {};
|
curr[key] = {};
|
||||||
if (!data.InstallConfigStore.Software.Valve)
|
curr = curr[key];
|
||||||
data.InstallConfigStore.Software.Valve = {};
|
}
|
||||||
if (!data.InstallConfigStore.Software.Valve.Steam)
|
return curr;
|
||||||
data.InstallConfigStore.Software.Valve.Steam = {};
|
};
|
||||||
if (!data.InstallConfigStore.Software.Valve.Steam.Accounts)
|
const steamAccounts = ensurePath(data, ['InstallConfigStore', 'Software', 'Valve', 'Steam', 'Accounts']);
|
||||||
data.InstallConfigStore.Software.Valve.Steam.Accounts = {};
|
// FAILPROOF: Force crucial flags that Steam uses to decide session validity
|
||||||
data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData;
|
steamAccounts[accountName] = {
|
||||||
|
...accountData,
|
||||||
|
RememberPassword: "1",
|
||||||
|
AllowAutoLogin: "1",
|
||||||
|
Timestamp: Math.floor(Date.now() / 1000).toString()
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
fs_1.default.writeFileSync(configPath, (0, simple_vdf_1.stringify)(data));
|
this.safeWriteVdf(configPath, data);
|
||||||
console.log(`[SteamClient] Injected login config for ${accountName} into config.vdf`);
|
console.log(`[SteamClient] Safely injected session for ${accountName}`);
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('[SteamClient] Failed to write config.vdf');
|
|
||||||
}
|
}
|
||||||
|
catch (e) { }
|
||||||
}
|
}
|
||||||
async setAutoLoginUser(accountName, accountConfig, steamId) {
|
async setAutoLoginUser(accountName, accountConfig, steamId) {
|
||||||
const platform = os_1.default.platform();
|
const platform = os_1.default.platform();
|
||||||
const loginUsersPath = this.getLoginUsersPath();
|
const loginUsersPath = this.getLoginUsersPath();
|
||||||
if (loginUsersPath) {
|
if (loginUsersPath) {
|
||||||
const configDir = path_1.default.dirname(loginUsersPath);
|
|
||||||
if (!fs_1.default.existsSync(configDir))
|
|
||||||
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
||||||
let data = { users: {} };
|
let data = { users: {} };
|
||||||
if (fs_1.default.existsSync(loginUsersPath)) {
|
if (fs_1.default.existsSync(loginUsersPath)) {
|
||||||
try {
|
try {
|
||||||
const content = fs_1.default.readFileSync(loginUsersPath, 'utf-8');
|
const content = fs_1.default.readFileSync(loginUsersPath, 'utf-8');
|
||||||
data = (0, simple_vdf_1.parse)(content);
|
const parsed = (0, simple_vdf_1.parse)(content);
|
||||||
|
if (parsed && parsed.users)
|
||||||
|
data = parsed;
|
||||||
}
|
}
|
||||||
catch (e) { }
|
catch (e) { }
|
||||||
}
|
}
|
||||||
@@ -156,7 +173,7 @@ class SteamClientService {
|
|||||||
let found = false;
|
let found = false;
|
||||||
for (const [id, user] of Object.entries(data.users)) {
|
for (const [id, user] of Object.entries(data.users)) {
|
||||||
const u = user;
|
const u = user;
|
||||||
if (u.AccountName.toLowerCase() === accountName.toLowerCase()) {
|
if (u.AccountName?.toLowerCase() === accountName.toLowerCase()) {
|
||||||
u.mostrecent = "1";
|
u.mostrecent = "1";
|
||||||
u.RememberPassword = "1";
|
u.RememberPassword = "1";
|
||||||
u.AllowAutoLogin = "1";
|
u.AllowAutoLogin = "1";
|
||||||
@@ -169,8 +186,8 @@ class SteamClientService {
|
|||||||
u.mostrecent = "0";
|
u.mostrecent = "0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!found && steamId) {
|
if (!found && steamId && accountName) {
|
||||||
console.log(`[SteamClient] Provisioning user ${accountName} into loginusers.vdf`);
|
console.log(`[SteamClient] Provisioning new user profile for ${accountName}`);
|
||||||
data.users[steamId] = {
|
data.users[steamId] = {
|
||||||
AccountName: accountName,
|
AccountName: accountName,
|
||||||
PersonaName: accountName,
|
PersonaName: accountName,
|
||||||
@@ -184,51 +201,49 @@ class SteamClientService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
fs_1.default.writeFileSync(loginUsersPath, (0, simple_vdf_1.stringify)(data));
|
this.safeWriteVdf(loginUsersPath, data);
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('[SteamClient] Failed to write loginusers.vdf');
|
|
||||||
}
|
}
|
||||||
|
catch (e) { }
|
||||||
}
|
}
|
||||||
if (accountConfig) {
|
// Injection of the actual authentication blob
|
||||||
|
if (accountConfig && accountName) {
|
||||||
this.injectAccountConfig(accountName, accountConfig);
|
this.injectAccountConfig(accountName, accountConfig);
|
||||||
}
|
}
|
||||||
|
// --- Linux Registry / Registry.vdf Hardening ---
|
||||||
if (platform === 'linux') {
|
if (platform === 'linux') {
|
||||||
const regLocations = [
|
const regLocations = [
|
||||||
path_1.default.join(os_1.default.homedir(), '.steam', 'registry.vdf'),
|
path_1.default.join(os_1.default.homedir(), '.steam', 'registry.vdf'),
|
||||||
path_1.default.join(os_1.default.homedir(), '.steam', 'steam', 'registry.vdf')
|
path_1.default.join(os_1.default.homedir(), '.steam', 'steam', 'registry.vdf')
|
||||||
];
|
];
|
||||||
for (const regPath of regLocations) {
|
for (const regPath of regLocations) {
|
||||||
let regData = { Registry: { HKCU: { Software: { Valve: { Steam: {} } } } } };
|
if (!fs_1.default.existsSync(path_1.default.dirname(regPath)))
|
||||||
|
continue;
|
||||||
|
let regData = { Registry: { HKCU: { Software: { Valve: { Steam: { AutoLoginUser: "", RememberPassword: "1", AlreadyLoggedIn: "1" } } } } } };
|
||||||
if (fs_1.default.existsSync(regPath)) {
|
if (fs_1.default.existsSync(regPath)) {
|
||||||
try {
|
try {
|
||||||
const content = fs_1.default.readFileSync(regPath, 'utf-8');
|
const content = fs_1.default.readFileSync(regPath, 'utf-8');
|
||||||
regData = (0, simple_vdf_1.parse)(content);
|
const parsed = (0, simple_vdf_1.parse)(content);
|
||||||
|
if (parsed && typeof parsed === 'object')
|
||||||
|
regData = parsed;
|
||||||
}
|
}
|
||||||
catch (e) { }
|
catch (e) { }
|
||||||
}
|
}
|
||||||
else {
|
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 (let i = 0; i < keys.length - 1; i++) {
|
for (const key of keys) {
|
||||||
if (!curr[keys[i]])
|
if (!curr[key] || typeof curr[key] !== 'object')
|
||||||
curr[keys[i]] = {};
|
curr[key] = {};
|
||||||
curr = curr[keys[i]];
|
curr = curr[key];
|
||||||
}
|
}
|
||||||
curr[keys[keys.length - 1]] = val;
|
return curr;
|
||||||
};
|
};
|
||||||
const steamReg = ['Registry', 'HKCU', 'Software', 'Valve', 'Steam'];
|
const steamKey = ensurePath(regData, ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']);
|
||||||
setPath(regData, [...steamReg, 'AutoLoginUser'], accountName);
|
steamKey.AutoLoginUser = accountName;
|
||||||
setPath(regData, [...steamReg, 'RememberPassword'], "1");
|
steamKey.RememberPassword = "1";
|
||||||
setPath(regData, [...steamReg, 'AlreadyLoggedIn'], "1");
|
steamKey.AlreadyLoggedIn = "1";
|
||||||
setPath(regData, [...steamReg, 'WantsOfflineMode'], "0");
|
steamKey.WantsOfflineMode = "0";
|
||||||
try {
|
try {
|
||||||
fs_1.default.writeFileSync(regPath, (0, simple_vdf_1.stringify)(regData));
|
this.safeWriteVdf(regPath, regData);
|
||||||
console.log(`[SteamClient] Registry updated: ${regPath}`);
|
|
||||||
}
|
}
|
||||||
catch (e) { }
|
catch (e) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import axios from 'axios';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { pathToFileURL } from 'url';
|
import { pathToFileURL } from 'url';
|
||||||
import { fetchProfileData, scrapeBanStatus } from './services/steam-web';
|
import { fetchProfileData, scrapeBanStatus } from './services/steam-web';
|
||||||
import { scrapeCooldown } from './services/scraper';
|
import { scrapeCooldown, SteamAuthError } from './services/scraper';
|
||||||
import { steamClient, LocalSteamAccount } from './services/steam-client';
|
import { steamClient, LocalSteamAccount } from './services/steam-client';
|
||||||
import { BackendService } from './services/backend';
|
import { BackendService } from './services/backend';
|
||||||
|
|
||||||
@@ -40,6 +40,7 @@ interface Account {
|
|||||||
cooldownExpiresAt?: string;
|
cooldownExpiresAt?: string;
|
||||||
authError?: boolean;
|
authError?: boolean;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
sharedWith?: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServerConfig {
|
interface ServerConfig {
|
||||||
@@ -48,6 +49,7 @@ interface ServerConfig {
|
|||||||
serverSteamId?: string;
|
serverSteamId?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
|
isAdmin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- App State ---
|
// --- App State ---
|
||||||
@@ -91,22 +93,40 @@ const initBackend = () => {
|
|||||||
|
|
||||||
// --- System Tray ---
|
// --- System Tray ---
|
||||||
const createTray = () => {
|
const createTray = () => {
|
||||||
const assetsDir = path.join(__dirname, '..', 'assets-build');
|
// Try to find the icon in various standard locations
|
||||||
const possibleIcons = ['icon.svg', 'icon.png'];
|
const possiblePaths = [
|
||||||
let iconPath = '';
|
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)
|
||||||
|
];
|
||||||
|
|
||||||
for (const name of possibleIcons) {
|
let assetsDir = '';
|
||||||
const fullPath = path.join(assetsDir, name);
|
for (const p of possiblePaths) {
|
||||||
if (fs.existsSync(fullPath)) {
|
if (fs.existsSync(p)) {
|
||||||
iconPath = fullPath;
|
assetsDir = p;
|
||||||
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 ${assetsDir}`);
|
console.warn(`[Tray] FAILED: No valid icon found in searched paths.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,24 +134,22 @@ 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', () => {
|
tray.on('click', () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } });
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.show();
|
// Load initial themed icon
|
||||||
mainWindow.focus();
|
const config = store.get('serverConfig');
|
||||||
}
|
if (config?.theme) {
|
||||||
});
|
setAppIcon(config.theme);
|
||||||
updateTrayMenu();
|
} else {
|
||||||
console.log(`[Tray] Successfully initialized`);
|
updateTrayMenu(); // Fallback to refresh menu
|
||||||
} catch (e: any) {
|
}
|
||||||
console.error(`[Tray] Critical error during initialization: ${e.message}`);
|
} catch (e) { }
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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' },
|
||||||
@@ -143,19 +161,33 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setAppIcon = (themeName: string = 'steam') => {
|
||||||
|
const assetsDir = path.join(__dirname, '..', 'assets-build', 'icons');
|
||||||
|
const iconPath = path.join(assetsDir, `${themeName}.svg`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(iconPath)) return;
|
||||||
|
|
||||||
|
const icon = nativeImage.createFromPath(iconPath);
|
||||||
|
|
||||||
|
// Update Tray
|
||||||
|
if (tray) {
|
||||||
|
tray.setImage(icon.resize({ width: 16, height: 16 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Main Window
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.setIcon(icon);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- Steam Logic ---
|
// --- Steam Logic ---
|
||||||
const killSteam = async () => {
|
const killSteam = async () => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
@@ -188,8 +220,53 @@ 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 instanceof SteamAuthError) {
|
||||||
|
account.authError = true;
|
||||||
|
} else {
|
||||||
|
console.error(`[Scraper] Temporary error for ${account.personaName}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 () => {
|
const syncAccounts = async (isManual = false) => {
|
||||||
|
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;
|
||||||
@@ -201,26 +278,66 @@ const syncAccounts = async () => {
|
|||||||
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}`,
|
_id: `shared_${s.steamId}`, steamId: s.steamId, personaName: s.personaName,
|
||||||
steamId: s.steamId, personaName: s.personaName, avatar: s.avatar, profileUrl: s.profileUrl,
|
avatar: s.avatar, profileUrl: s.profileUrl, vacBanned: s.vacBanned,
|
||||||
vacBanned: s.vacBanned, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
|
gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
|
||||||
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginConfig: s.loginConfig,
|
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure,
|
||||||
sessionUpdatedAt: s.sessionUpdatedAt, autoCheckCooldown: !!s.steamLoginSecure,
|
loginConfig: s.loginConfig, sessionUpdatedAt: s.sessionUpdatedAt,
|
||||||
status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString()
|
autoCheckCooldown: !!s.steamLoginSecure, status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none',
|
||||||
|
lastBanCheck: new Date().toISOString(), sharedWith: s.sharedWith
|
||||||
});
|
});
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
} else {
|
} else {
|
||||||
const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
|
const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
|
||||||
const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
|
const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
|
||||||
if (sDate > lDate) {
|
// 1. SENSITIVE DATA SYNC (Credentials)
|
||||||
|
const sSessionDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
|
||||||
|
const lSessionDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
|
||||||
|
|
||||||
|
const isLocalAccount = !exists._id.startsWith('shared_');
|
||||||
|
const isLocalSessionHealthy = exists.steamLoginSecure && !exists.authError;
|
||||||
|
|
||||||
|
// SMART OVERWRITE LOGIC:
|
||||||
|
// - If it's a remote shared account: Newest wins.
|
||||||
|
// - If it's a LOCAL account: Only overwrite if our local session is broken/missing.
|
||||||
|
const shouldOverwriteCredentials = !isLocalAccount ? (sSessionDate > lSessionDate) : (!isLocalSessionHealthy && sSessionDate > lSessionDate);
|
||||||
|
|
||||||
|
if (shouldOverwriteCredentials) {
|
||||||
if (s.loginName) exists.loginName = s.loginName;
|
if (s.loginName) exists.loginName = s.loginName;
|
||||||
if (s.loginConfig) exists.loginConfig = s.loginConfig;
|
if (s.loginConfig) exists.loginConfig = s.loginConfig;
|
||||||
if (s.steamLoginSecure) { exists.steamLoginSecure = s.steamLoginSecure; exists.autoCheckCooldown = true; exists.authError = false; }
|
if (s.steamLoginSecure) {
|
||||||
|
exists.steamLoginSecure = s.steamLoginSecure;
|
||||||
|
exists.autoCheckCooldown = true;
|
||||||
|
exists.authError = 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))) {
|
|
||||||
|
// 2. Metadata Sync (Pull) - Always "Newest Wins"
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,76 +351,39 @@ const syncAccounts = async () => {
|
|||||||
updateTrayMenu();
|
updateTrayMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accounts.length === 0) return;
|
// Phase 2: Background Scrapes
|
||||||
|
const runScrapes = async () => {
|
||||||
const updatedAccounts = [...accounts];
|
console.log(`[Sync] Phase 2: Starting background checks for ${accounts.length} accounts...`);
|
||||||
let scrapeChanges = false;
|
const currentAccounts = [...store.get('accounts') as Account[]];
|
||||||
|
let scrapeChanges = false;
|
||||||
for (const account of updatedAccounts) {
|
for (const account of currentAccounts) {
|
||||||
try {
|
try {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
if (backend && !account._id.startsWith('shared_')) await backend.shareAccount(account);
|
||||||
// OPTIMIZATION: Ensure ALL authenticated accounts are shared with the server on every sync cycle
|
const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
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 (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);
|
||||||
if ((now.getTime() - lastScrape.getTime()) / 3600000 > 8) {
|
const needsMetadata = (now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName;
|
||||||
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000));
|
const needsCooldown = account.autoCheckCooldown && account.steamLoginSecure && (now.getTime() - lastScrape.getTime()) / 3600000 > 8;
|
||||||
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) {
|
if (needsMetadata || needsCooldown || isManual) {
|
||||||
store.set('accounts', updatedAccounts);
|
if (!isManual && needsCooldown) await new Promise(r => setTimeout(r, Math.floor(Math.random() * 30000) + 5000));
|
||||||
if (mainWindow) mainWindow.webContents.send('accounts-updated', updatedAccounts);
|
if (await scrapeAccountData(account)) scrapeChanges = true;
|
||||||
updateTrayMenu();
|
}
|
||||||
}
|
} 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();
|
||||||
};
|
};
|
||||||
|
|
||||||
const scheduleNextSync = () => {
|
const scheduleNextSync = () => {
|
||||||
setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, isDev ? 120000 : 1800000);
|
setTimeout(async () => { await syncAccounts(false); scheduleNextSync(); }, isDev ? 300000 : 1800000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Discovery ---
|
// --- Discovery ---
|
||||||
@@ -322,11 +402,21 @@ const handleLocalAccountsFound = async (localAccounts: LocalSteamAccount[]) => {
|
|||||||
const profile = await fetchProfileData(local.steamId);
|
const profile = await fetchProfileData(local.steamId);
|
||||||
const bans = await scrapeBanStatus(profile.profileUrl);
|
const bans = await scrapeBanStatus(profile.profileUrl);
|
||||||
const localPath = await downloadAvatar(profile.steamId, profile.avatar);
|
const localPath = await downloadAvatar(profile.steamId, profile.avatar);
|
||||||
|
|
||||||
|
// Wait and retry snagging the config (Steam takes time to write it)
|
||||||
|
let loginConfig = undefined;
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
loginConfig = steamClient.extractAccountConfig(local.accountName);
|
||||||
|
if (loginConfig) break;
|
||||||
|
}
|
||||||
|
|
||||||
currentAccounts.push({
|
currentAccounts.push({
|
||||||
_id: Date.now().toString() + Math.random().toString().slice(2, 5),
|
_id: Date.now().toString() + Math.random().toString().slice(2, 5),
|
||||||
steamId: local.steamId, personaName: profile.personaName || local.accountName,
|
steamId: local.steamId, personaName: profile.personaName || local.accountName,
|
||||||
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
|
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
|
||||||
localAvatar: localPath, profileUrl: profile.profileUrl,
|
localAvatar: localPath, profileUrl: profile.profileUrl,
|
||||||
|
loginConfig, sessionUpdatedAt: loginConfig ? new Date().toISOString() : undefined,
|
||||||
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none',
|
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none',
|
||||||
vacBanned: bans.vacBanned, gameBans: bans.gameBans, lastBanCheck: new Date().toISOString()
|
vacBanned: bans.vacBanned, gameBans: bans.gameBans, lastBanCheck: new Date().toISOString()
|
||||||
});
|
});
|
||||||
@@ -342,28 +432,21 @@ const handleLocalAccountsFound = async (localAccounts: LocalSteamAccount[]) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Main Window Creation ---
|
// --- Main Window ---
|
||||||
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) {
|
if (!(app as any).isQuitting) { event.preventDefault(); mainWindow?.hide(); }
|
||||||
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://', ''));
|
||||||
@@ -372,11 +455,10 @@ 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, 5000);
|
setTimeout(() => syncAccounts(false), 5000);
|
||||||
scheduleNextSync();
|
scheduleNextSync();
|
||||||
steamClient.startWatching(handleLocalAccountsFound);
|
steamClient.startWatching(handleLocalAccountsFound);
|
||||||
});
|
});
|
||||||
@@ -401,19 +483,17 @@ 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 Ban Tracker Server',
|
width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to 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 serverSteamId = undefined; let isAdmin = false;
|
||||||
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;
|
serverSteamId = payload.steamId; isAdmin = !!payload.isAdmin;
|
||||||
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 });
|
||||||
@@ -436,7 +516,21 @@ 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(); return true; });
|
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('add-account', async (event, { identifier }) => {
|
ipcMain.handle('add-account', async (event, { identifier }) => {
|
||||||
try {
|
try {
|
||||||
initBackend();
|
initBackend();
|
||||||
@@ -453,7 +547,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()
|
lastBanCheck: new Date().toISOString(), sharedWith: existing.sharedWith
|
||||||
};
|
};
|
||||||
store.set('accounts', [...accounts, newAccount]);
|
store.set('accounts', [...accounts, newAccount]);
|
||||||
updateTrayMenu();
|
updateTrayMenu();
|
||||||
@@ -523,64 +617,81 @@ ipcMain.handle('admin-delete-user', async (event, userId: string) => { initBacke
|
|||||||
ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; });
|
ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; });
|
||||||
ipcMain.handle('admin-remove-account', async (event, steamId: string) => { initBackend(); if (backend) await backend.forceRemoveAccount(steamId); return true; });
|
ipcMain.handle('admin-remove-account', async (event, steamId: string) => { initBackend(); if (backend) await backend.forceRemoveAccount(steamId); return true; });
|
||||||
|
|
||||||
ipcMain.handle('switch-account', async (event, loginName: string) => await handleSwitchAccount(loginName));
|
ipcMain.handle('force-sync', async () => { await syncAccounts(true); return true; });
|
||||||
|
|
||||||
|
ipcMain.handle('update-app-icon', (event, themeName: string) => {
|
||||||
|
setAppIcon(themeName);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('switch-account', async (event, loginName: string) => {
|
||||||
|
if (!loginName) return false;
|
||||||
|
try {
|
||||||
|
// PROACTIVE SYNC: Try to snag the freshest token before we kill Steam
|
||||||
|
const accounts = store.get('accounts') as Account[];
|
||||||
|
const account = accounts.find(a => a.loginName === loginName);
|
||||||
|
if (account && !account._id.startsWith('shared_')) {
|
||||||
|
const freshConfig = steamClient.extractAccountConfig(loginName);
|
||||||
|
if (freshConfig) {
|
||||||
|
account.loginConfig = freshConfig;
|
||||||
|
account.sessionUpdatedAt = new Date().toISOString();
|
||||||
|
if (backend) await backend.shareAccount(account);
|
||||||
|
store.set('accounts', accounts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await killSteam();
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const regBase = 'reg add "HKCU\\Software\\Valve\\Steam"';
|
||||||
|
const commands = [
|
||||||
|
`${regBase} /v AutoLoginUser /t REG_SZ /d "${loginName}" /f`,
|
||||||
|
`${regBase} /v RememberPassword /t REG_DWORD /d 1 /f`,
|
||||||
|
`${regBase} /v AlreadyLoggedIn /t REG_DWORD /d 1 /f`,
|
||||||
|
`${regBase} /v WantsOfflineMode /t REG_DWORD /d 0 /f`
|
||||||
|
];
|
||||||
|
await new Promise<void>((res, rej) => exec(commands.join(' && '), (e) => e ? rej(e) : res()));
|
||||||
|
if (account && account.loginConfig) steamClient.injectAccountConfig(loginName, account.loginConfig);
|
||||||
|
} else if (process.platform === 'linux') {
|
||||||
|
await steamClient.setAutoLoginUser(loginName, account?.loginConfig, account?.steamId);
|
||||||
|
}
|
||||||
|
startSteam();
|
||||||
|
return true;
|
||||||
|
} catch (e) { return false; }
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url));
|
ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url));
|
||||||
|
|
||||||
ipcMain.handle('open-steam-app-login', async () => {
|
ipcMain.handle('open-steam-app-login', async () => {
|
||||||
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 {
|
try { await loginSession.cookies.set({ url: 'https://steamcommunity.com', domain: 'steamcommunity.com', name, value, path: '/', secure: true, httpOnly: name.includes('Secure') }); } catch (e) {}
|
||||||
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',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
revokeAccountAccess: (steamId: string, targetSteamId: string) => ipcRenderer.invoke('revoke-account-access', steamId, targetSteamId),
|
revokeAccountAccess: (steamId: string, targetSteamId: string) => ipcRenderer.invoke('revoke-account-access', steamId, targetSteamId),
|
||||||
revokeAllAccountAccess: (steamId: string) => ipcRenderer.invoke('revoke-all-account-access', steamId),
|
revokeAllAccountAccess: (steamId: string) => ipcRenderer.invoke('revoke-all-account-access', steamId),
|
||||||
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
|
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
|
||||||
|
updateAppIcon: (theme: string) => ipcRenderer.invoke('update-app-icon', theme),
|
||||||
openSteamAppLogin: () => ipcRenderer.invoke('open-steam-app-login'),
|
openSteamAppLogin: () => ipcRenderer.invoke('open-steam-app-login'),
|
||||||
openSteamLogin: (steamId: string) => ipcRenderer.invoke('open-steam-login', steamId),
|
openSteamLogin: (steamId: string) => ipcRenderer.invoke('open-steam-login', steamId),
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ 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'),
|
||||||
|
|
||||||
|
|||||||
@@ -62,18 +62,22 @@ export class BackendService {
|
|||||||
loginName: account.loginName,
|
loginName: account.loginName,
|
||||||
steamLoginSecure: account.steamLoginSecure,
|
steamLoginSecure: account.steamLoginSecure,
|
||||||
loginConfig: account.loginConfig,
|
loginConfig: account.loginConfig,
|
||||||
sessionUpdatedAt: account.sessionUpdatedAt
|
sessionUpdatedAt: account.sessionUpdatedAt,
|
||||||
|
lastMetadataCheck: account.lastBanCheck,
|
||||||
|
lastScrapeTime: account.lastScrapeTime,
|
||||||
|
cooldownExpiresAt: account.cooldownExpiresAt
|
||||||
}, { headers: this.headers });
|
}, { headers: this.headers });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Backend] Failed to share account');
|
console.error('[Backend] Failed to share account');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async pushCooldown(steamId: string, cooldownExpiresAt?: string) {
|
public async pushCooldown(steamId: string, cooldownExpiresAt?: string, lastScrapeTime?: string) {
|
||||||
if (!this.token) return;
|
if (!this.token) return;
|
||||||
try {
|
try {
|
||||||
await axios.patch(`${this.url}/api/sync/${steamId}/cooldown`, {
|
await axios.patch(`${this.url}/api/sync/${steamId}/cooldown`, {
|
||||||
cooldownExpiresAt
|
cooldownExpiresAt,
|
||||||
|
lastScrapeTime
|
||||||
}, { headers: this.headers });
|
}, { headers: this.headers });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[Backend] Failed to push cooldown for ${steamId}`);
|
console.error(`[Backend] Failed to push cooldown for ${steamId}`);
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ export interface CooldownData {
|
|||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom error to identify session death
|
||||||
|
export class SteamAuthError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "SteamAuthError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const scrapeCooldown = async (steamId: string, steamLoginSecure: string): Promise<CooldownData> => {
|
export const scrapeCooldown = async (steamId: string, steamLoginSecure: string): Promise<CooldownData> => {
|
||||||
const url = `https://steamcommunity.com/profiles/${steamId}/gcpd/730?tab=matchmaking`;
|
const url = `https://steamcommunity.com/profiles/${steamId}/gcpd/730?tab=matchmaking`;
|
||||||
|
|
||||||
@@ -15,43 +23,44 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
|
|||||||
'Cookie': steamLoginSecure,
|
'Cookie': steamLoginSecure,
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||||
},
|
},
|
||||||
timeout: 10000
|
timeout: 10000,
|
||||||
|
validateStatus: (status) => status < 500 // Allow redirects to handle them manually
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If Steam redirects us to the login page, the cookie is dead
|
||||||
|
if (response.data.includes('Sign In') || response.request.path.includes('/login')) {
|
||||||
|
throw new SteamAuthError('Invalid or expired steamLoginSecure cookie');
|
||||||
|
}
|
||||||
|
|
||||||
const $ = cheerio.load(response.data);
|
const $ = cheerio.load(response.data);
|
||||||
|
|
||||||
if (response.data.includes('Sign In') || !response.data.includes('Personal Game Data')) {
|
if (!response.data.includes('Personal Game Data')) {
|
||||||
throw new Error('Invalid or expired steamLoginSecure cookie');
|
throw new SteamAuthError('Session invalid: Personal Game Data not accessible');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Locate the specific table containing cooldown info
|
|
||||||
let expirationDate: Date | undefined = undefined;
|
let expirationDate: Date | undefined = undefined;
|
||||||
|
|
||||||
$('table').each((_, table) => {
|
$('table').each((_, table) => {
|
||||||
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
|
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
|
||||||
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration'));
|
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration') || h.includes('Cooldown Expiration'));
|
||||||
|
|
||||||
if (expirationIndex !== -1) {
|
if (expirationIndex !== -1) {
|
||||||
const firstRow = $(table).find('tr').not(':has(th)').first();
|
const rows = $(table).find('tr').not(':has(th)');
|
||||||
const dateText = firstRow.find('td').eq(expirationIndex).text().trim();
|
rows.each((_, row) => {
|
||||||
|
const dateText = $(row).find('td').eq(expirationIndex).text().trim();
|
||||||
if (dateText && dateText !== '') {
|
if (dateText && dateText !== '') {
|
||||||
const cleanDateText = dateText.replace(' GMT', ' UTC');
|
const cleanDateText = dateText.replace(' GMT', ' UTC');
|
||||||
const parsed = new Date(cleanDateText);
|
const parsed = new Date(cleanDateText);
|
||||||
|
if (!isNaN(parsed.getTime())) {
|
||||||
if (!isNaN(parsed.getTime())) {
|
if (!expirationDate || parsed > (expirationDate as Date)) expirationDate = parsed;
|
||||||
expirationDate = parsed;
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (expirationDate && (expirationDate as Date).getTime() > Date.now()) {
|
if (expirationDate && (expirationDate as Date).getTime() > Date.now()) {
|
||||||
console.log(`[Scraper] Found active cooldown until: ${(expirationDate as Date).toISOString()}`);
|
return { isActive: true, expiresAt: expirationDate };
|
||||||
return {
|
|
||||||
isActive: true,
|
|
||||||
expiresAt: expirationDate
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = $('#personal_game_data_content').text();
|
const content = $('#personal_game_data_content').text();
|
||||||
@@ -61,7 +70,8 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
|
|||||||
|
|
||||||
return { isActive: false };
|
return { isActive: false };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`[Scraper] Error for ${steamId}:`, error.message);
|
if (error instanceof SteamAuthError) throw error;
|
||||||
throw error;
|
console.error(`[Scraper] Network/Internal Error for ${steamId}:`, error.message);
|
||||||
|
throw error; // Generic errors don't trigger re-auth
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,14 +26,16 @@ class SteamClientService {
|
|||||||
if (platform === 'win32') {
|
if (platform === 'win32') {
|
||||||
const possiblePaths = [
|
const possiblePaths = [
|
||||||
'C:\\Program Files (x86)\\Steam',
|
'C:\\Program Files (x86)\\Steam',
|
||||||
'C:\\Program Files\\Steam'
|
'C:\\Program Files\\Steam',
|
||||||
|
path.join(process.env.APPDATA || '', 'Steam'),
|
||||||
];
|
];
|
||||||
this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null;
|
this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null;
|
||||||
} else if (platform === 'linux') {
|
} else if (platform === 'linux') {
|
||||||
const possiblePaths = [
|
const possiblePaths = [
|
||||||
path.join(home, '.steam/steam'),
|
path.join(home, '.steam/steam'),
|
||||||
path.join(home, '.local/share/Steam'),
|
path.join(home, '.local/share/Steam'),
|
||||||
path.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam')
|
path.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam'), // Flatpak
|
||||||
|
path.join(home, 'snap/steam/common/.steam/steam'), // Snap
|
||||||
];
|
];
|
||||||
this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null;
|
this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null;
|
||||||
}
|
}
|
||||||
@@ -53,13 +55,27 @@ class SteamClientService {
|
|||||||
return path.join(this.steamPath, 'config', 'config.vdf');
|
return path.join(this.steamPath, 'config', 'config.vdf');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private safeWriteVdf(filePath: string, data: any) {
|
||||||
|
const tempPath = `${filePath}.tmp_${Date.now()}`;
|
||||||
|
const dir = path.dirname(filePath);
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
const vdfContent = stringify(data);
|
||||||
|
fs.writeFileSync(tempPath, vdfContent, 'utf-8');
|
||||||
|
fs.renameSync(tempPath, filePath);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`[SteamClient] Atomic write failed for ${filePath}: ${e.message}`);
|
||||||
|
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public startWatching(callback: (accounts: LocalSteamAccount[]) => void) {
|
public startWatching(callback: (accounts: LocalSteamAccount[]) => void) {
|
||||||
this.onAccountsChanged = callback;
|
this.onAccountsChanged = callback;
|
||||||
const loginUsersPath = this.getLoginUsersPath();
|
const loginUsersPath = this.getLoginUsersPath();
|
||||||
|
|
||||||
if (loginUsersPath && fs.existsSync(loginUsersPath)) {
|
if (loginUsersPath && fs.existsSync(loginUsersPath)) {
|
||||||
this.readLocalAccounts();
|
this.readLocalAccounts();
|
||||||
chokidar.watch(loginUsersPath, { persistent: true }).on('change', () => {
|
chokidar.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => {
|
||||||
this.readLocalAccounts();
|
this.readLocalAccounts();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -68,79 +84,72 @@ class SteamClientService {
|
|||||||
private readLocalAccounts() {
|
private readLocalAccounts() {
|
||||||
const filePath = this.getLoginUsersPath();
|
const filePath = this.getLoginUsersPath();
|
||||||
if (!filePath || !fs.existsSync(filePath)) return;
|
if (!filePath || !fs.existsSync(filePath)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
if (!content.trim()) return;
|
||||||
const data = parse(content) as any;
|
const data = parse(content) as any;
|
||||||
if (!data || !data.users) return;
|
if (!data || !data.users) return;
|
||||||
|
|
||||||
const accounts: LocalSteamAccount[] = [];
|
const accounts: LocalSteamAccount[] = [];
|
||||||
for (const [steamId64, userData] of Object.entries(data.users)) {
|
for (const [steamId64, userData] of Object.entries(data.users)) {
|
||||||
const user = userData as any;
|
const user = userData as any;
|
||||||
|
if (!user || !user.AccountName) continue;
|
||||||
accounts.push({
|
accounts.push({
|
||||||
steamId: steamId64,
|
steamId: steamId64, accountName: user.AccountName,
|
||||||
accountName: user.AccountName,
|
personaName: user.PersonaName || user.AccountName,
|
||||||
personaName: user.PersonaName,
|
|
||||||
timestamp: parseInt(user.Timestamp) || 0
|
timestamp: parseInt(user.Timestamp) || 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.onAccountsChanged) this.onAccountsChanged(accounts);
|
if (this.onAccountsChanged) this.onAccountsChanged(accounts);
|
||||||
} catch (error) {
|
} catch (error) { }
|
||||||
console.error('[SteamClient] Error parsing loginusers.vdf:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extractAccountConfig(accountName: string): any | null {
|
public extractAccountConfig(accountName: string): any | null {
|
||||||
const configPath = this.getConfigVdfPath();
|
const configPath = this.getConfigVdfPath();
|
||||||
if (!configPath || !fs.existsSync(configPath)) return null;
|
if (!configPath || !fs.existsSync(configPath)) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(configPath, 'utf-8');
|
const content = fs.readFileSync(configPath, 'utf-8');
|
||||||
const data = parse(content) as any;
|
const data = parse(content) as any;
|
||||||
|
|
||||||
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
|
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
|
||||||
if (accounts && accounts[accountName]) {
|
return (accounts && accounts[accountName]) ? accounts[accountName] : null;
|
||||||
return accounts[accountName];
|
} catch (e) { return null; }
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[SteamClient] Failed to extract config.vdf data');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public injectAccountConfig(accountName: string, accountData: any) {
|
public injectAccountConfig(accountName: string, accountData: any) {
|
||||||
const configPath = this.getConfigVdfPath();
|
const configPath = this.getConfigVdfPath();
|
||||||
if (!configPath) return;
|
if (!configPath) return;
|
||||||
|
|
||||||
// Create directory if it doesn't exist
|
|
||||||
const configDir = path.dirname(configPath);
|
|
||||||
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
||||||
|
|
||||||
let data: any = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } };
|
let data: any = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } };
|
||||||
|
|
||||||
if (fs.existsSync(configPath)) {
|
if (fs.existsSync(configPath)) {
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(configPath, 'utf-8');
|
const content = fs.readFileSync(configPath, 'utf-8');
|
||||||
data = parse(content) as any;
|
const parsed = parse(content) as any;
|
||||||
|
if (parsed && typeof parsed === 'object') data = parsed;
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure structure exists
|
const ensurePath = (obj: any, keys: string[]) => {
|
||||||
if (!data.InstallConfigStore) data.InstallConfigStore = {};
|
let curr = obj;
|
||||||
if (!data.InstallConfigStore.Software) data.InstallConfigStore.Software = {};
|
for (const key of keys) {
|
||||||
if (!data.InstallConfigStore.Software.Valve) data.InstallConfigStore.Software.Valve = {};
|
if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {};
|
||||||
if (!data.InstallConfigStore.Software.Valve.Steam) data.InstallConfigStore.Software.Valve.Steam = {};
|
curr = curr[key];
|
||||||
if (!data.InstallConfigStore.Software.Valve.Steam.Accounts) data.InstallConfigStore.Software.Valve.Steam.Accounts = {};
|
}
|
||||||
|
return curr;
|
||||||
|
};
|
||||||
|
|
||||||
data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData;
|
const steamAccounts = ensurePath(data, ['InstallConfigStore', 'Software', 'Valve', 'Steam', 'Accounts']);
|
||||||
|
|
||||||
|
// FAILPROOF: Force crucial flags that Steam uses to decide session validity
|
||||||
|
steamAccounts[accountName] = {
|
||||||
|
...accountData,
|
||||||
|
RememberPassword: "1",
|
||||||
|
AllowAutoLogin: "1",
|
||||||
|
Timestamp: Math.floor(Date.now() / 1000).toString()
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(configPath, stringify(data));
|
this.safeWriteVdf(configPath, data);
|
||||||
console.log(`[SteamClient] Injected login config for ${accountName} into config.vdf`);
|
console.log(`[SteamClient] Safely injected session for ${accountName}`);
|
||||||
} catch (e) {
|
} catch (e) { }
|
||||||
console.error('[SteamClient] Failed to write config.vdf');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setAutoLoginUser(accountName: string, accountConfig?: any, steamId?: string): Promise<boolean> {
|
public async setAutoLoginUser(accountName: string, accountConfig?: any, steamId?: string): Promise<boolean> {
|
||||||
@@ -148,14 +157,12 @@ class SteamClientService {
|
|||||||
const loginUsersPath = this.getLoginUsersPath();
|
const loginUsersPath = this.getLoginUsersPath();
|
||||||
|
|
||||||
if (loginUsersPath) {
|
if (loginUsersPath) {
|
||||||
const configDir = path.dirname(loginUsersPath);
|
|
||||||
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
||||||
|
|
||||||
let data: any = { users: {} };
|
let data: any = { users: {} };
|
||||||
if (fs.existsSync(loginUsersPath)) {
|
if (fs.existsSync(loginUsersPath)) {
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(loginUsersPath, 'utf-8');
|
const content = fs.readFileSync(loginUsersPath, 'utf-8');
|
||||||
data = parse(content) as any;
|
const parsed = parse(content) as any;
|
||||||
|
if (parsed && parsed.users) data = parsed;
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +171,7 @@ class SteamClientService {
|
|||||||
let found = false;
|
let found = false;
|
||||||
for (const [id, user] of Object.entries(data.users)) {
|
for (const [id, user] of Object.entries(data.users)) {
|
||||||
const u = user as any;
|
const u = user as any;
|
||||||
if (u.AccountName.toLowerCase() === accountName.toLowerCase()) {
|
if (u.AccountName?.toLowerCase() === accountName.toLowerCase()) {
|
||||||
u.mostrecent = "1";
|
u.mostrecent = "1";
|
||||||
u.RememberPassword = "1";
|
u.RememberPassword = "1";
|
||||||
u.AllowAutoLogin = "1";
|
u.AllowAutoLogin = "1";
|
||||||
@@ -177,8 +184,8 @@ class SteamClientService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!found && steamId) {
|
if (!found && steamId && accountName) {
|
||||||
console.log(`[SteamClient] Provisioning user ${accountName} into loginusers.vdf`);
|
console.log(`[SteamClient] Provisioning new user profile for ${accountName}`);
|
||||||
data.users[steamId] = {
|
data.users[steamId] = {
|
||||||
AccountName: accountName,
|
AccountName: accountName,
|
||||||
PersonaName: accountName,
|
PersonaName: accountName,
|
||||||
@@ -193,16 +200,16 @@ class SteamClientService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(loginUsersPath, stringify(data));
|
this.safeWriteVdf(loginUsersPath, data);
|
||||||
} catch (e) {
|
} catch (e) { }
|
||||||
console.error('[SteamClient] Failed to write loginusers.vdf');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accountConfig) {
|
// Injection of the actual authentication blob
|
||||||
|
if (accountConfig && accountName) {
|
||||||
this.injectAccountConfig(accountName, accountConfig);
|
this.injectAccountConfig(accountName, accountConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Linux Registry / Registry.vdf Hardening ---
|
||||||
if (platform === 'linux') {
|
if (platform === 'linux') {
|
||||||
const regLocations = [
|
const regLocations = [
|
||||||
path.join(os.homedir(), '.steam', 'registry.vdf'),
|
path.join(os.homedir(), '.steam', 'registry.vdf'),
|
||||||
@@ -210,37 +217,28 @@ class SteamClientService {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const regPath of regLocations) {
|
for (const regPath of regLocations) {
|
||||||
let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: {} } } } } };
|
if (!fs.existsSync(path.dirname(regPath))) continue;
|
||||||
|
let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: { AutoLoginUser: "", RememberPassword: "1", AlreadyLoggedIn: "1" } } } } } };
|
||||||
if (fs.existsSync(regPath)) {
|
if (fs.existsSync(regPath)) {
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(regPath, 'utf-8');
|
const content = fs.readFileSync(regPath, 'utf-8');
|
||||||
regData = parse(content) as any;
|
const parsed = parse(content) as any;
|
||||||
|
if (parsed && typeof parsed === 'object') regData = parsed;
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
} else {
|
|
||||||
const regDir = path.dirname(regPath);
|
|
||||||
if (!fs.existsSync(regDir)) fs.mkdirSync(regDir, { recursive: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPath = (obj: any, keys: string[], val: string) => {
|
const ensurePath = (obj: any, keys: string[]) => {
|
||||||
let curr = obj;
|
let curr = obj;
|
||||||
for (let i = 0; i < keys.length - 1; i++) {
|
for (const key of keys) { if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {}; curr = curr[key]; }
|
||||||
if (!curr[keys[i]!]) curr[keys[i]!] = {};
|
return curr;
|
||||||
curr = curr[keys[i]!];
|
|
||||||
}
|
|
||||||
curr[keys[keys.length - 1]!] = val;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const steamReg = ['Registry', 'HKCU', 'Software', 'Valve', 'Steam'];
|
const steamKey = ensurePath(regData, ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']);
|
||||||
setPath(regData, [...steamReg, 'AutoLoginUser'], accountName);
|
steamKey.AutoLoginUser = accountName;
|
||||||
setPath(regData, [...steamReg, 'RememberPassword'], "1");
|
steamKey.RememberPassword = "1";
|
||||||
setPath(regData, [...steamReg, 'AlreadyLoggedIn'], "1");
|
steamKey.AlreadyLoggedIn = "1";
|
||||||
setPath(regData, [...steamReg, 'WantsOfflineMode'], "0");
|
steamKey.WantsOfflineMode = "0";
|
||||||
|
try { this.safeWriteVdf(regPath, regData); } catch (e) { }
|
||||||
try {
|
|
||||||
fs.writeFileSync(regPath, stringify(regData));
|
|
||||||
console.log(`[SteamClient] Registry updated: ${regPath}`);
|
|
||||||
} catch (e) { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ultimate-ban-tracker-desktop",
|
"name": "ultimate-ban-tracker-desktop",
|
||||||
"version": "1.2.0",
|
"version": "1.3.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ultimate-ban-tracker-desktop",
|
"name": "ultimate-ban-tracker-desktop",
|
||||||
"version": "1.2.0",
|
"version": "1.3.3",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
|
|||||||
@@ -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.2.0",
|
"version": "1.3.3",
|
||||||
"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,7 +28,8 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
"dist-electron/**/*"
|
"dist-electron/**/*",
|
||||||
|
"assets-build/**/*"
|
||||||
],
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"target": [
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ 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>;
|
||||||
@@ -114,6 +115,12 @@ 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();
|
||||||
@@ -194,7 +201,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,
|
||||||
adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount
|
scrapeAccount, adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</AccountsContext.Provider>
|
</AccountsContext.Provider>
|
||||||
|
|||||||
@@ -376,11 +376,12 @@ 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 } = useAccounts();
|
const { shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, getServerUsers, serverConfig, scrapeAccount } = 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;
|
||||||
@@ -404,6 +405,12 @@ 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 {
|
||||||
@@ -522,7 +529,12 @@ 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 && (
|
||||||
<Typography variant="caption" sx={{ color: 'success.main', fontWeight: 'bold', fontSize: '0.6rem' }}>TRACKING</Typography>
|
<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>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -31,8 +31,27 @@ export const AppThemeProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
if (api?.updateServerConfig) {
|
if (api?.updateServerConfig) {
|
||||||
await api.updateServerConfig({ theme });
|
await api.updateServerConfig({ theme });
|
||||||
}
|
}
|
||||||
|
if (api?.updateAppIcon) {
|
||||||
|
try {
|
||||||
|
await api.updateAppIcon(theme);
|
||||||
|
} catch (e) { }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateIcon = async () => {
|
||||||
|
const api = (window as any).electronAPI;
|
||||||
|
if (api?.updateAppIcon && currentTheme) {
|
||||||
|
try {
|
||||||
|
await api.updateAppIcon(currentTheme);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[ThemeContext] updateAppIcon failed (likely not registered yet)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updateIcon();
|
||||||
|
}, [currentTheme]);
|
||||||
|
|
||||||
const theme = useMemo(() => getTheme(currentTheme), [currentTheme]);
|
const theme = useMemo(() => getTheme(currentTheme), [currentTheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||