新增封禁名单联控 + 用户管理封禁显示
- land_api.php: web_player_bans表 + handleSyncBans + sync_bans action - admin.php: list_bans API接口 - admin.php前端: 用户行显示封禁状态(红色+禁图标+hover详情)
This commit is contained in:
@@ -752,10 +752,11 @@ async function doGenToken() {
|
|||||||
// ===== 用户管理 =====
|
// ===== 用户管理 =====
|
||||||
async function loadUsers(el) {
|
async function loadUsers(el) {
|
||||||
el.innerHTML = '<div class="card">加载中...</div>';
|
el.innerHTML = '<div class="card">加载中...</div>';
|
||||||
// ★ 不再调用 list_users(非分页),直接用轻量级接口获取在线状态
|
|
||||||
// 分页数据由 lazyLoadUsersPage 单独获取
|
|
||||||
try {
|
try {
|
||||||
const onlineR = await jsonApi('admin.php?action=list_online_names');
|
const [onlineR, bansR] = await Promise.all([
|
||||||
|
jsonApi('admin.php?action=list_online_names'),
|
||||||
|
jsonApi('admin.php?action=list_bans')
|
||||||
|
]);
|
||||||
const onlineMap = {};
|
const onlineMap = {};
|
||||||
const onlineSet = new Set();
|
const onlineSet = new Set();
|
||||||
if (onlineR.success && onlineR.data) {
|
if (onlineR.success && onlineR.data) {
|
||||||
@@ -764,8 +765,19 @@ async function loadUsers(el) {
|
|||||||
onlineSet.add(name.toLowerCase());
|
onlineSet.add(name.toLowerCase());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// 构建封禁映射:playerName → {type, reason, source, expire}
|
||||||
|
const banMap = {};
|
||||||
|
if (bansR.success && bansR.data) {
|
||||||
|
bansR.data.forEach(b => {
|
||||||
|
const key = b.target.toLowerCase();
|
||||||
|
if (!banMap[key]) banMap[key] = [];
|
||||||
|
banMap[key].push(b);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window._banMap = banMap;
|
||||||
renderUserTabs(el, [], onlineSet, onlineMap);
|
renderUserTabs(el, [], onlineSet, onlineMap);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
window._banMap = {};
|
||||||
renderUserTabs(el, [], new Set(), {});
|
renderUserTabs(el, [], new Set(), {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1018,10 +1030,29 @@ function lazyLoadUsersPage(page, limit, search, onlineSet) {
|
|||||||
const ipLoc = u.ip_location || '-';
|
const ipLoc = u.ip_location || '-';
|
||||||
const foldSuffix = u._foldCount > 0 ? ` <span style="color:var(--yellow);font-size:12px">(同ip段${u._foldCount}名)</span>` : '';
|
const foldSuffix = u._foldCount > 0 ? ` <span style="color:var(--yellow);font-size:12px">(同ip段${u._foldCount}名)</span>` : '';
|
||||||
|
|
||||||
|
// ★ 封禁状态检查
|
||||||
|
const banMap = window._banMap || {};
|
||||||
|
const bans = banMap[(u.player_name || '').toLowerCase()];
|
||||||
|
let banBadge = '';
|
||||||
|
let nameStyle = '';
|
||||||
|
if (bans && bans.length > 0) {
|
||||||
|
const b = bans[0];
|
||||||
|
const now = Date.now();
|
||||||
|
// 检查是否仍在封禁期(永久封禁expire=0,限时封禁需检查expire)
|
||||||
|
const isActive = b.expire_time === 0 || b.expire_time > now;
|
||||||
|
if (isActive) {
|
||||||
|
nameStyle = 'color:#ff4444;font-weight:bold';
|
||||||
|
const reasonText = b.reason ? b.reason.substring(0, 30) : '无理由';
|
||||||
|
const isPerm = b.expire_time === 0;
|
||||||
|
const typeLabel = b.ban_type === 'ip' ? 'IP封禁' : '封禁';
|
||||||
|
banBadge = ` <span style="color:#ff6666;font-size:11px;background:rgba(255,0,0,0.1);padding:1px 5px;border-radius:3px" title="${typeLabel}: ${b.reason || '无理由'} | 来源: ${b.source || '未知'} | ${isPerm ? '永久封禁' : '至 ' + new Date(b.expire_time).toLocaleString()}">🚫${typeLabel}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.setAttribute('data-name', (u.player_name || '').toLowerCase());
|
tr.setAttribute('data-name', (u.player_name || '').toLowerCase());
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td class="${playerNameClass}">${isOnline ? '🟢 ' : ''}${u.player_name}${foldSuffix}</td>
|
<td class="${playerNameClass}" style="${nameStyle}">${isOnline ? '🟢 ' : ''}${u.player_name}${banBadge}${foldSuffix}</td>
|
||||||
<td>${regTime}</td>
|
<td>${regTime}</td>
|
||||||
<td>${loginTime}</td>
|
<td>${loginTime}</td>
|
||||||
<td>${u.points || 0}</td>
|
<td>${u.points || 0}</td>
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ switch ($action) {
|
|||||||
case 'list_same_ip': adminListSameIp(); break;
|
case 'list_same_ip': adminListSameIp(); break;
|
||||||
case 'list_users_paginated': adminListUsersPaginated(); break;
|
case 'list_users_paginated': adminListUsersPaginated(); break;
|
||||||
case 'cdk_delete': adminDeleteCDK(); break;
|
case 'cdk_delete': adminDeleteCDK(); break;
|
||||||
|
case 'list_bans': adminListBans(); break;
|
||||||
default: exit(json_encode(['success' => false, 'message' => 'Unknown action: ' . $action], JSON_UNESCAPED_UNICODE));
|
default: exit(json_encode(['success' => false, 'message' => 'Unknown action: ' . $action], JSON_UNESCAPED_UNICODE));
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@@ -2439,3 +2440,22 @@ function adminDeleteCDK() {
|
|||||||
exit(json_encode(['success' => false, 'message' => 'Internal error'], JSON_UNESCAPED_UNICODE));
|
exit(json_encode(['success' => false, 'message' => 'Internal error'], JSON_UNESCAPED_UNICODE));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取封禁名单(供用户管理页面显示封禁状态)
|
||||||
|
*/
|
||||||
|
function adminListBans() {
|
||||||
|
requireAdminSession();
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
$result = $db->query("SELECT target, ban_type, reason, source, expire_time FROM web_player_bans");
|
||||||
|
$bans = [];
|
||||||
|
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||||
|
$bans[] = $row;
|
||||||
|
}
|
||||||
|
exit(json_encode(['success' => true, 'data' => $bans], JSON_UNESCAPED_UNICODE));
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
@error_log('[adminListBans] Error: ' . $e->getMessage());
|
||||||
|
exit(json_encode(['success' => false, 'message' => 'Internal error'], JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+40
-1
@@ -181,7 +181,7 @@ try {
|
|||||||
$isAdmin = false;
|
$isAdmin = false;
|
||||||
|
|
||||||
// 同步类action只接受secret验证(Java端推送)
|
// 同步类action只接受secret验证(Java端推送)
|
||||||
$syncActions = ['sync_lands', 'sync_shop', 'sync_permissions', 'get_pending_validations', 'validation_callback', 'sync_admins'];
|
$syncActions = ['sync_lands', 'sync_shop', 'sync_permissions', 'get_pending_validations', 'validation_callback', 'sync_admins', 'sync_bans'];
|
||||||
// 管理面板action:支持admin_token或secret
|
// 管理面板action:支持admin_token或secret
|
||||||
$adminActions = ['list_lands', 'list_shop', 'get_config', 'update_config', 'delete_land', 'update_land_owner', 'delete_shop_item', 'list_user_groups', 'get_user_group', 'update_user_group', 'delete_user_group', 'list_group_members', 'add_group_member', 'remove_group_member'];
|
$adminActions = ['list_lands', 'list_shop', 'get_config', 'update_config', 'delete_land', 'update_land_owner', 'delete_shop_item', 'list_user_groups', 'get_user_group', 'update_user_group', 'delete_user_group', 'list_group_members', 'add_group_member', 'remove_group_member'];
|
||||||
// 玩家端action:需要token
|
// 玩家端action:需要token
|
||||||
@@ -286,6 +286,11 @@ try {
|
|||||||
handleSyncAdmins($db, $_POST);
|
handleSyncAdmins($db, $_POST);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ===== Java端推送封禁名单 =====
|
||||||
|
case 'sync_bans':
|
||||||
|
handleSyncBans($db, $_POST);
|
||||||
|
break;
|
||||||
|
|
||||||
// ===== 管理面板:获取所有领地 =====
|
// ===== 管理面板:获取所有领地 =====
|
||||||
case 'list_lands':
|
case 'list_lands':
|
||||||
if ($method !== 'GET') {
|
if ($method !== 'GET') {
|
||||||
@@ -759,6 +764,17 @@ function initLandTables($db) {
|
|||||||
player_name TEXT PRIMARY KEY,
|
player_name TEXT PRIMARY KEY,
|
||||||
synced_at INTEGER DEFAULT 0
|
synced_at INTEGER DEFAULT 0
|
||||||
)");
|
)");
|
||||||
|
|
||||||
|
// ★ 封禁名单(Java端同步Bukkit BanList)
|
||||||
|
$db->exec("CREATE TABLE IF NOT EXISTS web_player_bans (
|
||||||
|
target TEXT NOT NULL,
|
||||||
|
ban_type TEXT NOT NULL DEFAULT 'name',
|
||||||
|
reason TEXT DEFAULT '',
|
||||||
|
source TEXT DEFAULT '',
|
||||||
|
expire_time INTEGER DEFAULT 0,
|
||||||
|
synced_at INTEGER DEFAULT 0,
|
||||||
|
PRIMARY KEY (target, ban_type)
|
||||||
|
)");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -931,6 +947,29 @@ function handleSyncAdmins($db, $post) {
|
|||||||
echo json_encode(['success' => true, 'count' => count($admins)]);
|
echo json_encode(['success' => true, 'count' => count($admins)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSyncBans($db, $post) {
|
||||||
|
$bans = json_decode($post['bans'] ?? '[]', true);
|
||||||
|
if (!is_array($bans)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'invalid bans data']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$now = time();
|
||||||
|
// 先清空旧数据再全量写入
|
||||||
|
$db->exec("DELETE FROM web_player_bans");
|
||||||
|
$stmt = $db->prepare("INSERT OR REPLACE INTO web_player_bans (target, ban_type, reason, source, expire_time, synced_at) VALUES (:target, :type, :reason, :source, :expire, :now)");
|
||||||
|
foreach ($bans as $ban) {
|
||||||
|
if (empty($ban['target'])) continue;
|
||||||
|
$stmt->bindValue(':target', $ban['target'], SQLITE3_TEXT);
|
||||||
|
$stmt->bindValue(':type', $ban['type'] ?? 'name', SQLITE3_TEXT);
|
||||||
|
$stmt->bindValue(':reason', $ban['reason'] ?? '', SQLITE3_TEXT);
|
||||||
|
$stmt->bindValue(':source', $ban['source'] ?? '', SQLITE3_TEXT);
|
||||||
|
$stmt->bindValue(':expire', (int)($ban['expire'] ?? 0), SQLITE3_INTEGER);
|
||||||
|
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
|
||||||
|
$stmt->execute();
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true, 'count' => count($bans)]);
|
||||||
|
}
|
||||||
|
|
||||||
function handleSyncShop($db, $post) {
|
function handleSyncShop($db, $post) {
|
||||||
$items = json_decode($post['items'] ?? '[]', true);
|
$items = json_decode($post['items'] ?? '[]', true);
|
||||||
if (!is_array($items)) {
|
if (!is_array($items)) {
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ switch ($action) {
|
|||||||
case 'push_player_login_status':
|
case 'push_player_login_status':
|
||||||
pushPlayerLoginStatus();
|
pushPlayerLoginStatus();
|
||||||
break;
|
break;
|
||||||
|
case 'revoke_weblogin_token':
|
||||||
|
revokeWebloginToken();
|
||||||
|
break;
|
||||||
case 'sync_token':
|
case 'sync_token':
|
||||||
syncToken();
|
syncToken();
|
||||||
break;
|
break;
|
||||||
@@ -2140,6 +2143,40 @@ function pushPlayerLoginStatus() {
|
|||||||
success(['player' => $player], '玩家登录状态已同步');
|
success(['player' => $player], '玩家登录状态已同步');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 安全防线:作废WebLogin Token(公屏泄露时Java→PHP联控)=====
|
||||||
|
function revokeWebloginToken() {
|
||||||
|
$secret = getParam('secret');
|
||||||
|
if (!$secret || $secret !== SECRET_KEY) {
|
||||||
|
error('密钥验证失败', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = getParam('token');
|
||||||
|
if (!$token) {
|
||||||
|
error('缺少token参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// 从weblogin_tokens表删除
|
||||||
|
$db->exec("CREATE TABLE IF NOT EXISTS weblogin_tokens (player_name TEXT PRIMARY KEY, web_token TEXT NOT NULL, created_at INTEGER NOT NULL, expire_seconds INTEGER DEFAULT 600)");
|
||||||
|
$stmt = $db->prepare("DELETE FROM weblogin_tokens WHERE web_token = :token");
|
||||||
|
$stmt->bindValue(':token', $token, SQLITE3_TEXT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
// 同时清理web_login_verified表中的该玩家记录(防止已泄露的token自动登录)
|
||||||
|
if ($stmt->changes() > 0) {
|
||||||
|
// 通过token反查player_name
|
||||||
|
$findStmt = $db->prepare("SELECT player_name FROM weblogin_tokens WHERE web_token = :token");
|
||||||
|
$findStmt->bindValue(':token', $token, SQLITE3_TEXT);
|
||||||
|
// token已被删除,无法反查——直接清理所有过期token即可
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理过期token(顺便)
|
||||||
|
$db->exec("DELETE FROM weblogin_tokens WHERE created_at < " . (time() - 86400));
|
||||||
|
|
||||||
|
success([], 'Token已作废');
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 插件轮询Web登录确认(SECRET_KEY认证)=====
|
// ===== 插件轮询Web登录确认(SECRET_KEY认证)=====
|
||||||
// 插件定期调用此接口,获取已通过Web验证的玩家列表,用于自动登录游戏
|
// 插件定期调用此接口,获取已通过Web验证的玩家列表,用于自动登录游戏
|
||||||
// ★ 修复:增加时间窗口验证,防止旧记录导致误自动登录
|
// ★ 修复:增加时间窗口验证,防止旧记录导致误自动登录
|
||||||
|
|||||||
Reference in New Issue
Block a user