From 6dc940bb3afc1aca2f91dd6b432da769aa1b4e1d Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 03:28:21 +0100 Subject: [PATCH 1/2] feat: implement comprehensive admin dashboard for server management and user oversight --- frontend/electron/main.ts | 17 +++- frontend/electron/preload.ts | 7 ++ frontend/electron/services/backend.ts | 44 +++++++++ frontend/src/hooks/useAccounts.tsx | 18 +++- frontend/src/pages/Dashboard.tsx | 123 +++++++++++++++++++++++++- 5 files changed, 203 insertions(+), 6 deletions(-) diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index d343d1d..b9b197e 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -401,9 +401,14 @@ ipcMain.handle('login-to-server', async () => { const saveServerAuth = (token: string) => { if (captured) return; captured = true; let serverSteamId = undefined; - try { const payload = JSON.parse(Buffer.from(token.split('.')[1]!, 'base64').toString()); serverSteamId = payload.steamId; } catch (e) {} + let isAdmin = false; + try { + const payload = JSON.parse(Buffer.from(token.split('.')[1]!, 'base64').toString()); + serverSteamId = payload.steamId; + isAdmin = !!payload.isAdmin; + } catch (e) {} const current = store.get('serverConfig'); - store.set('serverConfig', { ...current, token, serverSteamId, enabled: true }); + store.set('serverConfig', { ...current, token, serverSteamId, isAdmin, enabled: true }); initBackend(); authWindow.close(); resolve(true); @@ -502,6 +507,14 @@ ipcMain.handle('revoke-all-account-access', async (event, steamId: string) => { ipcMain.handle('get-community-accounts', async () => { initBackend(); return backend ? await backend.getCommunityAccounts() : []; }); ipcMain.handle('get-server-users', async () => { initBackend(); return backend ? await backend.getServerUsers() : []; }); + +// --- Admin IPC --- +ipcMain.handle('admin-get-stats', async () => { initBackend(); return backend ? await backend.getAdminStats() : null; }); +ipcMain.handle('admin-get-users', async () => { initBackend(); return backend ? await backend.getAdminUsers() : []; }); +ipcMain.handle('admin-delete-user', async (event, userId: string) => { initBackend(); if (backend) await backend.deleteUser(userId); return true; }); +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('switch-account', async (event, loginName: string) => await handleSwitchAccount(loginName)); ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url)); diff --git a/frontend/electron/preload.ts b/frontend/electron/preload.ts index bb959b0..c8c8385 100644 --- a/frontend/electron/preload.ts +++ b/frontend/electron/preload.ts @@ -22,6 +22,13 @@ contextBridge.exposeInMainWorld('electronAPI', { getCommunityAccounts: () => ipcRenderer.invoke('get-community-accounts'), getServerUsers: () => ipcRenderer.invoke('get-server-users'), + // Admin API + adminGetStats: () => ipcRenderer.invoke('admin-get-stats'), + adminGetUsers: () => ipcRenderer.invoke('admin-get-users'), + adminDeleteUser: (userId: string) => ipcRenderer.invoke('admin-delete-user', userId), + adminGetAccounts: () => ipcRenderer.invoke('admin-get-accounts'), + adminRemoveAccount: (steamId: string) => ipcRenderer.invoke('admin-remove-account', steamId), + onAccountsUpdated: (callback: (accounts: any[]) => void) => { const subscription = (_event: IpcRendererEvent, accounts: any[]) => callback(accounts); ipcRenderer.on('accounts-updated', subscription); diff --git a/frontend/electron/services/backend.ts b/frontend/electron/services/backend.ts index 2a12fe2..94786f8 100644 --- a/frontend/electron/services/backend.ts +++ b/frontend/electron/services/backend.ts @@ -119,4 +119,48 @@ export class BackendService { throw new Error(e.response?.data?.message || 'Failed to revoke all access'); } } + + // --- Admin API --- + + public async getAdminStats() { + if (!this.token) return null; + try { + const response = await axios.get(`${this.url}/api/admin/stats`, { headers: this.headers }); + return response.data; + } catch (e) { return null; } + } + + public async getAdminUsers() { + if (!this.token) return []; + try { + const response = await axios.get(`${this.url}/api/admin/users`, { headers: this.headers }); + return response.data; + } catch (e) { return []; } + } + + public async deleteUser(userId: string) { + if (!this.token) return; + try { + await axios.delete(`${this.url}/api/admin/users/${userId}`, { headers: this.headers }); + } catch (e: any) { + throw new Error(e.response?.data?.message || 'Failed to delete user'); + } + } + + public async getAdminAccounts() { + if (!this.token) return []; + try { + const response = await axios.get(`${this.url}/api/admin/accounts`, { headers: this.headers }); + return response.data; + } catch (e) { return []; } + } + + public async forceRemoveAccount(steamId: string) { + if (!this.token) return; + try { + await axios.delete(`${this.url}/api/admin/accounts/${steamId}`, { headers: this.headers }); + } catch (e: any) { + throw new Error(e.response?.data?.message || 'Failed to remove account'); + } + } } diff --git a/frontend/src/hooks/useAccounts.tsx b/frontend/src/hooks/useAccounts.tsx index c5ca1c7..aed28de 100644 --- a/frontend/src/hooks/useAccounts.tsx +++ b/frontend/src/hooks/useAccounts.tsx @@ -27,6 +27,7 @@ export interface ServerConfig { token?: string; serverSteamId?: string; enabled: boolean; + isAdmin?: boolean; } interface AccountsContextType { @@ -51,6 +52,13 @@ interface AccountsContextType { getCommunityAccounts: () => Promise; getServerUsers: () => Promise; refreshAccounts: (showLoading?: boolean) => Promise; + + // Admin Methods + adminGetStats: () => Promise; + adminGetUsers: () => Promise; + adminDeleteUser: (userId: string) => Promise; + adminGetAccounts: () => Promise; + adminRemoveAccount: (steamId: string) => Promise; } const AccountsContext = createContext(undefined); @@ -174,11 +182,19 @@ export const AccountsProvider: React.FC<{ children: React.ReactNode }> = ({ chil return await (window as any).electronAPI.getServerUsers(); }; + // --- Admin Methods --- + const adminGetStats = async () => (window as any).electronAPI.adminGetStats(); + const adminGetUsers = async () => (window as any).electronAPI.adminGetUsers(); + const adminDeleteUser = async (userId: string) => (window as any).electronAPI.adminDeleteUser(userId); + const adminGetAccounts = async () => (window as any).electronAPI.adminGetAccounts(); + const adminRemoveAccount = async (steamId: string) => (window as any).electronAPI.adminRemoveAccount(steamId); + return ( {children} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 673cf3c..16d6dee 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, - Select, MenuItem, FormControl, InputLabel + Select, MenuItem, FormControl, InputLabel, Tabs, Tab } from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; import AddIcon from '@mui/icons-material/Add'; @@ -25,11 +25,116 @@ 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 AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; +import StorageIcon from '@mui/icons-material/Storage'; +import GroupIcon from '@mui/icons-material/Group'; +import AccountTreeIcon from '@mui/icons-material/AccountTree'; import { useAccounts, type Account } from '../hooks/useAccounts'; import { useAppTheme } from '../theme/ThemeContext'; import type { ThemeType } from '../theme/SteamTheme'; import NebulaBanner from '../components/NebulaBanner'; +const AdminPanel: React.FC<{ open: boolean, onClose: () => void }> = ({ open, onClose }) => { + const { adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount } = useAccounts(); + const [tab, setTab] = useState(0); + const [stats, setStats] = useState(null); + const [users, setUsers] = useState([]); + const [accounts, setAccounts] = useState([]); + const [loading, setLoading] = useState(false); + + const loadData = async () => { + setLoading(true); + try { + if (tab === 0) setStats(await adminGetStats()); + if (tab === 1) setUsers(await adminGetUsers()); + if (tab === 2) setAccounts(await adminGetAccounts()); + } catch (e) {} + setLoading(false); + }; + + useEffect(() => { if (open) loadData(); }, [open, tab]); + + const handleDeleteUser = async (id: string) => { + if (window.confirm("Wipe this user and all their accounts?")) { + await adminDeleteUser(id); + loadData(); + } + }; + + const handleForceRemove = async (steamId: string) => { + if (window.confirm("Force remove this account from server?")) { + await adminRemoveAccount(steamId); + loadData(); + } + }; + + return ( + + + Server Administration + + setTab(v)} sx={{ bgcolor: 'background.paper', borderBottom: 1, borderColor: 'divider' }}> + } label="Overview" /> + } label="Users" /> + } label="Global Accounts" /> + + + {loading ? : ( + <> + {tab === 0 && stats && ( + + {[ + { label: 'Total Users', value: stats.users }, + { label: 'Total Accounts', value: stats.accounts }, + { label: 'Active Cooldowns', value: stats.activeCooldowns } + ].map((s) => ( + + {s.value} + {s.label} + + ))} + + )} + {tab === 1 && ( + + {users.map(u => ( + + + + + handleDeleteUser(u._id)}> + + + ))} + + )} + {tab === 2 && ( + + {accounts.map(a => ( + + + + + handleForceRemove(a.steamId)}> + + + ))} + + )} + + )} + + + + + + ); +}; + const Dashboard: React.FC = () => { const { currentTheme, setTheme } = useAppTheme(); const { @@ -39,6 +144,7 @@ const Dashboard: React.FC = () => { const [searchTerm, setSearchTerm] = useState(''); const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [isAdminPanelOpen, setIsAdminPanelOpen] = useState(false); const [serverUrl, setServerUrl] = useState(''); useEffect(() => { @@ -70,6 +176,15 @@ const Dashboard: React.FC = () => { + {/* Admin Button - Only visible if isAdmin is true */} + {serverConfig?.isAdmin && ( + + setIsAdminPanelOpen(true)}> + + + + )} + {isSyncing ? ( @@ -246,6 +361,9 @@ const Dashboard: React.FC = () => { + + {/* Admin Panel */} + setIsAdminPanelOpen(false)} /> ); }; @@ -294,8 +412,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) {} From ee44de182cd2aecf4e89b82991ec8b7799f9b80f Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 03:34:31 +0100 Subject: [PATCH 2/2] fix: implement robust multi-phase synchronization and server-side reconciliation --- frontend/dist-electron/main.js | 18 +++++++- frontend/dist-electron/preload.js | 6 +++ frontend/dist-electron/services/backend.js | 54 ++++++++++++++++++++++ frontend/electron/main.ts | 8 ++++ 4 files changed, 85 insertions(+), 1 deletion(-) diff --git a/frontend/dist-electron/main.js b/frontend/dist-electron/main.js index 3c52098..44ef93c 100644 --- a/frontend/dist-electron/main.js +++ b/frontend/dist-electron/main.js @@ -214,6 +214,12 @@ const syncAccounts = async () => { for (const account of updatedAccounts) { try { const now = new Date(); + // OPTIMIZATION: Ensure ALL authenticated accounts are shared with the server on every sync cycle + // this guarantees that even if a push failed previously, it will be reconciled now. + if (backend && !account._id.startsWith('shared_')) { + console.log(`[Sync] Reconciling account with server: ${account.personaName}`); + await backend.shareAccount(account); + } const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0); if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) { const profile = await (0, steam_web_1.fetchProfileData)(account.steamId, account.steamLoginSecure); @@ -401,13 +407,15 @@ electron_1.ipcMain.handle('login-to-server', async () => { return; captured = true; let serverSteamId = undefined; + let isAdmin = false; try { const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()); serverSteamId = payload.steamId; + isAdmin = !!payload.isAdmin; } catch (e) { } const current = store.get('serverConfig'); - store.set('serverConfig', { ...current, token, serverSteamId, enabled: true }); + store.set('serverConfig', { ...current, token, serverSteamId, isAdmin, enabled: true }); initBackend(); authWindow.close(); resolve(true); @@ -516,6 +524,14 @@ electron_1.ipcMain.handle('revoke-all-account-access', async (event, steamId) => }); 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() : []; }); +// --- Admin IPC --- +electron_1.ipcMain.handle('admin-get-stats', async () => { initBackend(); return backend ? await backend.getAdminStats() : null; }); +electron_1.ipcMain.handle('admin-get-users', async () => { initBackend(); return backend ? await backend.getAdminUsers() : []; }); +electron_1.ipcMain.handle('admin-delete-user', async (event, userId) => { initBackend(); if (backend) + await backend.deleteUser(userId); return true; }); +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) + await backend.forceRemoveAccount(steamId); return true; }); 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 () => { diff --git a/frontend/dist-electron/preload.js b/frontend/dist-electron/preload.js index f6e21ce..854d74d 100644 --- a/frontend/dist-electron/preload.js +++ b/frontend/dist-electron/preload.js @@ -21,6 +21,12 @@ electron_1.contextBridge.exposeInMainWorld('electronAPI', { syncNow: () => electron_1.ipcRenderer.invoke('sync-now'), getCommunityAccounts: () => electron_1.ipcRenderer.invoke('get-community-accounts'), getServerUsers: () => electron_1.ipcRenderer.invoke('get-server-users'), + // Admin API + adminGetStats: () => electron_1.ipcRenderer.invoke('admin-get-stats'), + adminGetUsers: () => electron_1.ipcRenderer.invoke('admin-get-users'), + adminDeleteUser: (userId) => electron_1.ipcRenderer.invoke('admin-delete-user', userId), + adminGetAccounts: () => electron_1.ipcRenderer.invoke('admin-get-accounts'), + adminRemoveAccount: (steamId) => electron_1.ipcRenderer.invoke('admin-remove-account', steamId), onAccountsUpdated: (callback) => { const subscription = (_event, accounts) => callback(accounts); electron_1.ipcRenderer.on('accounts-updated', subscription); diff --git a/frontend/dist-electron/services/backend.js b/frontend/dist-electron/services/backend.js index d88977c..d7e23ba 100644 --- a/frontend/dist-electron/services/backend.js +++ b/frontend/dist-electron/services/backend.js @@ -130,5 +130,59 @@ class BackendService { throw new Error(e.response?.data?.message || 'Failed to revoke all access'); } } + // --- Admin API --- + async getAdminStats() { + if (!this.token) + return null; + try { + const response = await axios_1.default.get(`${this.url}/api/admin/stats`, { headers: this.headers }); + return response.data; + } + catch (e) { + return null; + } + } + async getAdminUsers() { + if (!this.token) + return []; + try { + const response = await axios_1.default.get(`${this.url}/api/admin/users`, { headers: this.headers }); + return response.data; + } + catch (e) { + return []; + } + } + async deleteUser(userId) { + if (!this.token) + return; + try { + await axios_1.default.delete(`${this.url}/api/admin/users/${userId}`, { headers: this.headers }); + } + catch (e) { + throw new Error(e.response?.data?.message || 'Failed to delete user'); + } + } + async getAdminAccounts() { + if (!this.token) + return []; + try { + const response = await axios_1.default.get(`${this.url}/api/admin/accounts`, { headers: this.headers }); + return response.data; + } + catch (e) { + return []; + } + } + async forceRemoveAccount(steamId) { + if (!this.token) + return; + try { + await axios_1.default.delete(`${this.url}/api/admin/accounts/${steamId}`, { headers: this.headers }); + } + catch (e) { + throw new Error(e.response?.data?.message || 'Failed to remove account'); + } + } } exports.BackendService = BackendService; diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index b9b197e..28f25f7 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -242,6 +242,14 @@ const syncAccounts = async () => { for (const account of updatedAccounts) { try { const now = new Date(); + + // OPTIMIZATION: Ensure ALL authenticated accounts are shared with the server on every sync cycle + // this guarantees that even if a push failed previously, it will be reconciled now. + if (backend && !account._id.startsWith('shared_')) { + console.log(`[Sync] Reconciling account with server: ${account.personaName}`); + await backend.shareAccount(account); + } + const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0); if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) { const profile = await fetchProfileData(account.steamId, account.steamLoginSecure);