Compare commits
5 Commits
v1.2.0
...
4037d7bce3
| Author | SHA1 | Date | |
|---|---|---|---|
| 4037d7bce3 | |||
| fc3382c91e | |||
| 5d611fd8be | |||
| ee44de182c | |||
| 6dc940bb3a |
@@ -214,6 +214,12 @@ const syncAccounts = async () => {
|
|||||||
for (const account of updatedAccounts) {
|
for (const account of updatedAccounts) {
|
||||||
try {
|
try {
|
||||||
const now = new Date();
|
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);
|
const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
|
||||||
if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) {
|
if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) {
|
||||||
const profile = await (0, steam_web_1.fetchProfileData)(account.steamId, account.steamLoginSecure);
|
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;
|
return;
|
||||||
captured = true;
|
captured = true;
|
||||||
let serverSteamId = undefined;
|
let serverSteamId = undefined;
|
||||||
|
let isAdmin = false;
|
||||||
try {
|
try {
|
||||||
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
|
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
|
||||||
serverSteamId = payload.steamId;
|
serverSteamId = payload.steamId;
|
||||||
|
isAdmin = !!payload.isAdmin;
|
||||||
}
|
}
|
||||||
catch (e) { }
|
catch (e) { }
|
||||||
const current = store.get('serverConfig');
|
const current = store.get('serverConfig');
|
||||||
store.set('serverConfig', { ...current, token, serverSteamId, enabled: true });
|
store.set('serverConfig', { ...current, token, serverSteamId, isAdmin, enabled: true });
|
||||||
initBackend();
|
initBackend();
|
||||||
authWindow.close();
|
authWindow.close();
|
||||||
resolve(true);
|
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-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('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('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-external', (event, url) => electron_1.shell.openExternal(url));
|
||||||
electron_1.ipcMain.handle('open-steam-app-login', async () => {
|
electron_1.ipcMain.handle('open-steam-app-login', async () => {
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ electron_1.contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
syncNow: () => electron_1.ipcRenderer.invoke('sync-now'),
|
syncNow: () => electron_1.ipcRenderer.invoke('sync-now'),
|
||||||
getCommunityAccounts: () => electron_1.ipcRenderer.invoke('get-community-accounts'),
|
getCommunityAccounts: () => electron_1.ipcRenderer.invoke('get-community-accounts'),
|
||||||
getServerUsers: () => electron_1.ipcRenderer.invoke('get-server-users'),
|
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) => {
|
onAccountsUpdated: (callback) => {
|
||||||
const subscription = (_event, accounts) => callback(accounts);
|
const subscription = (_event, accounts) => callback(accounts);
|
||||||
electron_1.ipcRenderer.on('accounts-updated', subscription);
|
electron_1.ipcRenderer.on('accounts-updated', subscription);
|
||||||
|
|||||||
@@ -130,5 +130,59 @@ class BackendService {
|
|||||||
throw new Error(e.response?.data?.message || 'Failed to revoke all access');
|
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;
|
exports.BackendService = BackendService;
|
||||||
|
|||||||
@@ -242,6 +242,14 @@ const syncAccounts = async () => {
|
|||||||
for (const account of updatedAccounts) {
|
for (const account of updatedAccounts) {
|
||||||
try {
|
try {
|
||||||
const now = new Date();
|
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);
|
const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
|
||||||
if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) {
|
if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) {
|
||||||
const profile = await fetchProfileData(account.steamId, account.steamLoginSecure);
|
const profile = await fetchProfileData(account.steamId, account.steamLoginSecure);
|
||||||
@@ -401,9 +409,14 @@ ipcMain.handle('login-to-server', async () => {
|
|||||||
const saveServerAuth = (token: string) => {
|
const saveServerAuth = (token: string) => {
|
||||||
if (captured) return; captured = true;
|
if (captured) return; captured = true;
|
||||||
let serverSteamId = undefined;
|
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');
|
const current = store.get('serverConfig');
|
||||||
store.set('serverConfig', { ...current, token, serverSteamId, enabled: true });
|
store.set('serverConfig', { ...current, token, serverSteamId, isAdmin, enabled: true });
|
||||||
initBackend();
|
initBackend();
|
||||||
authWindow.close();
|
authWindow.close();
|
||||||
resolve(true);
|
resolve(true);
|
||||||
@@ -502,6 +515,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-community-accounts', async () => { initBackend(); return backend ? await backend.getCommunityAccounts() : []; });
|
||||||
ipcMain.handle('get-server-users', async () => { initBackend(); return backend ? await backend.getServerUsers() : []; });
|
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('switch-account', async (event, loginName: string) => await handleSwitchAccount(loginName));
|
||||||
ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url));
|
ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url));
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
getCommunityAccounts: () => ipcRenderer.invoke('get-community-accounts'),
|
getCommunityAccounts: () => ipcRenderer.invoke('get-community-accounts'),
|
||||||
getServerUsers: () => ipcRenderer.invoke('get-server-users'),
|
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) => {
|
onAccountsUpdated: (callback: (accounts: any[]) => void) => {
|
||||||
const subscription = (_event: IpcRendererEvent, accounts: any[]) => callback(accounts);
|
const subscription = (_event: IpcRendererEvent, accounts: any[]) => callback(accounts);
|
||||||
ipcRenderer.on('accounts-updated', subscription);
|
ipcRenderer.on('accounts-updated', subscription);
|
||||||
|
|||||||
@@ -119,4 +119,48 @@ export class BackendService {
|
|||||||
throw new Error(e.response?.data?.message || 'Failed to revoke all access');
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>frontend</title>
|
<title>Ultimate Ban Tracker</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface ServerConfig {
|
|||||||
token?: string;
|
token?: string;
|
||||||
serverSteamId?: string;
|
serverSteamId?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
isAdmin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AccountsContextType {
|
interface AccountsContextType {
|
||||||
@@ -51,6 +52,13 @@ interface AccountsContextType {
|
|||||||
getCommunityAccounts: () => Promise<any[]>;
|
getCommunityAccounts: () => Promise<any[]>;
|
||||||
getServerUsers: () => Promise<any[]>;
|
getServerUsers: () => Promise<any[]>;
|
||||||
refreshAccounts: (showLoading?: boolean) => Promise<void>;
|
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);
|
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();
|
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 (
|
return (
|
||||||
<AccountsContext.Provider value={{
|
<AccountsContext.Provider value={{
|
||||||
accounts, serverConfig, isLoading, isSyncing, addAccount, updateAccount, deleteAccount,
|
accounts, serverConfig, isLoading, isSyncing, addAccount, updateAccount, deleteAccount,
|
||||||
switchAccount, openSteamAppLogin, openSteamLogin, updateServerConfig, loginToServer,
|
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}
|
{children}
|
||||||
</AccountsContext.Provider>
|
</AccountsContext.Provider>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
DialogActions, CircularProgress, Paper, Chip,
|
DialogActions, CircularProgress, Paper, Chip,
|
||||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
|
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
|
||||||
Switch, FormControlLabel, Divider, List, ListItem, ListItemText, ListItemSecondaryAction,
|
Switch, FormControlLabel, Divider, List, ListItem, ListItemText, ListItemSecondaryAction,
|
||||||
Select, MenuItem, FormControl, InputLabel
|
Select, MenuItem, FormControl, InputLabel, Tabs, Tab
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
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 PeopleIcon from '@mui/icons-material/People';
|
||||||
import VerifiedUserIcon from '@mui/icons-material/VerifiedUser';
|
import VerifiedUserIcon from '@mui/icons-material/VerifiedUser';
|
||||||
import WorkspacePremiumIcon from '@mui/icons-material/WorkspacePremium';
|
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 { useAccounts, type Account } from '../hooks/useAccounts';
|
||||||
import { useAppTheme } from '../theme/ThemeContext';
|
import { useAppTheme } from '../theme/ThemeContext';
|
||||||
import type { ThemeType } from '../theme/SteamTheme';
|
import type { ThemeType } from '../theme/SteamTheme';
|
||||||
import NebulaBanner from '../components/NebulaBanner';
|
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 Dashboard: React.FC = () => {
|
||||||
const { currentTheme, setTheme } = useAppTheme();
|
const { currentTheme, setTheme } = useAppTheme();
|
||||||
const {
|
const {
|
||||||
@@ -39,6 +144,7 @@ const Dashboard: React.FC = () => {
|
|||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
const [isAdminPanelOpen, setIsAdminPanelOpen] = useState(false);
|
||||||
const [serverUrl, setServerUrl] = useState('');
|
const [serverUrl, setServerUrl] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -70,6 +176,15 @@ const Dashboard: React.FC = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, WebkitAppRegion: 'no-drag' } as any}>
|
<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 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', mr: 1 }}>
|
||||||
{isSyncing ? (
|
{isSyncing ? (
|
||||||
<CircularProgress size={16} sx={{ color: 'primary.main', mr: 1 }} />
|
<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>
|
<Button onClick={() => setIsSettingsOpen(false)} color="inherit" variant="contained">Done</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Admin Panel */}
|
||||||
|
<AdminPanel open={isAdminPanelOpen} onClose={() => setIsAdminPanelOpen(false)} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -294,8 +412,7 @@ const AccountRow: React.FC<{
|
|||||||
(window as any).electronAPI.getServerUserInfo()
|
(window as any).electronAPI.getServerUserInfo()
|
||||||
]);
|
]);
|
||||||
const filtered = (Array.isArray(users) ? users : []).filter(u =>
|
const filtered = (Array.isArray(users) ? users : []).filter(u =>
|
||||||
u.steamId !== selfInfo.steamId &&
|
u.steamId !== selfInfo.steamId && u.steamId !== account.steamId
|
||||||
u.steamId !== account.steamId
|
|
||||||
);
|
);
|
||||||
setServerUsers(filtered);
|
setServerUsers(filtered);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|||||||
Reference in New Issue
Block a user