新增封禁名单联控 + 用户管理封禁显示

- land_api.php: web_player_bans表 + handleSyncBans + sync_bans action
- admin.php: list_bans API接口
- admin.php前端: 用户行显示封禁状态(红色+禁图标+hover详情)
This commit is contained in:
ypsdf1
2026-07-12 22:08:46 +08:00
parent 498245d967
commit b34753d1fd
4 changed files with 132 additions and 5 deletions
+35 -4
View File
@@ -752,10 +752,11 @@ async function doGenToken() {
// ===== 用户管理 =====
async function loadUsers(el) {
el.innerHTML = '<div class="card">加载中...</div>';
// ★ 不再调用 list_users(非分页),直接用轻量级接口获取在线状态
// 分页数据由 lazyLoadUsersPage 单独获取
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 onlineSet = new Set();
if (onlineR.success && onlineR.data) {
@@ -764,8 +765,19 @@ async function loadUsers(el) {
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);
} catch(e) {
window._banMap = {};
renderUserTabs(el, [], new Set(), {});
}
}
@@ -1018,10 +1030,29 @@ function lazyLoadUsersPage(page, limit, search, onlineSet) {
const ipLoc = u.ip_location || '-';
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');
tr.setAttribute('data-name', (u.player_name || '').toLowerCase());
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>${loginTime}</td>
<td>${u.points || 0}</td>
+20
View File
@@ -77,6 +77,7 @@ switch ($action) {
case 'list_same_ip': adminListSameIp(); break;
case 'list_users_paginated': adminListUsersPaginated(); break;
case 'cdk_delete': adminDeleteCDK(); break;
case 'list_bans': adminListBans(); break;
default: exit(json_encode(['success' => false, 'message' => 'Unknown action: ' . $action], JSON_UNESCAPED_UNICODE));
}
} catch (\Throwable $e) {
@@ -2439,3 +2440,22 @@ function adminDeleteCDK() {
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
View File
@@ -181,7 +181,7 @@ try {
$isAdmin = false;
// 同步类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
$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
@@ -286,6 +286,11 @@ try {
handleSyncAdmins($db, $_POST);
break;
// ===== Java端推送封禁名单 =====
case 'sync_bans':
handleSyncBans($db, $_POST);
break;
// ===== 管理面板:获取所有领地 =====
case 'list_lands':
if ($method !== 'GET') {
@@ -759,6 +764,17 @@ function initLandTables($db) {
player_name TEXT PRIMARY KEY,
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)]);
}
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) {
$items = json_decode($post['items'] ?? '[]', true);
if (!is_array($items)) {
+37
View File
@@ -139,6 +139,9 @@ switch ($action) {
case 'push_player_login_status':
pushPlayerLoginStatus();
break;
case 'revoke_weblogin_token':
revokeWebloginToken();
break;
case 'sync_token':
syncToken();
break;
@@ -2140,6 +2143,40 @@ function pushPlayerLoginStatus() {
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验证的玩家列表,用于自动登录游戏
// ★ 修复:增加时间窗口验证,防止旧记录导致误自动登录