Compare commits
16 Commits
release/v1
...
2124845848
| Author | SHA1 | Date | |
|---|---|---|---|
| 2124845848 | |||
| 4ad4e1c9de | |||
| 3f7c325604 | |||
| 776e05fb52 | |||
| fc19f66ace | |||
| eca3a728fc | |||
| b64ddafab9 | |||
| 9174bcfca2 | |||
| d30005acbd | |||
| a5cc155ffc | |||
| 276d3bd4de | |||
| 60b3dd1ca1 | |||
| 589acdebcb | |||
| 4037d7bce3 | |||
| 5d611fd8be | |||
| 88d2a2133c |
@@ -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,9 +60,23 @@ const initBackend = () => {
|
|||||||
};
|
};
|
||||||
// --- System Tray ---
|
// --- System Tray ---
|
||||||
const createTray = () => {
|
const createTray = () => {
|
||||||
const assetsDir = path_1.default.join(__dirname, '..', 'assets-build');
|
// Try to find the icon in various standard locations
|
||||||
const possibleIcons = ['icon.svg', 'icon.png'];
|
const possiblePaths = [
|
||||||
|
path_1.default.join(__dirname, '..', 'assets-build'), // Dev
|
||||||
|
path_1.default.join(process.resourcesPath, 'assets-build'), // Packaged (External)
|
||||||
|
path_1.default.join(electron_1.app.getAppPath(), 'dist', 'assets-build'), // Packaged (Internal dist)
|
||||||
|
path_1.default.join(electron_1.app.getAppPath(), 'assets-build') // Packaged (Internal root)
|
||||||
|
];
|
||||||
|
let assetsDir = '';
|
||||||
|
for (const p of possiblePaths) {
|
||||||
|
if (fs_1.default.existsSync(p)) {
|
||||||
|
assetsDir = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const possibleIcons = ['icon.png', 'icon.svg'];
|
||||||
let iconPath = '';
|
let iconPath = '';
|
||||||
|
if (assetsDir) {
|
||||||
for (const name of possibleIcons) {
|
for (const name of possibleIcons) {
|
||||||
const fullPath = path_1.default.join(assetsDir, name);
|
const fullPath = path_1.default.join(assetsDir, name);
|
||||||
if (fs_1.default.existsSync(fullPath)) {
|
if (fs_1.default.existsSync(fullPath)) {
|
||||||
@@ -70,8 +84,13 @@ const createTray = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!iconPath)
|
}
|
||||||
|
console.log(`[Tray] Resolved assets directory: ${assetsDir || 'NOT FOUND'}`);
|
||||||
|
console.log(`[Tray] Attempting to initialize with icon: ${iconPath || 'NONE FOUND'}`);
|
||||||
|
if (!iconPath) {
|
||||||
|
console.warn(`[Tray] FAILED: No valid icon found in searched paths.`);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
|
const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
|
||||||
tray = new electron_1.Tray(icon);
|
tray = new electron_1.Tray(icon);
|
||||||
@@ -80,7 +99,14 @@ const createTray = () => {
|
|||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
} });
|
} });
|
||||||
updateTrayMenu();
|
// Load initial themed icon
|
||||||
|
const config = store.get('serverConfig');
|
||||||
|
if (config?.theme) {
|
||||||
|
setAppIcon(config.theme);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
updateTrayMenu(); // Fallback to refresh menu
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (e) { }
|
catch (e) { }
|
||||||
};
|
};
|
||||||
@@ -108,6 +134,21 @@ const updateTrayMenu = () => {
|
|||||||
]);
|
]);
|
||||||
tray.setContextMenu(contextMenu);
|
tray.setContextMenu(contextMenu);
|
||||||
};
|
};
|
||||||
|
const setAppIcon = (themeName = 'steam') => {
|
||||||
|
const assetsDir = path_1.default.join(__dirname, '..', 'assets-build', 'icons');
|
||||||
|
const iconPath = path_1.default.join(assetsDir, `${themeName}.svg`);
|
||||||
|
if (!fs_1.default.existsSync(iconPath))
|
||||||
|
return;
|
||||||
|
const icon = electron_1.nativeImage.createFromPath(iconPath);
|
||||||
|
// Update Tray
|
||||||
|
if (tray) {
|
||||||
|
tray.setImage(icon.resize({ width: 16, height: 16 }));
|
||||||
|
}
|
||||||
|
// Update Main Window
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.setIcon(icon);
|
||||||
|
}
|
||||||
|
};
|
||||||
// --- Steam Logic ---
|
// --- Steam Logic ---
|
||||||
const killSteam = async () => {
|
const killSteam = async () => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -169,12 +210,12 @@ const scrapeAccountData = async (account) => {
|
|||||||
if (result.isActive) {
|
if (result.isActive) {
|
||||||
account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString();
|
account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString();
|
||||||
if (backend)
|
if (backend)
|
||||||
await backend.pushCooldown(account.steamId, account.cooldownExpiresAt);
|
await backend.pushCooldown(account.steamId, account.cooldownExpiresAt, now.toISOString());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
account.cooldownExpiresAt = undefined;
|
account.cooldownExpiresAt = undefined;
|
||||||
if (backend)
|
if (backend)
|
||||||
await backend.pushCooldown(account.steamId, undefined);
|
await backend.pushCooldown(account.steamId, undefined, now.toISOString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
@@ -231,8 +272,24 @@ const syncAccounts = async (isManual = false) => {
|
|||||||
exists.sessionUpdatedAt = s.sessionUpdatedAt;
|
exists.sessionUpdatedAt = s.sessionUpdatedAt;
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
}
|
}
|
||||||
if (s.cooldownExpiresAt && (!exists.cooldownExpiresAt || new Date(s.cooldownExpiresAt) > new Date(exists.cooldownExpiresAt))) {
|
// Metadata Sync (Pull)
|
||||||
|
const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0);
|
||||||
|
const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0);
|
||||||
|
if (sMetaDate > lMetaDate) {
|
||||||
|
exists.personaName = s.personaName;
|
||||||
|
exists.avatar = s.avatar;
|
||||||
|
exists.vacBanned = s.vacBanned;
|
||||||
|
exists.gameBans = s.gameBans;
|
||||||
|
exists.status = (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none';
|
||||||
|
exists.lastBanCheck = s.lastMetadataCheck;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
// Cooldown Sync (Pull)
|
||||||
|
const sScrapeDate = s.lastScrapeTime ? new Date(s.lastScrapeTime) : new Date(0);
|
||||||
|
const lScrapeDate = exists.lastScrapeTime ? new Date(exists.lastScrapeTime) : new Date(0);
|
||||||
|
if (sScrapeDate > lScrapeDate) {
|
||||||
exists.cooldownExpiresAt = s.cooldownExpiresAt;
|
exists.cooldownExpiresAt = s.cooldownExpiresAt;
|
||||||
|
exists.lastScrapeTime = s.lastScrapeTime;
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
}
|
}
|
||||||
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
|
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
|
||||||
@@ -310,11 +367,20 @@ const handleLocalAccountsFound = async (localAccounts) => {
|
|||||||
const profile = await (0, steam_web_1.fetchProfileData)(local.steamId);
|
const profile = await (0, steam_web_1.fetchProfileData)(local.steamId);
|
||||||
const bans = await (0, steam_web_1.scrapeBanStatus)(profile.profileUrl);
|
const bans = await (0, steam_web_1.scrapeBanStatus)(profile.profileUrl);
|
||||||
const localPath = await downloadAvatar(profile.steamId, profile.avatar);
|
const localPath = await downloadAvatar(profile.steamId, profile.avatar);
|
||||||
|
// Wait and retry snagging the config (Steam takes time to write it)
|
||||||
|
let loginConfig = undefined;
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
loginConfig = steam_client_1.steamClient.extractAccountConfig(local.accountName);
|
||||||
|
if (loginConfig)
|
||||||
|
break;
|
||||||
|
}
|
||||||
currentAccounts.push({
|
currentAccounts.push({
|
||||||
_id: Date.now().toString() + Math.random().toString().slice(2, 5),
|
_id: Date.now().toString() + Math.random().toString().slice(2, 5),
|
||||||
steamId: local.steamId, personaName: profile.personaName || local.accountName,
|
steamId: local.steamId, personaName: profile.personaName || local.accountName,
|
||||||
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
|
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
|
||||||
localAvatar: localPath, profileUrl: profile.profileUrl,
|
localAvatar: localPath, profileUrl: profile.profileUrl,
|
||||||
|
loginConfig, sessionUpdatedAt: loginConfig ? new Date().toISOString() : undefined,
|
||||||
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none',
|
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none',
|
||||||
vacBanned: bans.vacBanned, gameBans: bans.gameBans, lastBanCheck: new Date().toISOString()
|
vacBanned: bans.vacBanned, gameBans: bans.gameBans, lastBanCheck: new Date().toISOString()
|
||||||
});
|
});
|
||||||
@@ -544,7 +610,51 @@ electron_1.ipcMain.handle('admin-delete-user', async (event, userId) => { initBa
|
|||||||
electron_1.ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; });
|
electron_1.ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; });
|
||||||
electron_1.ipcMain.handle('admin-remove-account', async (event, steamId) => { initBackend(); if (backend)
|
electron_1.ipcMain.handle('admin-remove-account', async (event, steamId) => { initBackend(); if (backend)
|
||||||
await backend.forceRemoveAccount(steamId); return true; });
|
await backend.forceRemoveAccount(steamId); return true; });
|
||||||
electron_1.ipcMain.handle('switch-account', async (event, loginName) => await handleSwitchAccount(loginName));
|
electron_1.ipcMain.handle('force-sync', async () => { await syncAccounts(true); return true; });
|
||||||
|
electron_1.ipcMain.handle('update-app-icon', (event, themeName) => {
|
||||||
|
setAppIcon(themeName);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
electron_1.ipcMain.handle('switch-account', async (event, loginName) => {
|
||||||
|
if (!loginName)
|
||||||
|
return false;
|
||||||
|
try {
|
||||||
|
// PROACTIVE SYNC: Try to snag the freshest token before we kill Steam
|
||||||
|
const accounts = store.get('accounts');
|
||||||
|
const account = accounts.find(a => a.loginName === loginName);
|
||||||
|
if (account && !account._id.startsWith('shared_')) {
|
||||||
|
const freshConfig = steam_client_1.steamClient.extractAccountConfig(loginName);
|
||||||
|
if (freshConfig) {
|
||||||
|
account.loginConfig = freshConfig;
|
||||||
|
account.sessionUpdatedAt = new Date().toISOString();
|
||||||
|
if (backend)
|
||||||
|
await backend.shareAccount(account);
|
||||||
|
store.set('accounts', accounts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await killSteam();
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const regBase = 'reg add "HKCU\\Software\\Valve\\Steam"';
|
||||||
|
const commands = [
|
||||||
|
`${regBase} /v AutoLoginUser /t REG_SZ /d "${loginName}" /f`,
|
||||||
|
`${regBase} /v RememberPassword /t REG_DWORD /d 1 /f`,
|
||||||
|
`${regBase} /v AlreadyLoggedIn /t REG_DWORD /d 1 /f`,
|
||||||
|
`${regBase} /v WantsOfflineMode /t REG_DWORD /d 0 /f`
|
||||||
|
];
|
||||||
|
await new Promise((res, rej) => (0, child_process_1.exec)(commands.join(' && '), (e) => e ? rej(e) : res()));
|
||||||
|
if (account && account.loginConfig)
|
||||||
|
steam_client_1.steamClient.injectAccountConfig(loginName, account.loginConfig);
|
||||||
|
}
|
||||||
|
else if (process.platform === 'linux') {
|
||||||
|
await steam_client_1.steamClient.setAutoLoginUser(loginName, account?.loginConfig, account?.steamId);
|
||||||
|
}
|
||||||
|
startSteam();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
electron_1.ipcMain.handle('open-external', (event, url) => electron_1.shell.openExternal(url));
|
electron_1.ipcMain.handle('open-external', (event, url) => electron_1.shell.openExternal(url));
|
||||||
electron_1.ipcMain.handle('open-steam-app-login', async () => {
|
electron_1.ipcMain.handle('open-steam-app-login', async () => {
|
||||||
await killSteam();
|
await killSteam();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -57,19 +57,25 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => {
|
|||||||
let expirationDate = undefined;
|
let expirationDate = undefined;
|
||||||
$('table').each((_, table) => {
|
$('table').each((_, table) => {
|
||||||
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
|
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
|
||||||
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration'));
|
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration') || h.includes('Cooldown Expiration'));
|
||||||
if (expirationIndex !== -1) {
|
if (expirationIndex !== -1) {
|
||||||
const firstRow = $(table).find('tr').not(':has(th)').first();
|
const rows = $(table).find('tr').not(':has(th)');
|
||||||
const dateText = firstRow.find('td').eq(expirationIndex).text().trim();
|
rows.each((_, row) => {
|
||||||
|
const dateText = $(row).find('td').eq(expirationIndex).text().trim();
|
||||||
if (dateText && dateText !== '') {
|
if (dateText && dateText !== '') {
|
||||||
|
// Steam uses 'GMT' which some JS engines don't parse well, replace with 'UTC'
|
||||||
const cleanDateText = dateText.replace(' GMT', ' UTC');
|
const cleanDateText = dateText.replace(' GMT', ' UTC');
|
||||||
const parsed = new Date(cleanDateText);
|
const parsed = new Date(cleanDateText);
|
||||||
if (!isNaN(parsed.getTime())) {
|
if (!isNaN(parsed.getTime())) {
|
||||||
|
// We want the newest expiration date found
|
||||||
|
if (!expirationDate || parsed > expirationDate) {
|
||||||
expirationDate = parsed;
|
expirationDate = parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
if (expirationDate && expirationDate.getTime() > Date.now()) {
|
if (expirationDate && expirationDate.getTime() > Date.now()) {
|
||||||
console.log(`[Scraper] Found active cooldown until: ${expirationDate.toISOString()}`);
|
console.log(`[Scraper] Found active cooldown until: ${expirationDate.toISOString()}`);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ class SteamClientService {
|
|||||||
if (platform === 'win32') {
|
if (platform === 'win32') {
|
||||||
const possiblePaths = [
|
const possiblePaths = [
|
||||||
'C:\\Program Files (x86)\\Steam',
|
'C:\\Program Files (x86)\\Steam',
|
||||||
'C:\\Program Files\\Steam'
|
'C:\\Program Files\\Steam',
|
||||||
|
path_1.default.join(process.env.APPDATA || '', 'Steam'),
|
||||||
];
|
];
|
||||||
this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null;
|
this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null;
|
||||||
}
|
}
|
||||||
@@ -29,7 +30,8 @@ class SteamClientService {
|
|||||||
const possiblePaths = [
|
const possiblePaths = [
|
||||||
path_1.default.join(home, '.steam/steam'),
|
path_1.default.join(home, '.steam/steam'),
|
||||||
path_1.default.join(home, '.local/share/Steam'),
|
path_1.default.join(home, '.local/share/Steam'),
|
||||||
path_1.default.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam')
|
path_1.default.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam'), // Flatpak
|
||||||
|
path_1.default.join(home, 'snap/steam/common/.steam/steam'), // Snap
|
||||||
];
|
];
|
||||||
this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null;
|
this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null;
|
||||||
}
|
}
|
||||||
@@ -47,12 +49,35 @@ class SteamClientService {
|
|||||||
return null;
|
return null;
|
||||||
return path_1.default.join(this.steamPath, 'config', 'config.vdf');
|
return path_1.default.join(this.steamPath, 'config', 'config.vdf');
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Safe Atomic Write: Writes to a temp file and renames it.
|
||||||
|
* This prevents file corruption if the app crashes during write.
|
||||||
|
*/
|
||||||
|
safeWriteVdf(filePath, data) {
|
||||||
|
const tempPath = `${filePath}.tmp_${Date.now()}`;
|
||||||
|
const dir = path_1.default.dirname(filePath);
|
||||||
|
try {
|
||||||
|
if (!fs_1.default.existsSync(dir))
|
||||||
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
||||||
|
const vdfContent = (0, simple_vdf_1.stringify)(data);
|
||||||
|
fs_1.default.writeFileSync(tempPath, vdfContent, 'utf-8');
|
||||||
|
// Atomic rename
|
||||||
|
fs_1.default.renameSync(tempPath, filePath);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(`[SteamClient] Atomic write failed for ${filePath}: ${e.message}`);
|
||||||
|
if (fs_1.default.existsSync(tempPath))
|
||||||
|
fs_1.default.unlinkSync(tempPath);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
startWatching(callback) {
|
startWatching(callback) {
|
||||||
this.onAccountsChanged = callback;
|
this.onAccountsChanged = callback;
|
||||||
const loginUsersPath = this.getLoginUsersPath();
|
const loginUsersPath = this.getLoginUsersPath();
|
||||||
if (loginUsersPath && fs_1.default.existsSync(loginUsersPath)) {
|
if (loginUsersPath && fs_1.default.existsSync(loginUsersPath)) {
|
||||||
this.readLocalAccounts();
|
this.readLocalAccounts();
|
||||||
chokidar_1.default.watch(loginUsersPath, { persistent: true }).on('change', () => {
|
chokidar_1.default.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => {
|
||||||
|
console.log(`[SteamClient] loginusers.vdf changed, re-scanning...`);
|
||||||
this.readLocalAccounts();
|
this.readLocalAccounts();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -63,16 +88,20 @@ class SteamClientService {
|
|||||||
return;
|
return;
|
||||||
try {
|
try {
|
||||||
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
||||||
|
if (!content.trim())
|
||||||
|
return; // Empty file
|
||||||
const data = (0, simple_vdf_1.parse)(content);
|
const data = (0, simple_vdf_1.parse)(content);
|
||||||
if (!data || !data.users)
|
if (!data || !data.users)
|
||||||
return;
|
return;
|
||||||
const accounts = [];
|
const accounts = [];
|
||||||
for (const [steamId64, userData] of Object.entries(data.users)) {
|
for (const [steamId64, userData] of Object.entries(data.users)) {
|
||||||
const user = userData;
|
const user = userData;
|
||||||
|
if (!user || !user.AccountName)
|
||||||
|
continue;
|
||||||
accounts.push({
|
accounts.push({
|
||||||
steamId: steamId64,
|
steamId: steamId64,
|
||||||
accountName: user.AccountName,
|
accountName: user.AccountName,
|
||||||
personaName: user.PersonaName,
|
personaName: user.PersonaName || user.AccountName,
|
||||||
timestamp: parseInt(user.Timestamp) || 0
|
timestamp: parseInt(user.Timestamp) || 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -91,32 +120,38 @@ class SteamClientService {
|
|||||||
const content = fs_1.default.readFileSync(configPath, 'utf-8');
|
const content = fs_1.default.readFileSync(configPath, 'utf-8');
|
||||||
const data = (0, simple_vdf_1.parse)(content);
|
const data = (0, simple_vdf_1.parse)(content);
|
||||||
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
|
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
|
||||||
if (accounts && accounts[accountName]) {
|
return (accounts && accounts[accountName]) ? accounts[accountName] : null;
|
||||||
return accounts[accountName];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error('[SteamClient] Failed to extract config.vdf data');
|
console.error('[SteamClient] Failed to extract config.vdf data');
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
injectAccountConfig(accountName, accountData) {
|
injectAccountConfig(accountName, accountData) {
|
||||||
const configPath = this.getConfigVdfPath();
|
const configPath = this.getConfigVdfPath();
|
||||||
if (!configPath)
|
if (!configPath)
|
||||||
return;
|
return;
|
||||||
// Create directory if it doesn't exist
|
let data = {
|
||||||
const configDir = path_1.default.dirname(configPath);
|
InstallConfigStore: {
|
||||||
if (!fs_1.default.existsSync(configDir))
|
Software: {
|
||||||
fs_1.default.mkdirSync(configDir, { recursive: true });
|
Valve: {
|
||||||
let data = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } };
|
Steam: {
|
||||||
|
Accounts: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
if (fs_1.default.existsSync(configPath)) {
|
if (fs_1.default.existsSync(configPath)) {
|
||||||
try {
|
try {
|
||||||
const content = fs_1.default.readFileSync(configPath, 'utf-8');
|
const content = fs_1.default.readFileSync(configPath, 'utf-8');
|
||||||
data = (0, simple_vdf_1.parse)(content);
|
const parsed = (0, simple_vdf_1.parse)(content);
|
||||||
|
if (parsed && typeof parsed === 'object')
|
||||||
|
data = parsed;
|
||||||
}
|
}
|
||||||
catch (e) { }
|
catch (e) { }
|
||||||
}
|
}
|
||||||
// Ensure structure exists
|
// Ensure safe nesting
|
||||||
if (!data.InstallConfigStore)
|
if (!data.InstallConfigStore)
|
||||||
data.InstallConfigStore = {};
|
data.InstallConfigStore = {};
|
||||||
if (!data.InstallConfigStore.Software)
|
if (!data.InstallConfigStore.Software)
|
||||||
@@ -129,25 +164,22 @@ class SteamClientService {
|
|||||||
data.InstallConfigStore.Software.Valve.Steam.Accounts = {};
|
data.InstallConfigStore.Software.Valve.Steam.Accounts = {};
|
||||||
data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData;
|
data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData;
|
||||||
try {
|
try {
|
||||||
fs_1.default.writeFileSync(configPath, (0, simple_vdf_1.stringify)(data));
|
this.safeWriteVdf(configPath, data);
|
||||||
console.log(`[SteamClient] Injected login config for ${accountName} into config.vdf`);
|
console.log(`[SteamClient] Safely injected session for ${accountName}`);
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('[SteamClient] Failed to write config.vdf');
|
|
||||||
}
|
}
|
||||||
|
catch (e) { }
|
||||||
}
|
}
|
||||||
async setAutoLoginUser(accountName, accountConfig, steamId) {
|
async setAutoLoginUser(accountName, accountConfig, steamId) {
|
||||||
const platform = os_1.default.platform();
|
const platform = os_1.default.platform();
|
||||||
const loginUsersPath = this.getLoginUsersPath();
|
const loginUsersPath = this.getLoginUsersPath();
|
||||||
if (loginUsersPath) {
|
if (loginUsersPath) {
|
||||||
const configDir = path_1.default.dirname(loginUsersPath);
|
|
||||||
if (!fs_1.default.existsSync(configDir))
|
|
||||||
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
||||||
let data = { users: {} };
|
let data = { users: {} };
|
||||||
if (fs_1.default.existsSync(loginUsersPath)) {
|
if (fs_1.default.existsSync(loginUsersPath)) {
|
||||||
try {
|
try {
|
||||||
const content = fs_1.default.readFileSync(loginUsersPath, 'utf-8');
|
const content = fs_1.default.readFileSync(loginUsersPath, 'utf-8');
|
||||||
data = (0, simple_vdf_1.parse)(content);
|
const parsed = (0, simple_vdf_1.parse)(content);
|
||||||
|
if (parsed && parsed.users)
|
||||||
|
data = parsed;
|
||||||
}
|
}
|
||||||
catch (e) { }
|
catch (e) { }
|
||||||
}
|
}
|
||||||
@@ -156,7 +188,7 @@ class SteamClientService {
|
|||||||
let found = false;
|
let found = false;
|
||||||
for (const [id, user] of Object.entries(data.users)) {
|
for (const [id, user] of Object.entries(data.users)) {
|
||||||
const u = user;
|
const u = user;
|
||||||
if (u.AccountName.toLowerCase() === accountName.toLowerCase()) {
|
if (u.AccountName?.toLowerCase() === accountName.toLowerCase()) {
|
||||||
u.mostrecent = "1";
|
u.mostrecent = "1";
|
||||||
u.RememberPassword = "1";
|
u.RememberPassword = "1";
|
||||||
u.AllowAutoLogin = "1";
|
u.AllowAutoLogin = "1";
|
||||||
@@ -169,8 +201,8 @@ class SteamClientService {
|
|||||||
u.mostrecent = "0";
|
u.mostrecent = "0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!found && steamId) {
|
if (!found && steamId && accountName) {
|
||||||
console.log(`[SteamClient] Provisioning user ${accountName} into loginusers.vdf`);
|
console.log(`[SteamClient] Provisioning new user profile for ${accountName}`);
|
||||||
data.users[steamId] = {
|
data.users[steamId] = {
|
||||||
AccountName: accountName,
|
AccountName: accountName,
|
||||||
PersonaName: accountName,
|
PersonaName: accountName,
|
||||||
@@ -184,51 +216,53 @@ class SteamClientService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
fs_1.default.writeFileSync(loginUsersPath, (0, simple_vdf_1.stringify)(data));
|
this.safeWriteVdf(loginUsersPath, data);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) { }
|
||||||
console.error('[SteamClient] Failed to write loginusers.vdf');
|
|
||||||
}
|
}
|
||||||
}
|
if (accountConfig && accountName) {
|
||||||
if (accountConfig) {
|
|
||||||
this.injectAccountConfig(accountName, accountConfig);
|
this.injectAccountConfig(accountName, accountConfig);
|
||||||
}
|
}
|
||||||
|
// --- Linux Registry / Registry.vdf Hardening ---
|
||||||
if (platform === 'linux') {
|
if (platform === 'linux') {
|
||||||
const regLocations = [
|
const regLocations = [
|
||||||
path_1.default.join(os_1.default.homedir(), '.steam', 'registry.vdf'),
|
path_1.default.join(os_1.default.homedir(), '.steam', 'registry.vdf'),
|
||||||
path_1.default.join(os_1.default.homedir(), '.steam', 'steam', 'registry.vdf')
|
path_1.default.join(os_1.default.homedir(), '.steam', 'steam', 'registry.vdf')
|
||||||
];
|
];
|
||||||
for (const regPath of regLocations) {
|
for (const regPath of regLocations) {
|
||||||
let regData = { Registry: { HKCU: { Software: { Valve: { Steam: {} } } } } };
|
if (!fs_1.default.existsSync(path_1.default.dirname(regPath)))
|
||||||
|
continue;
|
||||||
|
let regData = { Registry: { HKCU: { Software: { Valve: { Steam: {
|
||||||
|
AutoLoginUser: "",
|
||||||
|
RememberPassword: "1",
|
||||||
|
AlreadyLoggedIn: "1"
|
||||||
|
} } } } } };
|
||||||
if (fs_1.default.existsSync(regPath)) {
|
if (fs_1.default.existsSync(regPath)) {
|
||||||
try {
|
try {
|
||||||
const content = fs_1.default.readFileSync(regPath, 'utf-8');
|
const content = fs_1.default.readFileSync(regPath, 'utf-8');
|
||||||
regData = (0, simple_vdf_1.parse)(content);
|
const parsed = (0, simple_vdf_1.parse)(content);
|
||||||
|
if (parsed && typeof parsed === 'object')
|
||||||
|
regData = parsed;
|
||||||
}
|
}
|
||||||
catch (e) { }
|
catch (e) { }
|
||||||
}
|
}
|
||||||
else {
|
// Deep merge helper
|
||||||
const regDir = path_1.default.dirname(regPath);
|
const ensurePath = (obj, keys) => {
|
||||||
if (!fs_1.default.existsSync(regDir))
|
|
||||||
fs_1.default.mkdirSync(regDir, { recursive: true });
|
|
||||||
}
|
|
||||||
const setPath = (obj, keys, val) => {
|
|
||||||
let curr = obj;
|
let curr = obj;
|
||||||
for (let i = 0; i < keys.length - 1; i++) {
|
for (const key of keys) {
|
||||||
if (!curr[keys[i]])
|
if (!curr[key] || typeof curr[key] !== 'object')
|
||||||
curr[keys[i]] = {};
|
curr[key] = {};
|
||||||
curr = curr[keys[i]];
|
curr = curr[key];
|
||||||
}
|
}
|
||||||
curr[keys[keys.length - 1]] = val;
|
return curr;
|
||||||
};
|
};
|
||||||
const steamReg = ['Registry', 'HKCU', 'Software', 'Valve', 'Steam'];
|
const steamKey = ensurePath(regData, ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']);
|
||||||
setPath(regData, [...steamReg, 'AutoLoginUser'], accountName);
|
steamKey.AutoLoginUser = accountName;
|
||||||
setPath(regData, [...steamReg, 'RememberPassword'], "1");
|
steamKey.RememberPassword = "1";
|
||||||
setPath(regData, [...steamReg, 'AlreadyLoggedIn'], "1");
|
steamKey.AlreadyLoggedIn = "1";
|
||||||
setPath(regData, [...steamReg, 'WantsOfflineMode'], "0");
|
steamKey.WantsOfflineMode = "0";
|
||||||
try {
|
try {
|
||||||
fs_1.default.writeFileSync(regPath, (0, simple_vdf_1.stringify)(regData));
|
this.safeWriteVdf(regPath, regData);
|
||||||
console.log(`[SteamClient] Registry updated: ${regPath}`);
|
|
||||||
}
|
}
|
||||||
catch (e) { }
|
catch (e) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,14 @@ const createTray = () => {
|
|||||||
tray = new Tray(icon);
|
tray = new Tray(icon);
|
||||||
tray.setToolTip('Ultimate Ban Tracker');
|
tray.setToolTip('Ultimate Ban Tracker');
|
||||||
tray.on('click', () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } });
|
tray.on('click', () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } });
|
||||||
updateTrayMenu();
|
|
||||||
|
// Load initial themed icon
|
||||||
|
const config = store.get('serverConfig');
|
||||||
|
if (config?.theme) {
|
||||||
|
setAppIcon(config.theme);
|
||||||
|
} else {
|
||||||
|
updateTrayMenu(); // Fallback to refresh menu
|
||||||
|
}
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,6 +169,25 @@ const updateTrayMenu = () => {
|
|||||||
tray.setContextMenu(contextMenu);
|
tray.setContextMenu(contextMenu);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setAppIcon = (themeName: string = 'steam') => {
|
||||||
|
const assetsDir = path.join(__dirname, '..', 'assets-build', 'icons');
|
||||||
|
const iconPath = path.join(assetsDir, `${themeName}.svg`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(iconPath)) return;
|
||||||
|
|
||||||
|
const icon = nativeImage.createFromPath(iconPath);
|
||||||
|
|
||||||
|
// Update Tray
|
||||||
|
if (tray) {
|
||||||
|
tray.setImage(icon.resize({ width: 16, height: 16 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Main Window
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.setIcon(icon);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- Steam Logic ---
|
// --- Steam Logic ---
|
||||||
const killSteam = async () => {
|
const killSteam = async () => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
@@ -215,10 +241,10 @@ const scrapeAccountData = async (account: Account) => {
|
|||||||
account.authError = false; account.lastScrapeTime = now.toISOString();
|
account.authError = false; account.lastScrapeTime = now.toISOString();
|
||||||
if (result.isActive) {
|
if (result.isActive) {
|
||||||
account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString();
|
account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString();
|
||||||
if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt);
|
if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt, now.toISOString());
|
||||||
} else {
|
} else {
|
||||||
account.cooldownExpiresAt = undefined;
|
account.cooldownExpiresAt = undefined;
|
||||||
if (backend) await backend.pushCooldown(account.steamId, undefined);
|
if (backend) await backend.pushCooldown(account.steamId, undefined, now.toISOString());
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.message.includes('cookie') || e.message.includes('Sign In')) account.authError = true;
|
if (e.message.includes('cookie') || e.message.includes('Sign In')) account.authError = true;
|
||||||
@@ -267,10 +293,29 @@ const syncAccounts = async (isManual = false) => {
|
|||||||
exists.sessionUpdatedAt = s.sessionUpdatedAt;
|
exists.sessionUpdatedAt = s.sessionUpdatedAt;
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
}
|
}
|
||||||
if (s.cooldownExpiresAt && (!exists.cooldownExpiresAt || new Date(s.cooldownExpiresAt) > new Date(exists.cooldownExpiresAt))) {
|
|
||||||
exists.cooldownExpiresAt = s.cooldownExpiresAt;
|
// Metadata Sync (Pull)
|
||||||
|
const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0);
|
||||||
|
const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0);
|
||||||
|
if (sMetaDate > lMetaDate) {
|
||||||
|
exists.personaName = s.personaName;
|
||||||
|
exists.avatar = s.avatar;
|
||||||
|
exists.vacBanned = s.vacBanned;
|
||||||
|
exists.gameBans = s.gameBans;
|
||||||
|
exists.status = (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none';
|
||||||
|
exists.lastBanCheck = s.lastMetadataCheck;
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cooldown Sync (Pull)
|
||||||
|
const sScrapeDate = s.lastScrapeTime ? new Date(s.lastScrapeTime) : new Date(0);
|
||||||
|
const lScrapeDate = exists.lastScrapeTime ? new Date(exists.lastScrapeTime) : new Date(0);
|
||||||
|
if (sScrapeDate > lScrapeDate) {
|
||||||
|
exists.cooldownExpiresAt = s.cooldownExpiresAt;
|
||||||
|
exists.lastScrapeTime = s.lastScrapeTime;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
|
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
|
||||||
exists.sharedWith = s.sharedWith;
|
exists.sharedWith = s.sharedWith;
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
@@ -337,11 +382,21 @@ const handleLocalAccountsFound = async (localAccounts: LocalSteamAccount[]) => {
|
|||||||
const profile = await fetchProfileData(local.steamId);
|
const profile = await fetchProfileData(local.steamId);
|
||||||
const bans = await scrapeBanStatus(profile.profileUrl);
|
const bans = await scrapeBanStatus(profile.profileUrl);
|
||||||
const localPath = await downloadAvatar(profile.steamId, profile.avatar);
|
const localPath = await downloadAvatar(profile.steamId, profile.avatar);
|
||||||
|
|
||||||
|
// Wait and retry snagging the config (Steam takes time to write it)
|
||||||
|
let loginConfig = undefined;
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
loginConfig = steamClient.extractAccountConfig(local.accountName);
|
||||||
|
if (loginConfig) break;
|
||||||
|
}
|
||||||
|
|
||||||
currentAccounts.push({
|
currentAccounts.push({
|
||||||
_id: Date.now().toString() + Math.random().toString().slice(2, 5),
|
_id: Date.now().toString() + Math.random().toString().slice(2, 5),
|
||||||
steamId: local.steamId, personaName: profile.personaName || local.accountName,
|
steamId: local.steamId, personaName: profile.personaName || local.accountName,
|
||||||
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
|
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
|
||||||
localAvatar: localPath, profileUrl: profile.profileUrl,
|
localAvatar: localPath, profileUrl: profile.profileUrl,
|
||||||
|
loginConfig, sessionUpdatedAt: loginConfig ? new Date().toISOString() : undefined,
|
||||||
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none',
|
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none',
|
||||||
vacBanned: bans.vacBanned, gameBans: bans.gameBans, lastBanCheck: new Date().toISOString()
|
vacBanned: bans.vacBanned, gameBans: bans.gameBans, lastBanCheck: new Date().toISOString()
|
||||||
});
|
});
|
||||||
@@ -542,7 +597,49 @@ ipcMain.handle('admin-delete-user', async (event, userId: string) => { initBacke
|
|||||||
ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; });
|
ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; });
|
||||||
ipcMain.handle('admin-remove-account', async (event, steamId: string) => { initBackend(); if (backend) await backend.forceRemoveAccount(steamId); return true; });
|
ipcMain.handle('admin-remove-account', async (event, steamId: string) => { initBackend(); if (backend) await backend.forceRemoveAccount(steamId); return true; });
|
||||||
|
|
||||||
ipcMain.handle('switch-account', async (event, loginName: string) => await handleSwitchAccount(loginName));
|
ipcMain.handle('force-sync', async () => { await syncAccounts(true); return true; });
|
||||||
|
|
||||||
|
ipcMain.handle('update-app-icon', (event, themeName: string) => {
|
||||||
|
setAppIcon(themeName);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('switch-account', async (event, loginName: string) => {
|
||||||
|
if (!loginName) return false;
|
||||||
|
try {
|
||||||
|
// PROACTIVE SYNC: Try to snag the freshest token before we kill Steam
|
||||||
|
const accounts = store.get('accounts') as Account[];
|
||||||
|
const account = accounts.find(a => a.loginName === loginName);
|
||||||
|
if (account && !account._id.startsWith('shared_')) {
|
||||||
|
const freshConfig = steamClient.extractAccountConfig(loginName);
|
||||||
|
if (freshConfig) {
|
||||||
|
account.loginConfig = freshConfig;
|
||||||
|
account.sessionUpdatedAt = new Date().toISOString();
|
||||||
|
if (backend) await backend.shareAccount(account);
|
||||||
|
store.set('accounts', accounts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await killSteam();
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const regBase = 'reg add "HKCU\\Software\\Valve\\Steam"';
|
||||||
|
const commands = [
|
||||||
|
`${regBase} /v AutoLoginUser /t REG_SZ /d "${loginName}" /f`,
|
||||||
|
`${regBase} /v RememberPassword /t REG_DWORD /d 1 /f`,
|
||||||
|
`${regBase} /v AlreadyLoggedIn /t REG_DWORD /d 1 /f`,
|
||||||
|
`${regBase} /v WantsOfflineMode /t REG_DWORD /d 0 /f`
|
||||||
|
];
|
||||||
|
await new Promise<void>((res, rej) => exec(commands.join(' && '), (e) => e ? rej(e) : res()));
|
||||||
|
if (account && account.loginConfig) steamClient.injectAccountConfig(loginName, account.loginConfig);
|
||||||
|
} else if (process.platform === 'linux') {
|
||||||
|
await steamClient.setAutoLoginUser(loginName, account?.loginConfig, account?.steamId);
|
||||||
|
}
|
||||||
|
startSteam();
|
||||||
|
return true;
|
||||||
|
} catch (e) { return false; }
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url));
|
ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url));
|
||||||
|
|
||||||
ipcMain.handle('open-steam-app-login', async () => {
|
ipcMain.handle('open-steam-app-login', async () => {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -29,22 +29,27 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
|
|||||||
|
|
||||||
$('table').each((_, table) => {
|
$('table').each((_, table) => {
|
||||||
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
|
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
|
||||||
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration'));
|
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration') || h.includes('Cooldown Expiration'));
|
||||||
|
|
||||||
if (expirationIndex !== -1) {
|
if (expirationIndex !== -1) {
|
||||||
const firstRow = $(table).find('tr').not(':has(th)').first();
|
const rows = $(table).find('tr').not(':has(th)');
|
||||||
const dateText = firstRow.find('td').eq(expirationIndex).text().trim();
|
rows.each((_, row) => {
|
||||||
|
const dateText = $(row).find('td').eq(expirationIndex).text().trim();
|
||||||
if (dateText && dateText !== '') {
|
if (dateText && dateText !== '') {
|
||||||
|
// Steam uses 'GMT' which some JS engines don't parse well, replace with 'UTC'
|
||||||
const cleanDateText = dateText.replace(' GMT', ' UTC');
|
const cleanDateText = dateText.replace(' GMT', ' UTC');
|
||||||
const parsed = new Date(cleanDateText);
|
const parsed = new Date(cleanDateText);
|
||||||
|
|
||||||
if (!isNaN(parsed.getTime())) {
|
if (!isNaN(parsed.getTime())) {
|
||||||
|
// We want the newest expiration date found
|
||||||
|
if (!expirationDate || parsed > (expirationDate as Date)) {
|
||||||
expirationDate = parsed;
|
expirationDate = parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (expirationDate && (expirationDate as Date).getTime() > Date.now()) {
|
if (expirationDate && (expirationDate as Date).getTime() > Date.now()) {
|
||||||
console.log(`[Scraper] Found active cooldown until: ${(expirationDate as Date).toISOString()}`);
|
console.log(`[Scraper] Found active cooldown until: ${(expirationDate as Date).toISOString()}`);
|
||||||
|
|||||||
@@ -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.3.0",
|
"version": "1.3.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ultimate-ban-tracker-desktop",
|
"name": "ultimate-ban-tracker-desktop",
|
||||||
"version": "1.3.0",
|
"version": "1.3.2",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ultimate-ban-tracker-desktop",
|
"name": "ultimate-ban-tracker-desktop",
|
||||||
"description": "Professional Steam Account Manager & Ban Tracker",
|
"description": "Professional Steam Account Manager & Ban Tracker",
|
||||||
"version": "1.3.0",
|
"version": "1.3.2",
|
||||||
"author": "Nils Pukropp <nils@narl.io>",
|
"author": "Nils Pukropp <nils@narl.io>",
|
||||||
"homepage": "https://narl.io",
|
"homepage": "https://narl.io",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||