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) {}