From 1f5d2e08e57b1699d94eacb7553f3e3648d1be2a Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 02:45:01 +0100 Subject: [PATCH 1/8] feat: implement granular access revocation and global unsharing in the account permissions dialog --- frontend/electron/main.ts | 12 ++ frontend/electron/preload.ts | 2 + frontend/electron/services/backend.ts | 30 +++- frontend/src/hooks/useAccounts.tsx | 16 +- frontend/src/pages/Dashboard.tsx | 204 +++++++++------------- frontend/src/pages/DashboardRow.tsx | 234 ++++++++++++++++++++++++++ 6 files changed, 372 insertions(+), 126 deletions(-) create mode 100644 frontend/src/pages/DashboardRow.tsx diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index a009e28..481850d 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -488,6 +488,18 @@ ipcMain.handle('share-account-with-user', async (event, steamId: string, targetS throw new Error('Backend not configured'); }); +ipcMain.handle('revoke-account-access', async (event, steamId: string, targetSteamId: string) => { + initBackend(); + if (backend) return await backend.revokeAccess(steamId, targetSteamId); + throw new Error('Backend not configured'); +}); + +ipcMain.handle('revoke-all-account-access', async (event, steamId: string) => { + initBackend(); + if (backend) return await backend.revokeAllAccess(steamId); + throw new Error('Backend not configured'); +}); + ipcMain.handle('get-community-accounts', async () => { initBackend(); return backend ? await backend.getCommunityAccounts() : []; }); ipcMain.handle('get-server-users', async () => { initBackend(); return backend ? await backend.getServerUsers() : []; }); ipcMain.handle('switch-account', async (event, loginName: string) => await handleSwitchAccount(loginName)); diff --git a/frontend/electron/preload.ts b/frontend/electron/preload.ts index 4be24f8..510657b 100644 --- a/frontend/electron/preload.ts +++ b/frontend/electron/preload.ts @@ -7,6 +7,8 @@ contextBridge.exposeInMainWorld('electronAPI', { deleteAccount: (id: string) => ipcRenderer.invoke('delete-account', id), switchAccount: (loginName: string) => ipcRenderer.invoke('switch-account', loginName), shareAccountWithUser: (steamId: string, targetSteamId: string) => ipcRenderer.invoke('share-account-with-user', steamId, targetSteamId), + revokeAccountAccess: (steamId: string, targetSteamId: string) => ipcRenderer.invoke('revoke-account-access', steamId, targetSteamId), + revokeAllAccountAccess: (steamId: string) => ipcRenderer.invoke('revoke-all-account-access', steamId), openExternal: (url: string) => ipcRenderer.invoke('open-external', url), openSteamLogin: (steamId: string) => ipcRenderer.invoke('open-steam-login', steamId), diff --git a/frontend/electron/services/backend.ts b/frontend/electron/services/backend.ts index d838010..2a12fe2 100644 --- a/frontend/electron/services/backend.ts +++ b/frontend/electron/services/backend.ts @@ -61,7 +61,8 @@ export class BackendService { gameBans: account.gameBans, loginName: account.loginName, steamLoginSecure: account.steamLoginSecure, - loginConfig: account.loginConfig + loginConfig: account.loginConfig, + sessionUpdatedAt: account.sessionUpdatedAt }, { headers: this.headers }); } catch (e) { console.error('[Backend] Failed to share account'); @@ -91,4 +92,31 @@ export class BackendService { throw new Error(e.response?.data?.message || 'Failed to share account'); } } + + public async revokeAccess(steamId: string, targetSteamId: string) { + if (!this.token) return; + try { + const response = await axios.delete(`${this.url}/api/sync/${steamId}/share`, { + headers: this.headers, + data: { targetSteamId } + }); + return response.data; + } catch (e: any) { + console.error(`[Backend] Failed to revoke access for ${steamId} from ${targetSteamId}`); + throw new Error(e.response?.data?.message || 'Failed to revoke access'); + } + } + + public async revokeAllAccess(steamId: string) { + if (!this.token) return; + try { + const response = await axios.delete(`${this.url}/api/sync/${steamId}/share/all`, { + headers: this.headers + }); + return response.data; + } catch (e: any) { + console.error(`[Backend] Failed to revoke all access for ${steamId}`); + throw new Error(e.response?.data?.message || 'Failed to revoke all access'); + } + } } diff --git a/frontend/src/hooks/useAccounts.tsx b/frontend/src/hooks/useAccounts.tsx index b055eaa..bf27a08 100644 --- a/frontend/src/hooks/useAccounts.tsx +++ b/frontend/src/hooks/useAccounts.tsx @@ -40,6 +40,8 @@ interface AccountsContextType { switchAccount: (loginName: string) => Promise; openSteamLogin: (steamId: string) => Promise; shareAccountWithUser: (steamId: string, targetSteamId: string) => Promise; + revokeAccountAccess: (steamId: string, targetSteamId: string) => Promise; + revokeAllAccountAccess: (steamId: string) => Promise; // Server Methods updateServerConfig: (config: Partial) => Promise; @@ -136,6 +138,18 @@ export const AccountsProvider: React.FC<{ children: React.ReactNode }> = ({ chil return res; }; + const revokeAccountAccess = async (steamId: string, targetSteamId: string) => { + const res = await (window as any).electronAPI.revokeAccountAccess(steamId, targetSteamId); + await syncNow(); + return res; + }; + + const revokeAllAccountAccess = async (steamId: string) => { + const res = await (window as any).electronAPI.revokeAllAccountAccess(steamId); + await syncNow(); + return res; + }; + const updateServerConfig = async (config: Partial) => { const updated = await (window as any).electronAPI.updateServerConfig(config); setServerConfig(updated); @@ -159,7 +173,7 @@ export const AccountsProvider: React.FC<{ children: React.ReactNode }> = ({ chil {children} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 5ec57ae..293fa51 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -49,9 +49,7 @@ const Dashboard: React.FC = () => { const [serverUrl, setServerUrl] = useState(''); useEffect(() => { - if (serverConfig?.url) { - setServerUrl(serverConfig.url); - } + if (serverConfig?.url) setServerUrl(serverConfig.url); }, [serverConfig?.url]); const loadCommunity = async () => { @@ -59,16 +57,11 @@ const Dashboard: React.FC = () => { try { const data = await getCommunityAccounts(); setCommunityAccounts(Array.isArray(data) ? data : []); - } catch (e) { - } finally { - setIsCommunityLoading(false); - } + } catch (e) { } finally { setIsCommunityLoading(false); } }; useEffect(() => { - if (isAddDialogOpen && addTab === 1) { - loadCommunity(); - } + if (isAddDialogOpen && addTab === 1) loadCommunity(); }, [isAddDialogOpen, addTab]); const handleAddAccount = async () => { @@ -77,9 +70,7 @@ const Dashboard: React.FC = () => { await addAccount({ identifier }); setIsAddDialogOpen(false); setIdentifier(''); - } catch (e) { - console.error("[Dashboard] Add failed:", e); - } + } catch (e) { console.error("[Dashboard] Add failed:", e); } }; const handleAddFromCommunity = async (commAcc: any) => { @@ -235,14 +226,7 @@ const Dashboard: React.FC = () => { InputProps={{ endAdornment: ( - + ), }} @@ -365,7 +349,7 @@ const AccountRow: React.FC<{ onSwitch: (login: string) => void, onAuth: () => void }> = ({ account, onDelete, onSwitch, onAuth }) => { - const { shareAccountWithUser, getServerUsers, serverConfig } = useAccounts(); + const { shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, getServerUsers, serverConfig } = useAccounts(); const [timeLeft, setTimeLeft] = useState(null); const [isShareOpen, setIsShareOpen] = useState(false); const [targetUserId, setTargetUserId] = useState(''); @@ -376,10 +360,7 @@ const AccountRow: React.FC<{ const isCooldownActive = cooldownDate && !isNaN(cooldownDate.getTime()) && cooldownDate.getTime() > Date.now(); useEffect(() => { - if (!isCooldownActive || !cooldownDate) { - setTimeLeft(null); - return; - } + if (!isCooldownActive || !cooldownDate) { setTimeLeft(null); return; } const targetTime = cooldownDate.getTime(); const timer = setInterval(() => { const diff = targetTime - Date.now(); @@ -392,14 +373,9 @@ const AccountRow: React.FC<{ return () => clearInterval(timer); }, [account?.cooldownExpiresAt, isCooldownActive]); - const avatarSrc = account?.localAvatar - ? `steam-resource://${account.localAvatar}` - : (account?.avatar || ''); + const avatarSrc = account?.localAvatar ? `steam-resource://${account.localAvatar}` : (account?.avatar || ''); const [imgSrc, setImgSrc] = useState(avatarSrc); - - useEffect(() => { - setImgSrc(avatarSrc); - }, [avatarSrc]); + useEffect(() => { setImgSrc(avatarSrc); }, [avatarSrc]); const handleOpenShare = async () => { setIsShareOpen(true); @@ -409,8 +385,7 @@ const AccountRow: React.FC<{ (window as any).electronAPI.getServerUserInfo() ]); const filtered = (Array.isArray(users) ? users : []).filter(u => - u.steamId !== selfInfo.steamId && - u.steamId !== account.steamId + u.steamId !== selfInfo.steamId && u.steamId !== account.steamId ); setServerUsers(filtered); } catch (e) {} @@ -421,14 +396,21 @@ const AccountRow: React.FC<{ setIsSharing(true); try { await shareAccountWithUser(account.steamId, targetUserId); - alert(`Account shared successfully!`); - setIsShareOpen(false); setTargetUserId(''); - } catch (e: any) { - alert(e.message || "Failed to share account"); - } finally { - setIsSharing(false); - } + } catch (e: any) { alert(e.message || "Failed to share account"); + } finally { setIsSharing(false); } + }; + + const handleRevoke = async (targetSteamId: string) => { + if (!window.confirm("Revoke access for this user?")) return; + try { await revokeAccountAccess(account.steamId, targetSteamId); + } catch (e: any) { alert(e.message); } + }; + + const handleRevokeAll = async () => { + if (!window.confirm("Completely stop sharing this account?")) return; + try { await revokeAllAccountAccess(account.steamId); setIsShareOpen(false); + } catch (e: any) { alert(e.message); } }; const isBanned = account?.vacBanned || (account?.gameBans && account.gameBans > 0); @@ -447,44 +429,34 @@ const AccountRow: React.FC<{ - - {account?.personaName || 'Unknown'} - + {account?.personaName || 'Unknown'} {account?.steamId} {isBanned ? ( - - ACCOUNT BANNED + BANNED - {account?.vacBanned && ( - - )} - {account?.gameBans ? account.gameBans > 0 && ( - - ) : null} + {account?.vacBanned && } + {account?.gameBans ? account.gameBans > 0 && : null} ) : ( - - SECURE + SECURE )} {account?.authError ? ( - - Needs Re-auth + Needs Re-auth ) : isCooldownActive ? ( - - {timeLeft} + {timeLeft} ) : ( Available @@ -492,95 +464,79 @@ const AccountRow: React.FC<{ - {/* Fast Switcher Button - Always available if we have a login name */} {account.loginName && ( + variant="contained" size="small" onClick={() => onSwitch(account.loginName || '')} + sx={{ height: 28, fontSize: '0.7rem', bgcolor: 'secondary.main', '&:hover': { opacity: 0.9 }, minWidth: 60 }} + >LOGIN )} - - {/* Scraper Auth Button - Controls the optional cooldown tracking */} - + {account.steamLoginSecure && !account.authError ? : (account.authError ? : )} {account.steamLoginSecure && !account.authError && ( - - TRACKING - + TRACKING )} - - (window as any).electronAPI.openExternal(account?.profileUrl || '')}> onDelete(account?._id || '')}> - {/* Share Dialog */} setIsShareOpen(false)} maxWidth="xs" fullWidth> - Share Account + Permissions - - Select a community member to share this account with. - - - Select User - - + GRANT ACCESS + + + Select User + + + + + + CURRENT ACCESS + + {(account as any).sharedWith?.map((sw: any) => ( + + + + + handleRevoke(sw.steamId)}> + + + ))} + {(!(account as any).sharedWith || (account as any).sharedWith.length === 0) && ( + Not shared with anyone yet. + )} + + {(account as any).sharedWith?.length > 0 && ( + + )} - - - - + diff --git a/frontend/src/pages/DashboardRow.tsx b/frontend/src/pages/DashboardRow.tsx new file mode 100644 index 0000000..0301fc5 --- /dev/null +++ b/frontend/src/pages/DashboardRow.tsx @@ -0,0 +1,234 @@ +const AccountRow: React.FC<{ + account: Account, + onDelete: (id: string) => void, + onSwitch: (login: string) => void, + onAuth: () => void +}> = ({ account, onDelete, onSwitch, onAuth }) => { + const { shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, getServerUsers, serverConfig } = useAccounts(); + const [timeLeft, setTimeLeft] = useState(null); + const [isShareOpen, setIsShareOpen] = useState(false); + const [targetUserId, setTargetUserId] = useState(''); + const [isSharing, setIsSharing] = useState(false); + const [serverUsers, setServerUsers] = useState([]); + + const cooldownDate = account?.cooldownExpiresAt ? new Date(account.cooldownExpiresAt) : null; + const isCooldownActive = cooldownDate && !isNaN(cooldownDate.getTime()) && cooldownDate.getTime() > Date.now(); + + useEffect(() => { + if (!isCooldownActive || !cooldownDate) { + setTimeLeft(null); + return; + } + const targetTime = cooldownDate.getTime(); + const timer = setInterval(() => { + const diff = targetTime - Date.now(); + if (diff <= 0) { setTimeLeft(null); clearInterval(timer); return; } + const hours = Math.floor(diff / 3600000); + const mins = Math.floor((diff % 3600000) / 60000); + const secs = Math.floor((diff % 60000) / 1000); + setTimeLeft(`${hours}h ${mins}m ${secs}s`); + }, 1000); + return () => clearInterval(timer); + }, [account?.cooldownExpiresAt, isCooldownActive]); + + const avatarSrc = account?.localAvatar + ? `steam-resource://${account.localAvatar}` + : (account?.avatar || ''); + const [imgSrc, setImgSrc] = useState(avatarSrc); + + useEffect(() => { + setImgSrc(avatarSrc); + }, [avatarSrc]); + + const handleOpenShare = async () => { + setIsShareOpen(true); + try { + const [users, selfInfo] = await Promise.all([ + getServerUsers(), + (window as any).electronAPI.getServerUserInfo() + ]); + const filtered = (Array.isArray(users) ? users : []).filter(u => + u.steamId !== selfInfo.steamId && + u.steamId !== account.steamId + ); + setServerUsers(filtered); + } catch (e) {} + }; + + const handleShare = async () => { + if (!targetUserId) return; + setIsSharing(true); + try { + await shareAccountWithUser(account.steamId, targetUserId); + setTargetUserId(''); + } catch (e: any) { + alert(e.message || "Failed to share account"); + } finally { + setIsSharing(false); + } + }; + + const handleRevoke = async (targetSteamId: string) => { + if (!window.confirm("Revoke access for this user?")) return; + try { + await revokeAccountAccess(account.steamId, targetSteamId); + } catch (e: any) { alert(e.message); } + }; + + const handleRevokeAll = async () => { + if (!window.confirm("Completely stop sharing this account with the community?")) return; + try { + await revokeAllAccountAccess(account.steamId); + setIsShareOpen(false); + } catch (e: any) { alert(e.message); } + }; + + const isBanned = account?.vacBanned || (account?.gameBans && account.gameBans > 0); + const isShared = account?._id.startsWith('shared_'); + + return ( + + + + + {isShared && ( + + + + )} + + + + + {account?.personaName || 'Unknown'} + + {account?.steamId} + + + {isBanned ? ( + + + + ACCOUNT BANNED + + + {account?.vacBanned && ( + + )} + {account?.gameBans ? account.gameBans > 0 && ( + + ) : null} + + + ) : ( + + + SECURE + + )} + + + {account?.authError ? ( + + + Needs Re-auth + + ) : isCooldownActive ? ( + + + {timeLeft} + + ) : ( + Available + )} + + + + {account.loginName && ( + + )} + + + + + {account.steamLoginSecure && !account.authError ? : (account.authError ? : )} + + {account.steamLoginSecure && !account.authError && ( + + TRACKING + + )} + + + + + + + (window as any).electronAPI.openExternal(account?.profileUrl || '')}> + onDelete(account?._id || '')}> + + + setIsShareOpen(false)} maxWidth="xs" fullWidth> + Account Permissions + + GRANT ACCESS + + + Select User + + + + + + CURRENT ACCESS + + {(account as any).sharedWith?.map((sw: any) => ( + + + + + handleRevoke(sw.steamId)}> + + + ))} + {(!(account as any).sharedWith || (account as any).sharedWith.length === 0) && ( + Not shared with anyone yet. + )} + + {(account as any).sharedWith?.length > 0 && ( + + )} + + + + + + ); +}; -- 2.49.1 From f0740997d0817d995a2a9e36361ae57279c1f4d2 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 02:56:45 +0100 Subject: [PATCH 2/8] fix: refine community icon logic to only show when an account is actively shared with others --- frontend/src/pages/Dashboard.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 293fa51..c71ab04 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -414,15 +414,21 @@ const AccountRow: React.FC<{ }; const isBanned = account?.vacBanned || (account?.gameBans && account.gameBans > 0); - const isShared = account?._id.startsWith('shared_'); + + // Refined Shared Logic: + // 1. It was shared WITH you (starts with shared_) + // 2. OR you are the owner but you have shared it with at least one person + const isSharedWithYou = account?._id.startsWith('shared_'); + const hasSharedMembers = (account as any).sharedWith && (account as any).sharedWith.length > 0; + const showCommunityIcon = isSharedWithYou || hasSharedMembers; return ( - {isShared && ( - + {showCommunityIcon && ( + )} -- 2.49.1 From 6f66f33a9be7e8f7a3de208b815eb515105fa22f Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 03:01:33 +0100 Subject: [PATCH 3/8] feat: implement primary account identifier and streamline add account flow via direct Steam login --- frontend/dist-electron/main.js | 12 ++ frontend/dist-electron/preload.js | 2 + frontend/dist-electron/services/backend.js | 32 +++- frontend/src/pages/Dashboard.tsx | 195 ++------------------- 4 files changed, 56 insertions(+), 185 deletions(-) diff --git a/frontend/dist-electron/main.js b/frontend/dist-electron/main.js index 3b8a1e4..939ee20 100644 --- a/frontend/dist-electron/main.js +++ b/frontend/dist-electron/main.js @@ -502,6 +502,18 @@ electron_1.ipcMain.handle('share-account-with-user', async (event, steamId, targ } throw new Error('Backend not configured'); }); +electron_1.ipcMain.handle('revoke-account-access', async (event, steamId, targetSteamId) => { + initBackend(); + if (backend) + return await backend.revokeAccess(steamId, targetSteamId); + throw new Error('Backend not configured'); +}); +electron_1.ipcMain.handle('revoke-all-account-access', async (event, steamId) => { + initBackend(); + if (backend) + return await backend.revokeAllAccess(steamId); + throw new Error('Backend not configured'); +}); electron_1.ipcMain.handle('get-community-accounts', async () => { initBackend(); return backend ? await backend.getCommunityAccounts() : []; }); electron_1.ipcMain.handle('get-server-users', async () => { initBackend(); return backend ? await backend.getServerUsers() : []; }); electron_1.ipcMain.handle('switch-account', async (event, loginName) => await handleSwitchAccount(loginName)); diff --git a/frontend/dist-electron/preload.js b/frontend/dist-electron/preload.js index 7be0dfb..56a17c2 100644 --- a/frontend/dist-electron/preload.js +++ b/frontend/dist-electron/preload.js @@ -8,6 +8,8 @@ electron_1.contextBridge.exposeInMainWorld('electronAPI', { deleteAccount: (id) => electron_1.ipcRenderer.invoke('delete-account', id), switchAccount: (loginName) => electron_1.ipcRenderer.invoke('switch-account', loginName), shareAccountWithUser: (steamId, targetSteamId) => electron_1.ipcRenderer.invoke('share-account-with-user', 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), openExternal: (url) => electron_1.ipcRenderer.invoke('open-external', url), openSteamLogin: (steamId) => electron_1.ipcRenderer.invoke('open-steam-login', steamId), // Server Config & Auth diff --git a/frontend/dist-electron/services/backend.js b/frontend/dist-electron/services/backend.js index d0d7341..d88977c 100644 --- a/frontend/dist-electron/services/backend.js +++ b/frontend/dist-electron/services/backend.js @@ -67,7 +67,8 @@ class BackendService { gameBans: account.gameBans, loginName: account.loginName, steamLoginSecure: account.steamLoginSecure, - loginConfig: account.loginConfig + loginConfig: account.loginConfig, + sessionUpdatedAt: account.sessionUpdatedAt }, { headers: this.headers }); } catch (e) { @@ -100,5 +101,34 @@ class BackendService { throw new Error(e.response?.data?.message || 'Failed to share account'); } } + async revokeAccess(steamId, targetSteamId) { + if (!this.token) + return; + try { + const response = await axios_1.default.delete(`${this.url}/api/sync/${steamId}/share`, { + headers: this.headers, + data: { targetSteamId } + }); + return response.data; + } + catch (e) { + console.error(`[Backend] Failed to revoke access for ${steamId} from ${targetSteamId}`); + throw new Error(e.response?.data?.message || 'Failed to revoke access'); + } + } + async revokeAllAccess(steamId) { + if (!this.token) + return; + try { + const response = await axios_1.default.delete(`${this.url}/api/sync/${steamId}/share/all`, { + headers: this.headers + }); + return response.data; + } + catch (e) { + console.error(`[Backend] Failed to revoke all access for ${steamId}`); + throw new Error(e.response?.data?.message || 'Failed to revoke all access'); + } + } } exports.BackendService = BackendService; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index c71ab04..ddb0b96 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -25,6 +25,7 @@ import ShieldIcon from '@mui/icons-material/Shield'; import GppBadIcon from '@mui/icons-material/GppBad'; import PeopleIcon from '@mui/icons-material/People'; import VerifiedUserIcon from '@mui/icons-material/VerifiedUser'; +import WorkspacePremiumIcon from '@mui/icons-material/WorkspacePremium'; import { useAccounts, type Account } from '../hooks/useAccounts'; import { useAppTheme } from '../theme/ThemeContext'; import type { ThemeType } from '../theme/SteamTheme'; @@ -39,47 +40,13 @@ const Dashboard: React.FC = () => { } = useAccounts(); const [searchTerm, setSearchTerm] = useState(''); - const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); - const [identifier, setIdentifier] = useState(''); - - const [addTab, setAddTab] = useState(0); - const [communityAccounts, setCommunityAccounts] = useState([]); - const [isCommunityLoading, setIsCommunityLoading] = useState(false); const [serverUrl, setServerUrl] = useState(''); useEffect(() => { if (serverConfig?.url) setServerUrl(serverConfig.url); }, [serverConfig?.url]); - const loadCommunity = async () => { - setIsCommunityLoading(true); - try { - const data = await getCommunityAccounts(); - setCommunityAccounts(Array.isArray(data) ? data : []); - } catch (e) { } finally { setIsCommunityLoading(false); } - }; - - useEffect(() => { - if (isAddDialogOpen && addTab === 1) loadCommunity(); - }, [isAddDialogOpen, addTab]); - - const handleAddAccount = async () => { - if (!identifier) return; - try { - await addAccount({ identifier }); - setIsAddDialogOpen(false); - setIdentifier(''); - } catch (e) { console.error("[Dashboard] Add failed:", e); } - }; - - const handleAddFromCommunity = async (commAcc: any) => { - try { - await addAccount({ identifier: commAcc.steamId }); - setIsAddDialogOpen(false); - } catch (e) { } - }; - const saveSettings = async () => { await updateServerConfig({ url: serverUrl }); alert("Server URL updated!"); @@ -139,7 +106,7 @@ const Dashboard: React.FC = () => { variant="contained" color="primary" startIcon={} - onClick={() => setIsAddDialogOpen(true)} + onClick={() => openSteamLogin('')} sx={{ height: 32 }} > Add @@ -185,161 +152,13 @@ const Dashboard: React.FC = () => { {!isLoading && filteredAccounts.length === 0 && ( - No accounts tracked. Click "Add Account" to get started! + No accounts tracked. Click "Add" to get started! )} {/* Settings Dialog */} - setIsSettingsOpen(false)} maxWidth="sm" fullWidth> - Settings & Customization - - THEME SELECTION - - Active Theme - - - - - - BACKEND CONFIGURATION - setServerUrl(e.target.value)} - placeholder="https://ultimate-ban-tracker.narl.io" - margin="dense" - sx={{ mb: 2 }} - InputProps={{ - endAdornment: ( - - - - ), - }} - /> - - - - COMMUNITY AUTHENTICATION - - - - {serverConfig?.token ? "Connected to Server" : "Not Authenticated"} - - - {serverConfig?.token ? "Your accounts can now be shared with others." : "Login to share and sync with your community."} - - - - {serverConfig?.token && ( - - )} - - - - - updateServerConfig({ enabled: e.target.checked })} - disabled={!serverConfig?.token} - /> - } - label="Enable Community Sync" - sx={{ mt: 2 }} - /> - - - - - - - {/* Add Account Dialog */} - setIsAddDialogOpen(false)} maxWidth="sm" fullWidth> - - setAddTab(v)} variant="fullWidth" textColor="inherit" indicatorColor="primary"> - } iconPosition="start" /> - } iconPosition="start" disabled={!serverConfig?.token} /> - - - - {addTab === 0 ? ( - <> - - Enter a SteamID64 or Profile URL. You will need to authenticate to enable full tracking and instant login features. - - setIdentifier(e.target.value)} - sx={{ '& .MuiOutlinedInput-root': { backgroundColor: 'rgba(0, 0, 0, 0.1)' } }} - /> - - ) : ( - - {isCommunityLoading ? ( - - ) : ( - - {communityAccounts - .filter(ca => !safeAccounts.find(a => a.steamId === ca.steamId)) - .map((ca) => ( - - - - - - - - ))} - {communityAccounts.length === 0 && No shared accounts found on server.} - - )} - - )} - - - - {addTab === 0 && } - - - - ); -}; // --- Sub-Component: AccountRow --- @@ -415,6 +234,9 @@ const AccountRow: React.FC<{ const isBanned = account?.vacBanned || (account?.gameBans && account.gameBans > 0); + // Primary account check + const isPrimaryAccount = serverConfig?.serverSteamId === account.steamId; + // Refined Shared Logic: // 1. It was shared WITH you (starts with shared_) // 2. OR you are the owner but you have shared it with at least one person @@ -427,6 +249,11 @@ const AccountRow: React.FC<{ + {isPrimaryAccount && ( + + + + )} {showCommunityIcon && ( -- 2.49.1 From e16a5376212081a9efaf2e268c1c96d41895961f Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 03:02:46 +0100 Subject: [PATCH 4/8] fix: resolve JSX syntax error by correctly closing the Dashboard component --- frontend/src/pages/Dashboard.tsx | 107 ++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index ddb0b96..65726a9 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -6,7 +6,7 @@ import { DialogActions, CircularProgress, Paper, Chip, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Switch, FormControlLabel, Divider, List, ListItem, ListItemText, ListItemSecondaryAction, - Tabs, Tab, Select, MenuItem, FormControl, InputLabel + Select, MenuItem, FormControl, InputLabel } from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; import AddIcon from '@mui/icons-material/Add'; @@ -20,7 +20,6 @@ import LockResetIcon from '@mui/icons-material/LockReset'; import SettingsIcon from '@mui/icons-material/Settings'; import ShareIcon from '@mui/icons-material/Share'; import GroupAddIcon from '@mui/icons-material/GroupAdd'; -import PublicIcon from '@mui/icons-material/Public'; import ShieldIcon from '@mui/icons-material/Shield'; import GppBadIcon from '@mui/icons-material/GppBad'; import PeopleIcon from '@mui/icons-material/People'; @@ -34,9 +33,8 @@ import NebulaBanner from '../components/NebulaBanner'; const Dashboard: React.FC = () => { const { currentTheme, setTheme } = useAppTheme(); const { - accounts, isLoading, isSyncing, serverConfig, addAccount, deleteAccount, - switchAccount, openSteamLogin, updateServerConfig, loginToServer, - getCommunityAccounts, syncNow + accounts, isLoading, isSyncing, serverConfig, deleteAccount, + switchAccount, openSteamLogin, updateServerConfig, loginToServer, syncNow } = useAccounts(); const [searchTerm, setSearchTerm] = useState(''); @@ -159,6 +157,98 @@ const Dashboard: React.FC = () => { {/* Settings Dialog */} + setIsSettingsOpen(false)} maxWidth="sm" fullWidth> + Settings & Customization + + THEME SELECTION + + Active Theme + + + + + + BACKEND CONFIGURATION + setServerUrl(e.target.value)} + placeholder="https://ultimate-ban-tracker.narl.io" + margin="dense" + sx={{ mb: 2 }} + InputProps={{ + endAdornment: ( + + + + ), + }} + /> + + + + COMMUNITY AUTHENTICATION + + + + {serverConfig?.token ? "Connected to Server" : "Not Authenticated"} + + + {serverConfig?.token ? "Your accounts can now be shared with others." : "Login to share and sync with your community."} + + + + {serverConfig?.token && ( + + )} + + + + + updateServerConfig({ enabled: e.target.checked })} + disabled={!serverConfig?.token} + /> + } + label="Enable Community Sync" + sx={{ mt: 2 }} + /> + + + + + + + ); +}; // --- Sub-Component: AccountRow --- @@ -204,7 +294,8 @@ const AccountRow: React.FC<{ (window as any).electronAPI.getServerUserInfo() ]); const filtered = (Array.isArray(users) ? users : []).filter(u => - u.steamId !== selfInfo.steamId && u.steamId !== account.steamId + u.steamId !== selfInfo.steamId && + u.steamId !== account.steamId ); setServerUsers(filtered); } catch (e) {} @@ -237,9 +328,7 @@ const AccountRow: React.FC<{ // Primary account check const isPrimaryAccount = serverConfig?.serverSteamId === account.steamId; - // Refined Shared Logic: - // 1. It was shared WITH you (starts with shared_) - // 2. OR you are the owner but you have shared it with at least one person + // Refined Shared Logic const isSharedWithYou = account?._id.startsWith('shared_'); const hasSharedMembers = (account as any).sharedWith && (account as any).sharedWith.length > 0; const showCommunityIcon = isSharedWithYou || hasSharedMembers; -- 2.49.1 From d68f0a27404fc47dcbcc54cef9c8e965fb01d799 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 03:04:56 +0100 Subject: [PATCH 5/8] feat: trigger actual Steam desktop login window via protocol handler for native account addition --- frontend/dist-electron/main.js | 8 ++++++++ frontend/dist-electron/preload.js | 1 + frontend/electron/main.ts | 9 +++++++++ frontend/electron/preload.ts | 1 + frontend/src/hooks/useAccounts.tsx | 7 ++++++- frontend/src/pages/Dashboard.tsx | 4 ++-- 6 files changed, 27 insertions(+), 3 deletions(-) diff --git a/frontend/dist-electron/main.js b/frontend/dist-electron/main.js index 939ee20..d62915f 100644 --- a/frontend/dist-electron/main.js +++ b/frontend/dist-electron/main.js @@ -518,6 +518,14 @@ electron_1.ipcMain.handle('get-community-accounts', async () => { initBackend(); electron_1.ipcMain.handle('get-server-users', async () => { initBackend(); return backend ? await backend.getServerUsers() : []; }); electron_1.ipcMain.handle('switch-account', async (event, loginName) => await handleSwitchAccount(loginName)); electron_1.ipcMain.handle('open-external', (event, url) => electron_1.shell.openExternal(url)); +electron_1.ipcMain.handle('open-steam-app-login', async () => { + console.log('[SteamClient] Triggering desktop login window...'); + // Force Steam to show login window. + // steam://open/login is the protocol for this. + const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login'; + (0, child_process_1.exec)(command); + return true; +}); electron_1.ipcMain.handle('open-steam-login', async (event, expectedSteamId) => { const loginSession = electron_1.session.fromPartition('persist:steam-login'); // Removed: automatic clearStorageData to allow cookie persistence diff --git a/frontend/dist-electron/preload.js b/frontend/dist-electron/preload.js index 56a17c2..f6e21ce 100644 --- a/frontend/dist-electron/preload.js +++ b/frontend/dist-electron/preload.js @@ -11,6 +11,7 @@ electron_1.contextBridge.exposeInMainWorld('electronAPI', { revokeAccountAccess: (steamId, targetSteamId) => electron_1.ipcRenderer.invoke('revoke-account-access', steamId, targetSteamId), revokeAllAccountAccess: (steamId) => electron_1.ipcRenderer.invoke('revoke-all-account-access', steamId), openExternal: (url) => electron_1.ipcRenderer.invoke('open-external', url), + openSteamAppLogin: () => electron_1.ipcRenderer.invoke('open-steam-app-login'), openSteamLogin: (steamId) => electron_1.ipcRenderer.invoke('open-steam-login', steamId), // Server Config & Auth getServerConfig: () => electron_1.ipcRenderer.invoke('get-server-config'), diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index 481850d..215e173 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -505,6 +505,15 @@ ipcMain.handle('get-server-users', async () => { initBackend(); return backend ? ipcMain.handle('switch-account', async (event, loginName: string) => await handleSwitchAccount(loginName)); ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url)); +ipcMain.handle('open-steam-app-login', async () => { + console.log('[SteamClient] Triggering desktop login window...'); + // Force Steam to show login window. + // steam://open/login is the protocol for this. + const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login'; + exec(command); + return true; +}); + ipcMain.handle('open-steam-login', async (event, expectedSteamId: string) => { const loginSession = session.fromPartition('persist:steam-login'); // Removed: automatic clearStorageData to allow cookie persistence diff --git a/frontend/electron/preload.ts b/frontend/electron/preload.ts index 510657b..bb959b0 100644 --- a/frontend/electron/preload.ts +++ b/frontend/electron/preload.ts @@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld('electronAPI', { revokeAccountAccess: (steamId: string, targetSteamId: string) => ipcRenderer.invoke('revoke-account-access', steamId, targetSteamId), revokeAllAccountAccess: (steamId: string) => ipcRenderer.invoke('revoke-all-account-access', steamId), openExternal: (url: string) => ipcRenderer.invoke('open-external', url), + openSteamAppLogin: () => ipcRenderer.invoke('open-steam-app-login'), openSteamLogin: (steamId: string) => ipcRenderer.invoke('open-steam-login', steamId), // Server Config & Auth diff --git a/frontend/src/hooks/useAccounts.tsx b/frontend/src/hooks/useAccounts.tsx index bf27a08..c5ca1c7 100644 --- a/frontend/src/hooks/useAccounts.tsx +++ b/frontend/src/hooks/useAccounts.tsx @@ -38,6 +38,7 @@ interface AccountsContextType { updateAccount: (id: string, data: Partial) => Promise; deleteAccount: (id: string) => Promise; switchAccount: (loginName: string) => Promise; + openSteamAppLogin: () => Promise; openSteamLogin: (steamId: string) => Promise; shareAccountWithUser: (steamId: string, targetSteamId: string) => Promise; revokeAccountAccess: (steamId: string, targetSteamId: string) => Promise; @@ -127,6 +128,10 @@ export const AccountsProvider: React.FC<{ children: React.ReactNode }> = ({ chil await (window as any).electronAPI.switchAccount(loginName); }; + const openSteamAppLogin = async () => { + await (window as any).electronAPI.openSteamAppLogin(); + }; + const openSteamLogin = async (steamId: string) => { await (window as any).electronAPI.openSteamLogin(steamId); await syncNow(); @@ -172,7 +177,7 @@ export const AccountsProvider: React.FC<{ children: React.ReactNode }> = ({ chil return ( {children} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 65726a9..673cf3c 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -34,7 +34,7 @@ const Dashboard: React.FC = () => { const { currentTheme, setTheme } = useAppTheme(); const { accounts, isLoading, isSyncing, serverConfig, deleteAccount, - switchAccount, openSteamLogin, updateServerConfig, loginToServer, syncNow + switchAccount, openSteamAppLogin, openSteamLogin, updateServerConfig, loginToServer, syncNow } = useAccounts(); const [searchTerm, setSearchTerm] = useState(''); @@ -104,7 +104,7 @@ const Dashboard: React.FC = () => { variant="contained" color="primary" startIcon={} - onClick={() => openSteamLogin('')} + onClick={() => openSteamAppLogin()} sx={{ height: 32 }} > Add -- 2.49.1 From 2719bd527a000b4cb27a1c97e5ef5389d367a20d Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 03:11:56 +0100 Subject: [PATCH 6/8] fix: ensure fresh Steam login on Add by killing process and clearing auto-login state --- frontend/electron/main.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index 215e173..ec1df1c 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -506,9 +506,18 @@ ipcMain.handle('switch-account', async (event, loginName: string) => await handl ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url)); ipcMain.handle('open-steam-app-login', async () => { - console.log('[SteamClient] Triggering desktop login window...'); - // Force Steam to show login window. - // steam://open/login is the protocol for this. + console.log('[SteamClient] Preparing for fresh login...'); + await killSteam(); + + if (process.platform === 'win32') { + // Clear auto-login registry + const clearReg = 'reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "" /f'; + await new Promise((res) => exec(clearReg, () => res())); + } else if (process.platform === 'linux') { + // On Linux we can use the steamClient helper to set an empty user + await steamClient.setAutoLoginUser("", undefined, ""); + } + const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login'; exec(command); return true; -- 2.49.1 From 75accbe5b68f3469ef995b0b0e3fbf6da9df75a3 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 03:17:38 +0100 Subject: [PATCH 7/8] fix: implement per-account session isolation and cookie injection for robust authentication --- frontend/electron/main.ts | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index ec1df1c..fed6c7f 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -524,13 +524,40 @@ ipcMain.handle('open-steam-app-login', async () => { }); ipcMain.handle('open-steam-login', async (event, expectedSteamId: string) => { - const loginSession = session.fromPartition('persist:steam-login'); - // Removed: automatic clearStorageData to allow cookie persistence + // Use a unique partition per account to prevent session bleeding + const partitionId = expectedSteamId ? `persist:steam-login-${expectedSteamId}` : 'persist:steam-login-new'; + const loginSession = session.fromPartition(partitionId); + + // If we have an existing cookie string for this account, pre-inject it + if (expectedSteamId) { + const accounts = store.get('accounts') as Account[]; + const account = accounts.find(a => a.steamId === expectedSteamId); + if (account?.steamLoginSecure) { + console.log(`[Auth] Pre-injecting existing cookies for ${account.personaName}...`); + const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim()); + for (const pair of cookiePairs) { + const [name, value] = pair.split('='); + if (name && value) { + try { + await loginSession.cookies.set({ + url: 'https://steamcommunity.com', + domain: 'steamcommunity.com', + name: name, + value: value, + path: '/', + secure: true, + httpOnly: name.includes('Secure') + }); + } catch (e) {} + } + } + } + } return new Promise((resolve) => { const loginWindow = new BrowserWindow({ width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Steam', - webPreferences: { nodeIntegration: false, contextIsolation: true, partition: 'persist:steam-login' } + webPreferences: { nodeIntegration: false, contextIsolation: true, partition: partitionId } }); loginWindow.loadURL('https://steamcommunity.com/login/home/?goto=my/gcpd/730'); const checkCookie = setInterval(async () => { -- 2.49.1 From 9d5f77dc0958ec0e737ac336b56d95ff7961e036 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 03:19:00 +0100 Subject: [PATCH 8/8] fix: explicitly clear storage for new account logins to prevent session leakage --- frontend/electron/main.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index fed6c7f..d343d1d 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -528,6 +528,12 @@ ipcMain.handle('open-steam-login', async (event, expectedSteamId: string) => { const partitionId = expectedSteamId ? `persist:steam-login-${expectedSteamId}` : 'persist:steam-login-new'; const loginSession = session.fromPartition(partitionId); + // If adding a brand new account, explicitly clear previous trash + if (!expectedSteamId) { + console.log('[Auth] Clearing session for new account login...'); + await loginSession.clearStorageData({ storages: ['cookies', 'localstorage', 'indexdb'] }); + } + // If we have an existing cookie string for this account, pre-inject it if (expectedSteamId) { const accounts = store.get('accounts') as Account[]; -- 2.49.1