diff --git a/frontend/dist-electron/main.js b/frontend/dist-electron/main.js index 3b8a1e4..d62915f 100644 --- a/frontend/dist-electron/main.js +++ b/frontend/dist-electron/main.js @@ -502,10 +502,30 @@ 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)); 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 7be0dfb..f6e21ce 100644 --- a/frontend/dist-electron/preload.js +++ b/frontend/dist-electron/preload.js @@ -8,7 +8,10 @@ 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), + 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/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/electron/main.ts b/frontend/electron/main.ts index a009e28..d343d1d 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -488,19 +488,82 @@ 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)); ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url)); +ipcMain.handle('open-steam-app-login', async () => { + console.log('[SteamClient] Preparing for fresh login...'); + await killSteam(); + + if (process.platform === 'win32') { + // Clear auto-login registry + const clearReg = 'reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "" /f'; + await new Promise((res) => exec(clearReg, () => res())); + } else if (process.platform === 'linux') { + // On Linux we can use the steamClient helper to set an empty user + await steamClient.setAutoLoginUser("", undefined, ""); + } + + const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login'; + exec(command); + return true; +}); + ipcMain.handle('open-steam-login', async (event, expectedSteamId: string) => { - 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 adding a brand new account, explicitly clear previous trash + if (!expectedSteamId) { + console.log('[Auth] Clearing session for new account login...'); + await loginSession.clearStorageData({ storages: ['cookies', 'localstorage', 'indexdb'] }); + } + + // If we have an existing cookie string for this account, pre-inject it + if (expectedSteamId) { + const accounts = store.get('accounts') as Account[]; + const account = accounts.find(a => a.steamId === expectedSteamId); + if (account?.steamLoginSecure) { + console.log(`[Auth] Pre-injecting existing cookies for ${account.personaName}...`); + const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim()); + for (const pair of cookiePairs) { + const [name, value] = pair.split('='); + if (name && value) { + try { + await loginSession.cookies.set({ + url: 'https://steamcommunity.com', + domain: 'steamcommunity.com', + name: 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 () => { diff --git a/frontend/electron/preload.ts b/frontend/electron/preload.ts index 4be24f8..bb959b0 100644 --- a/frontend/electron/preload.ts +++ b/frontend/electron/preload.ts @@ -7,7 +7,10 @@ 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), + openSteamAppLogin: () => ipcRenderer.invoke('open-steam-app-login'), openSteamLogin: (steamId: string) => ipcRenderer.invoke('open-steam-login', steamId), // Server Config & Auth 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..c5ca1c7 100644 --- a/frontend/src/hooks/useAccounts.tsx +++ b/frontend/src/hooks/useAccounts.tsx @@ -38,8 +38,11 @@ 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; + revokeAllAccountAccess: (steamId: string) => Promise; // Server Methods updateServerConfig: (config: Partial) => Promise; @@ -125,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(); @@ -136,6 +143,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); @@ -158,8 +177,8 @@ 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 5ec57ae..673cf3c 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,11 +20,11 @@ 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'; 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'; @@ -33,62 +33,18 @@ 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, openSteamAppLogin, openSteamLogin, updateServerConfig, loginToServer, syncNow } = 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); - } + 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!"); @@ -148,7 +104,7 @@ const Dashboard: React.FC = () => { variant="contained" color="primary" startIcon={} - onClick={() => setIsAddDialogOpen(true)} + onClick={() => openSteamAppLogin()} sx={{ height: 32 }} > Add @@ -194,7 +150,7 @@ 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! )} @@ -235,14 +191,7 @@ const Dashboard: React.FC = () => { InputProps={{ endAdornment: ( - + ), }} @@ -297,62 +246,6 @@ const Dashboard: React.FC = () => { - - {/* 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 && } - - ); }; @@ -365,7 +258,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 +269,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 +282,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); @@ -421,70 +306,79 @@ 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); - const isShared = account?._id.startsWith('shared_'); + + // Primary account check + const isPrimaryAccount = serverConfig?.serverSteamId === account.steamId; + + // 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; return ( - {isShared && ( - + {isPrimaryAccount && ( + + + + )} + {showCommunityIcon && ( + )} - - {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 +386,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 && ( + + )} + + + + + + ); +};