feat: implement comprehensive admin dashboard for server management and user oversight
This commit is contained in:
@@ -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<any[]>;
|
||||
getServerUsers: () => Promise<any[]>;
|
||||
refreshAccounts: (showLoading?: boolean) => Promise<void>;
|
||||
|
||||
// Admin Methods
|
||||
adminGetStats: () => Promise<any>;
|
||||
adminGetUsers: () => Promise<any[]>;
|
||||
adminDeleteUser: (userId: string) => Promise<void>;
|
||||
adminGetAccounts: () => Promise<any[]>;
|
||||
adminRemoveAccount: (steamId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const AccountsContext = createContext<AccountsContextType | undefined>(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 (
|
||||
<AccountsContext.Provider value={{
|
||||
accounts, serverConfig, isLoading, isSyncing, addAccount, updateAccount, deleteAccount,
|
||||
switchAccount, openSteamAppLogin, openSteamLogin, updateServerConfig, loginToServer,
|
||||
getCommunityAccounts, getServerUsers, shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, syncNow, refreshAccounts
|
||||
getCommunityAccounts, getServerUsers, shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, syncNow, refreshAccounts,
|
||||
adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount
|
||||
}}>
|
||||
{children}
|
||||
</AccountsContext.Provider>
|
||||
|
||||
@@ -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<any>(null);
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [accounts, setAccounts] = useState<any[]>([]);
|
||||
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 (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle sx={{ bgcolor: 'background.paper', color: 'text.primary', display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<AdminPanelSettingsIcon color="primary" /> Server Administration
|
||||
</DialogTitle>
|
||||
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ bgcolor: 'background.paper', borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tab icon={<StorageIcon />} label="Overview" />
|
||||
<Tab icon={<GroupIcon />} label="Users" />
|
||||
<Tab icon={<AccountTreeIcon />} label="Global Accounts" />
|
||||
</Tabs>
|
||||
<DialogContent sx={{ bgcolor: 'background.paper', minHeight: 400, pt: 2 }}>
|
||||
{loading ? <Box sx={{ display: 'flex', justifyContent: 'center', mt: 10 }}><CircularProgress /></Box> : (
|
||||
<>
|
||||
{tab === 0 && stats && (
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 2, mt: 2 }}>
|
||||
{[
|
||||
{ label: 'Total Users', value: stats.users },
|
||||
{ label: 'Total Accounts', value: stats.accounts },
|
||||
{ label: 'Active Cooldowns', value: stats.activeCooldowns }
|
||||
].map((s) => (
|
||||
<Paper key={s.label} sx={{ p: 3, textAlign: 'center', bgcolor: 'rgba(0,0,0,0.1)' }}>
|
||||
<Typography variant="h4" color="primary" sx={{ fontWeight: 'bold' }}>{s.value}</Typography>
|
||||
<Typography variant="caption" color="textSecondary">{s.label}</Typography>
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
{tab === 1 && (
|
||||
<List>
|
||||
{users.map(u => (
|
||||
<ListItem key={u._id} divider sx={{ borderColor: 'divider' }}>
|
||||
<Avatar src={u.avatar} sx={{ mr: 2 }} />
|
||||
<ListItemText primary={u.personaName} secondary={u.steamId} primaryTypographyProps={{ color: 'text.primary' }} />
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton color="error" onClick={() => handleDeleteUser(u._id)}><DeleteIcon /></IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
{tab === 2 && (
|
||||
<List>
|
||||
{accounts.map(a => (
|
||||
<ListItem key={a.steamId} divider sx={{ borderColor: 'divider' }}>
|
||||
<Avatar src={a.avatar} variant="square" sx={{ mr: 2 }} />
|
||||
<ListItemText
|
||||
primary={a.personaName}
|
||||
secondary={`Owned by: ${a.addedBy?.personaName || 'Unknown'} (${a.steamId})`}
|
||||
primaryTypographyProps={{ color: 'text.primary' }}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton color="error" onClick={() => handleForceRemove(a.steamId)}><DeleteIcon /></IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ bgcolor: 'background.paper', p: 2 }}>
|
||||
<Button onClick={onClose} variant="contained" color="inherit">Close Panel</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, WebkitAppRegion: 'no-drag' } as any}>
|
||||
{/* Admin Button - Only visible if isAdmin is true */}
|
||||
{serverConfig?.isAdmin && (
|
||||
<Tooltip title="Open Admin Panel">
|
||||
<IconButton color="primary" onClick={() => setIsAdminPanelOpen(true)}>
|
||||
<AdminPanelSettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mr: 1 }}>
|
||||
{isSyncing ? (
|
||||
<CircularProgress size={16} sx={{ color: 'primary.main', mr: 1 }} />
|
||||
@@ -246,6 +361,9 @@ const Dashboard: React.FC = () => {
|
||||
<Button onClick={() => setIsSettingsOpen(false)} color="inherit" variant="contained">Done</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Admin Panel */}
|
||||
<AdminPanel open={isAdminPanelOpen} onClose={() => setIsAdminPanelOpen(false)} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -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) {}
|
||||
|
||||
Reference in New Issue
Block a user