SecHub/admin.php
2026-07-01 00:47:09 +08:00

1186 lines
51 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* SecHub - 数据管理后台
* 排序管理 + 数据CRUD + 批量导入导出 + 系统设置
*/
require_once __DIR__ . '/config.php';
session_start();
// 确保数据库目录存在
if (!is_dir(DB_DIR)) {
mkdir(DB_DIR, 0755, true);
}
// 引入数据库类
require_once __DIR__ . '/db.php';
// 处理登录
if (isset($_POST['login'])) {
if ($_POST['password'] === ADMIN_PASSWORD) {
$_SESSION['authenticated'] = true;
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
header('Location: admin.php');
exit;
} else {
$error = '密码错误!';
}
}
// 处理登出
if (isset($_GET['logout'])) {
session_destroy();
header('Location: admin.php');
exit;
}
// 检查是否已登录
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SecHub - 管理登录</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: white;
padding: 40px;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
width: 380px;
max-width: 90vw;
}
.login-container h1 {
text-align: center;
margin-bottom: 30px;
font-size: 1.5em;
color: #2c3e50;
}
.form-group { margin-bottom: 20px; }
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #555;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e6ed;
border-radius: 8px;
font-size: 1em;
transition: border-color 0.2s;
}
.form-group input:focus {
border-color: #667eea;
outline: none;
}
button[type="submit"] {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
button[type="submit"]:hover { transform: translateY(-2px); }
.error {
background: #ffeaea;
color: #d63031;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
text-align: center;
font-weight: 500;
}
</style>
</head>
<body>
<div class="login-container">
<h1>SecHub 管理后台</h1>
<?php if (isset($error)): ?>
<div class="error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<label for="password">访问密码</label>
<input type="password" id="password" name="password" required autofocus>
</div>
<button type="submit" name="login">登录</button>
</form>
</div>
</body>
</html>
<?php
exit;
}
// 初始化 CSRF token
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// 初始化数据库
try {
$database = new SecHubDatabase(DB_PATH, JSON_DIR);
$database->syncJsonToDatabase();
} catch (Exception $e) {
$error = '数据库初始化失败: ' . $e->getMessage();
}
// 消息提示
$message = '';
$messageType = 'success';
// ======================== 处理POST动作 ========================
// CSRF 校验(对非登录请求)
$csrfOk = true;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !isset($_POST['login'])) {
$token = $_POST['csrf_token'] ?? '';
if ($token !== $_SESSION['csrf_token']) {
$csrfOk = false;
$message = '安全令牌无效,请刷新页面重试';
$messageType = 'error';
}
}
if ($csrfOk) {
// --- 保存排序 ---
if (isset($_POST['save_order']) && isset($_POST['orders'])) {
try {
$newOrder = 1;
foreach ($_POST['orders'] as $filename) {
$filePath = JSON_DIR . $filename;
if (file_exists($filePath)) {
$content = file_get_contents($filePath);
$data = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($data) && !empty($data)) {
$data[0]['no'] = $newOrder;
$newContent = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
file_put_contents($filePath, $newContent . "\n");
$newOrder++;
}
}
}
$database->syncJsonToDatabase();
$message = '排序保存成功!';
$messageType = 'success';
} catch (Exception $e) {
$message = '保存失败: ' . $e->getMessage();
$messageType = 'error';
}
}
// --- 添加工具 ---
if (isset($_POST['add_item'])) {
$tableName = $_POST['table_name'] ?? '';
$name = trim($_POST['name'] ?? '');
$url = trim($_POST['url'] ?? '');
$description = trim($_POST['description'] ?? '');
if (empty($name)) {
$message = '工具名称不能为空';
$messageType = 'error';
} else {
$result = $database->addItemToSection($tableName, $name, $url, $description);
if ($result['success']) {
$message = '工具添加成功!';
} else {
$message = '添加失败: ' . ($result['error'] ?? '未知错误');
$messageType = 'error';
}
}
}
// --- 更新工具 ---
if (isset($_POST['update_item'])) {
$tableName = $_POST['table_name'] ?? '';
$itemIndex = intval($_POST['item_index'] ?? 0);
$name = trim($_POST['name'] ?? '');
$url = trim($_POST['url'] ?? '');
$description = trim($_POST['description'] ?? '');
if (empty($name)) {
$message = '工具名称不能为空';
$messageType = 'error';
} else {
$result = $database->updateItemInSection($tableName, $itemIndex, $name, $url, $description);
if ($result['success']) {
$message = '工具更新成功!';
} else {
$message = '更新失败: ' . ($result['error'] ?? '未知错误');
$messageType = 'error';
}
}
}
// --- 删除工具 ---
if (isset($_POST['delete_item'])) {
$tableName = $_POST['table_name'] ?? '';
$itemIndex = intval($_POST['item_index'] ?? 0);
$result = $database->deleteItemFromSection($tableName, $itemIndex);
if ($result['success']) {
$message = '工具已删除!';
} else {
$message = '删除失败: ' . ($result['error'] ?? '未知错误');
$messageType = 'error';
}
}
// --- 新增栏目 ---
if (isset($_POST['add_section'])) {
$sectionName = trim($_POST['section_name'] ?? '');
if (empty($sectionName)) {
$message = '栏目名称不能为空';
$messageType = 'error';
} else {
$result = $database->createSection($sectionName);
if ($result['success']) {
$message = '栏目 "' . htmlspecialchars($sectionName) . '" 创建成功!';
} else {
$message = '创建失败: ' . ($result['error'] ?? '未知错误');
$messageType = 'error';
}
}
}
// --- 删除栏目 ---
if (isset($_POST['delete_section'])) {
$tableName = $_POST['table_name'] ?? '';
$confirm = $_POST['confirm'] ?? '';
if ($confirm !== 'yes') {
$message = '请确认删除操作';
$messageType = 'error';
} else {
$result = $database->removeSection($tableName);
if ($result['success']) {
$message = '栏目已删除!';
} else {
$message = '删除失败: ' . ($result['error'] ?? '未知错误');
$messageType = 'error';
}
}
}
// --- 重命名栏目 ---
if (isset($_POST['rename_section'])) {
$tableName = $_POST['table_name'] ?? '';
$newName = trim($_POST['new_name'] ?? '');
if (empty($newName)) {
$message = '栏目名称不能为空';
$messageType = 'error';
} else {
$result = $database->renameSection($tableName, $newName);
if ($result['success']) {
$message = '栏目重命名成功!';
} else {
$message = '重命名失败: ' . ($result['error'] ?? '未知错误');
$messageType = 'error';
}
}
}
// --- 批量导入 ---
if (isset($_POST['batch_import'])) {
$tableName = $_POST['table_name'] ?? '';
$importData = trim($_POST['import_data'] ?? '');
$format = $_POST['import_format'] ?? 'csv';
if (empty($importData)) {
$message = '导入数据不能为空';
$messageType = 'error';
} else {
$lines = explode("\n", str_replace("\r\n", "\n", $importData));
$items = [];
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
if ($format === 'csv') {
// CSV格式: 名称,URL,描述
$parts = str_getcsv($line);
$items[] = [
trim($parts[0] ?? ''),
trim($parts[1] ?? ''),
trim($parts[2] ?? '')
];
} else {
// 竖线格式: 名称 | URL | 描述
$parts = explode('|', $line);
$items[] = [
trim($parts[0] ?? ''),
trim($parts[1] ?? ''),
trim($parts[2] ?? '')
];
}
}
$result = $database->batchAddItems($tableName, $items);
if ($result['success']) {
$message = "批量导入成功!成功添加 {$result['count']} 个工具(共 {$result['total']} 条数据)";
} else {
$message = '导入失败: ' . ($result['error'] ?? '未知错误');
$messageType = 'error';
}
}
}
// --- 导出JSON ---
if (isset($_POST['export_json'])) {
$tableName = $_POST['table_name'] ?? 'all';
if ($tableName === 'all') {
$data = $database->exportAllSections();
$filename = 'sechub_all_sections.json';
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} elseif ($database->getSectionInfo($tableName)) {
$filePath = JSON_DIR . $tableName . '.json';
if (file_exists($filePath)) {
$json = file_get_contents($filePath);
$filename = $tableName . '.json';
} else {
$message = '文件不存在';
$messageType = 'error';
$json = null;
}
} else {
$message = '无效的栏目';
$messageType = 'error';
$json = null;
}
if (isset($json)) {
header('Content-Type: application/json; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
echo $json;
$database->close();
exit;
}
}
// --- 导出CSV ---
if (isset($_POST['export_csv'])) {
$tableName = $_POST['table_name'] ?? '';
if (empty($tableName)) {
$message = '请选择要导出的栏目';
$messageType = 'error';
} else {
$csv = $database->exportSectionToCsv($tableName);
if (!empty($csv)) {
$info = $database->getSectionInfo($tableName);
$filename = ($info['title'] ?? $tableName) . '.csv';
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
echo "\xEF\xBB\xBF" . $csv;
$database->close();
exit;
} else {
$message = '导出失败或栏目为空';
$messageType = 'error';
}
}
}
// --- 强制重新同步 ---
if (isset($_POST['resync_db'])) {
try {
$database->syncJsonToDatabase();
$message = '数据库重新同步成功!';
} catch (Exception $e) {
$message = '同步失败: ' . $e->getMessage();
$messageType = 'error';
}
}
// --- 删除数据库 ---
if (isset($_POST['delete_db'])) {
if (file_exists(DB_PATH)) {
unlink(DB_PATH);
$message = '数据库已删除!刷新后自动重建。';
} else {
$message = '数据库文件不存在。';
$messageType = 'error';
}
}
// --- 修改密码 ---
if (isset($_POST['change_password'])) {
$oldPwd = $_POST['old_password'] ?? '';
$newPwd = $_POST['new_password'] ?? '';
$confirmPwd = $_POST['confirm_password'] ?? '';
if ($oldPwd !== ADMIN_PASSWORD) {
$message = '当前密码错误';
$messageType = 'error';
} elseif (strlen($newPwd) < 6) {
$message = '新密码长度不能少于6位';
$messageType = 'error';
} elseif ($newPwd !== $confirmPwd) {
$message = '两次密码输入不一致';
$messageType = 'error';
} else {
// 更新config.php中的密码
$configPath = __DIR__ . '/config.php';
$configContent = file_get_contents($configPath);
$newContent = preg_replace(
"/define\('ADMIN_PASSWORD',\s*'[^']*'\)/",
"define('ADMIN_PASSWORD', '" . addslashes($newPwd) . "')",
$configContent
);
if (file_put_contents($configPath, $newContent)) {
$message = '密码修改成功!';
// 更新当前session中的标记不需要改密码因为会话还在
} else {
$message = '密码修改失败(文件写入错误)';
$messageType = 'error';
}
}
}
} // end CSRF check
// 获取栏目列表
$allSections = $database->getAllSectionsWithInfo();
// 当前选中的栏目从URL参数或第一个
$currentTab = $_GET['tab'] ?? 'sort';
$selectedSection = $_GET['section'] ?? ($allSections[0]['name'] ?? '');
// 获取选中栏目的工具列表
$currentItems = [];
$currentSectionInfo = null;
if ($selectedSection) {
$currentItems = $database->getItemsBySection($selectedSection);
$currentSectionInfo = $database->getSectionInfo($selectedSection);
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="./assets/imgs/favicon.ico" type="image/x-icon">
<title>SecHub - 管理后台</title>
<link rel="stylesheet" href="assets/css/admin.css">
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<div>
<h1>SecHub 管理后台</h1>
<p class="header-sub">排序管理 / 数据管理 / 批量操作 / 系统设置</p>
</div>
<div class="header-actions">
<button id="themeToggle" class="theme-btn" title="切换暗黑模式">
<span id="themeIcon">&#9790;</span>
</button>
<a href="index.php" class="btn btn-outline" target="_blank">预览站点</a>
<a href="?logout=1" class="btn btn-danger">退出登录</a>
</div>
</div>
<!-- Message -->
<?php if (!empty($message)): ?>
<div id="pageAlert" class="alert alert-<?= $messageType === 'error' ? 'error' : ($messageType === 'warning' ? 'warning' : 'success') ?>">
<?= ($messageType === 'success' ? '&#10003; ' : '&#9888; ') . htmlspecialchars($message) ?>
</div>
<?php endif; ?>
<!-- Tabs -->
<div class="tabs">
<button class="tab-btn <?= $currentTab === 'sort' ? 'active' : '' ?>" onclick="switchTab('sort')">排序管理</button>
<button class="tab-btn <?= $currentTab === 'data' ? 'active' : '' ?>" onclick="switchTab('data')">数据管理</button>
<button class="tab-btn <?= $currentTab === 'import' ? 'active' : '' ?>" onclick="switchTab('import')">批量操作</button>
<button class="tab-btn <?= $currentTab === 'settings' ? 'active' : '' ?>" onclick="switchTab('settings')">系统设置</button>
</div>
<!-- Tab: 排序管理 -->
<div class="tab-content <?= $currentTab === 'sort' ? 'active' : '' ?>" id="tab-sort">
<div class="panel">
<div class="tip">拖动行左侧的 &#8942;&#8942; 图标来调整栏目顺序,修改后点击"保存排序"生效。</div>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:50px;"></th>
<th style="width:80px;">序列</th>
<th>文件名称</th>
<th>栏目名称</th>
<th style="width:120px;">工具数</th>
</tr>
</thead>
<tbody id="sortable-tbody">
<?php foreach ($allSections as $index => $sec): ?>
<tr class="draggable-row" data-filename="<?= htmlspecialchars($sec['filename']) ?>" draggable="true">
<td><span class="drag-handle">&#8942;&#8942;</span></td>
<td><span class="order-num"><?= $index + 1 ?></span></td>
<td><span class="mono"><?= htmlspecialchars($sec['filename']) ?></span></td>
<td><?= htmlspecialchars($sec['title']) ?></td>
<td><span class="count-badge"><?= $sec['item_count'] ?> 个</span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div id="order-inputs" style="display:none;"></div>
<div class="sort-save-bar">
<button type="submit" name="save_order" class="btn btn-success btn-lg">
保存排序
</button>
</div>
</form>
</div>
</div>
<!-- Tab: 数据管理 -->
<div class="tab-content <?= $currentTab === 'data' ? 'active' : '' ?>" id="tab-data">
<div class="panel">
<div class="two-col">
<!-- 左侧:栏目列表 -->
<div class="side">
<div class="action-bar">
<h3>栏目列表</h3>
<button class="btn btn-primary btn-small" onclick="showModal('addSectionModal')">+ 新增</button>
</div>
<div class="section-list">
<?php foreach ($allSections as $sec): ?>
<div class="section-list-item <?= $sec['name'] === $selectedSection ? 'active' : '' ?>"
onclick="selectSection('<?= htmlspecialchars($sec['name']) ?>')">
<div>
<div class="sname"><?= htmlspecialchars($sec['title']) ?></div>
<div class="scount"><?= $sec['item_count'] ?> 个工具</div>
</div>
<div class="sactions">
<button class="btn-text" onclick="event.stopPropagation();showRenameModal('<?= htmlspecialchars($sec['name']) ?>','<?= htmlspecialchars(addslashes($sec['title'])) ?>')" title="重命名">&#9998;</button>
<button class="btn-text danger" onclick="event.stopPropagation();showDeleteSectionModal('<?= htmlspecialchars($sec['name']) ?>','<?= htmlspecialchars(addslashes($sec['title'])) ?>')" title="删除">&#10005;</button>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($allSections)): ?>
<div class="empty-state">暂无栏目</div>
<?php endif; ?>
</div>
</div>
<!-- 右侧:工具列表 -->
<div class="main">
<div class="section-header-row">
<h3>
工具列表
<?php if ($currentSectionInfo): ?>
- <span class="col-section-title"><?= htmlspecialchars($currentSectionInfo['title']) ?></span>
<?php endif; ?>
</h3>
<?php if ($selectedSection): ?>
<button class="btn btn-primary btn-small" onclick="showAddToolModal('<?= htmlspecialchars($selectedSection) ?>')">+ 新增工具</button>
<?php endif; ?>
</div>
<?php if (!$selectedSection): ?>
<div class="empty-state">请从左侧选择一个栏目</div>
<?php elseif (empty($currentItems)): ?>
<div class="empty-state">
此栏目暂无工具数据
<br><br>
<button class="btn btn-primary" onclick="showAddToolModal('<?= htmlspecialchars($selectedSection) ?>')">+ 添加第一个工具</button>
</div>
<?php else: ?>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:50px;">#</th>
<th style="width:20%;">名称</th>
<th style="width:30%;">URL</th>
<th style="width:35%;">描述</th>
<th style="width:80px;">操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($currentItems as $index => $item): ?>
<tr>
<td><?= $index + 1 ?></td>
<td><strong><?= htmlspecialchars($item['name'] ?? '') ?></strong></td>
<td class="cell-url">
<a href="<?= htmlspecialchars($item['url'] ?? '#') ?>" target="_blank"><?= htmlspecialchars($item['url'] ?? '') ?></a>
</td>
<td class="cell-desc"><?= htmlspecialchars($item['description'] ?? '') ?></td>
<td>
<button class="btn-text" onclick="showEditToolModal('<?= htmlspecialchars($selectedSection) ?>',<?= $index ?>,'<?= htmlspecialchars(addslashes($item['name'] ?? '')) ?>','<?= htmlspecialchars(addslashes($item['url'] ?? '')) ?>','<?= htmlspecialchars(addslashes($item['description'] ?? '')) ?>')">&#9998;</button>
<button class="btn-text danger" onclick="showDeleteToolModal('<?= htmlspecialchars($selectedSection) ?>',<?= $index ?>,'<?= htmlspecialchars(addslashes($item['name'] ?? '')) ?>')">&#10005;</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- Tab: 批量操作 -->
<div class="tab-content <?= $currentTab === 'import' ? 'active' : '' ?>" id="tab-import">
<div class="panel">
<div class="two-col">
<!-- 批量导入 -->
<div class="main">
<h3 style="margin-bottom:15px;">批量导入</h3>
<div class="tip">
支持两种格式导入每行一个工具。支持CSV上传和粘贴文本。
</div>
<form method="POST" id="importForm">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="batch_import" value="1">
<div class="form-group">
<label>目标栏目</label>
<select name="table_name" required class="form-select">
<?php foreach ($allSections as $sec): ?>
<option value="<?= htmlspecialchars($sec['name']) ?>"><?= htmlspecialchars($sec['title']) ?> (<?= $sec['item_count'] ?>个工具)</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin-bottom:12px;">
<label>导入格式</label>
<select name="import_format" id="importFormat" class="form-select-sm">
<option value="csv">CSV: 名称,URL,描述</option>
<option value="pipe">竖线: 名称 | URL | 描述</option>
</select>
</div>
<div class="import-area">
<label>导入数据(每行一个工具)</label>
<textarea name="import_data" id="importData" placeholder="<?php
echo "CSV格式示例\n漏洞扫描器,https://example.com/scanner,一款快速漏洞扫描工具\n资产收集工具,https://example.com/asset,自动收集子域名和资产信息\n\n或竖线格式\n漏洞扫描器 | https://example.com/scanner | 一款快速漏洞扫描工具";
?>"></textarea>
</div>
<div class="import-preview" id="importPreview">
<div class="preview-empty">输入数据后点击"预览"查看解析结果</div>
</div>
<div style="display:flex;gap:10px;margin-top:15px;">
<button type="button" class="btn btn-outline" onclick="previewImport()">预览解析</button>
<button type="submit" class="btn btn-success" onclick="return confirm('确认导入 ' + document.querySelector('#importPreview tbody')?.rows.length + ' 个工具?')">确认导入</button>
</div>
</form>
</div>
<!-- 批量导出 -->
<div class="side">
<h3 style="margin-bottom:15px;">批量导出</h3>
<div class="export-box">
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="form-group">
<label>选择栏目</label>
<select name="table_name" class="form-select">
<option value="all">全部栏目JSON</option>
<?php foreach ($allSections as $sec): ?>
<option value="<?= htmlspecialchars($sec['name']) ?>"><?= htmlspecialchars($sec['title']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<button type="submit" name="export_json" class="btn btn-primary btn-small">导出 JSON</button>
<button type="submit" name="export_csv" class="btn btn-success btn-small">导出 CSV</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: 系统设置 -->
<div class="tab-content <?= $currentTab === 'settings' ? 'active' : '' ?>" id="tab-settings">
<div class="panel">
<div class="settings-group">
<h3>修改管理密码</h3>
<form method="POST" onsubmit="return confirm('确定要修改密码吗?')">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="change_password" value="1">
<div class="form-group">
<label>当前密码</label>
<input type="password" name="old_password" required>
</div>
<div class="form-group">
<label>新密码至少6位</label>
<input type="password" name="new_password" required minlength="6">
</div>
<div class="form-group">
<label>确认新密码</label>
<input type="password" name="confirm_password" required minlength="6">
</div>
<button type="submit" class="btn btn-warning">修改密码</button>
</form>
</div>
<div class="settings-group">
<h3>数据库维护</h3>
<div class="settings-info">
数据库位置: <?= htmlspecialchars(DB_PATH) ?><br>
JSON目录: <?= htmlspecialchars(JSON_DIR) ?><br>
栏目数量: <?= count($allSections) ?>
</div>
<div class="settings-actions">
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<button type="submit" name="resync_db" class="btn btn-primary">强制重新同步</button>
</form>
<form method="POST" onsubmit="return confirm('确定要删除数据库吗?下次访问将自动重建。')">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<button type="submit" name="delete_db" class="btn btn-danger">删除并重建数据库</button>
</form>
</div>
</div>
</div>
</div>
<footer class="admin-footer">
&copy; <?= date('Y') ?> SecHub - 网络安全工具集
</footer>
</div>
<!-- Toast 通知容器 -->
<div class="toast-container" id="toastContainer"></div>
<!-- ==================== Modals ==================== -->
<!-- 新增栏目 -->
<div class="modal-overlay" id="addSectionModal">
<div class="modal">
<h2>新增栏目</h2>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="add_section" value="1">
<div class="form-group">
<label>栏目名称</label>
<input type="text" name="section_name" required placeholder="例如:信息收集工具" autofocus>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-outline" onclick="hideModal('addSectionModal')">取消</button>
<button type="submit" class="btn btn-primary">创建</button>
</div>
</form>
</div>
</div>
<!-- 重命名栏目 -->
<div class="modal-overlay" id="renameSectionModal">
<div class="modal">
<h2>重命名栏目</h2>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="rename_section" value="1">
<input type="hidden" name="table_name" id="renameTableName">
<div class="form-group">
<label>新名称</label>
<input type="text" name="new_name" id="renameNewName" required autofocus>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-outline" onclick="hideModal('renameSectionModal')">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
<!-- 删除栏目确认 -->
<div class="modal-overlay" id="deleteSectionModal">
<div class="modal">
<h2>删除栏目</h2>
<div class="warning-box" id="deleteSectionInfo">
确定要删除栏目吗?此操作不可恢复!
</div>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="delete_section" value="1">
<input type="hidden" name="table_name" id="deleteSectionTable">
<input type="hidden" name="confirm" value="yes">
<div class="modal-actions">
<button type="button" class="btn btn-outline" onclick="hideModal('deleteSectionModal')">取消</button>
<button type="submit" class="btn btn-danger">确认删除</button>
</div>
</form>
</div>
</div>
<!-- 新增工具 -->
<div class="modal-overlay" id="addToolModal">
<div class="modal">
<h2>新增工具</h2>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="add_item" value="1">
<input type="hidden" name="table_name" id="addToolSection">
<div class="form-group">
<label>工具名称 <span style="color:var(--danger)">*</span></label>
<input type="text" name="name" required placeholder="例如Nmap">
</div>
<div class="form-group">
<label>URL</label>
<input type="url" name="url" placeholder="https://github.com/example/tool">
</div>
<div class="form-group">
<label>描述</label>
<textarea name="description" placeholder="工具功能简介"></textarea>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-outline" onclick="hideModal('addToolModal')">取消</button>
<button type="submit" class="btn btn-primary">添加</button>
</div>
</form>
</div>
</div>
<!-- 编辑工具 -->
<div class="modal-overlay" id="editToolModal">
<div class="modal">
<h2>编辑工具</h2>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="update_item" value="1">
<input type="hidden" name="table_name" id="editToolSection">
<input type="hidden" name="item_index" id="editToolIndex">
<div class="form-group">
<label>工具名称 <span style="color:var(--danger)">*</span></label>
<input type="text" name="name" id="editToolName" required>
</div>
<div class="form-group">
<label>URL</label>
<input type="url" name="url" id="editToolUrl">
</div>
<div class="form-group">
<label>描述</label>
<textarea name="description" id="editToolDesc"></textarea>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-outline" onclick="hideModal('editToolModal')">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
<!-- 删除工具确认 -->
<div class="modal-overlay" id="deleteToolModal">
<div class="modal">
<h2>删除工具</h2>
<p class="warning-box" id="deleteToolInfo">
确定要删除此工具吗?
</p>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="delete_item" value="1">
<input type="hidden" name="table_name" id="deleteToolSection">
<input type="hidden" name="item_index" id="deleteToolIndex">
<div class="modal-actions">
<button type="button" class="btn btn-outline" onclick="hideModal('deleteToolModal')">取消</button>
<button type="submit" class="btn btn-danger">确认删除</button>
</div>
</form>
</div>
</div>
<script>
// ======================== 暗黑模式 ========================
(function() {
const toggle = document.getElementById('themeToggle');
const icon = document.getElementById('themeIcon');
if (!toggle) return;
const current = localStorage.getItem('admin_theme') || 'light';
if (current === 'dark') {
document.body.classList.add('dark-mode');
icon.textContent = '\u2600\uFE0F';
}
toggle.addEventListener('click', function() {
document.body.classList.toggle('dark-mode');
const isDark = document.body.classList.contains('dark-mode');
icon.textContent = isDark ? '\u2600\uFE0F' : '\uD83C\uDF19';
localStorage.setItem('admin_theme', isDark ? 'dark' : 'light');
});
})();
// ======================== Toast 通知 ========================
function showToast(message, type) {
type = type || 'success';
const container = document.getElementById('toastContainer');
if (!container) return;
const toast = document.createElement('div');
const icons = { success: '\u2713', error: '\u26A0', warning: '\u26A0' };
toast.className = 'toast toast-' + type;
toast.innerHTML = '<span>' + (icons[type] || '') + '</span><span>' + message + '</span>';
container.appendChild(toast);
setTimeout(function() {
toast.classList.add('toast-out');
setTimeout(function() { toast.remove(); }, 300);
}, 4000);
}
// 页面加载时,如果存在页面级 Alert自动转为 Toast 并隐藏
(function() {
const alert = document.getElementById('pageAlert');
if (alert) {
setTimeout(function() {
alert.style.transition = 'opacity 0.5s';
alert.style.opacity = '0';
setTimeout(function() { alert.style.display = 'none'; }, 500);
}, 4000);
}
})();
// ======================== Tab切换 ========================
function switchTab(tab) {
// 更新URL
const url = new URL(window.location);
url.searchParams.set('tab', tab);
window.location.href = url.toString();
}
// ======================== 栏目选择 ========================
function selectSection(name) {
const url = new URL(window.location);
url.searchParams.set('tab', 'data');
url.searchParams.set('section', name);
window.location.href = url.toString();
}
// ======================== 模态框 ========================
function showModal(id) { document.getElementById(id).classList.add('show'); }
function hideModal(id) { document.getElementById(id).classList.remove('show'); }
// 点击遮罩关闭
document.querySelectorAll('.modal-overlay').forEach(el => {
el.addEventListener('click', function(e) {
if (e.target === this) this.classList.remove('show');
});
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay.show').forEach(el => el.classList.remove('show'));
}
});
// ======================== 重命名栏目 ========================
function showRenameModal(tableName, currentName) {
document.getElementById('renameTableName').value = tableName;
document.getElementById('renameNewName').value = currentName;
showModal('renameSectionModal');
}
// ======================== 删除栏目 ========================
function showDeleteSectionModal(tableName, title) {
document.getElementById('deleteSectionTable').value = tableName;
document.getElementById('deleteSectionInfo').innerHTML =
'<strong>&#9888; 确定要删除栏目 "' + title + '" 吗?</strong><br>此操作会删除JSON文件和数据库数据不可恢复';
showModal('deleteSectionModal');
}
// ======================== 新增工具 ========================
function showAddToolModal(section) {
document.getElementById('addToolSection').value = section;
document.getElementById('addToolModal').querySelector('form').reset();
document.getElementById('addToolModal').querySelector('[name="name"]').value = '';
document.getElementById('addToolModal').querySelector('[name="url"]').value = '';
document.getElementById('addToolModal').querySelector('[name="description"]').value = '';
showModal('addToolModal');
}
// ======================== 编辑工具 ========================
function showEditToolModal(section, index, name, url, desc) {
document.getElementById('editToolSection').value = section;
document.getElementById('editToolIndex').value = index;
document.getElementById('editToolName').value = name;
document.getElementById('editToolUrl').value = url;
document.getElementById('editToolDesc').value = desc;
showModal('editToolModal');
}
// ======================== 删除工具 ========================
function showDeleteToolModal(section, index, name) {
document.getElementById('deleteToolSection').value = section;
document.getElementById('deleteToolIndex').value = index;
document.getElementById('deleteToolInfo').innerHTML =
'<strong>&#9888; 确定要删除工具 "' + name + '" 吗?</strong><br>此操作不可恢复!';
showModal('deleteToolModal');
}
// ======================== 批量导入预览 ========================
function previewImport() {
const data = document.getElementById('importData').value.trim();
const format = document.getElementById('importFormat').value;
const preview = document.getElementById('importPreview');
if (!data) {
preview.innerHTML = '<div class="preview-empty">请输入数据后点击预览</div>';
return;
}
const lines = data.split('\n').filter(l => l.trim());
let html = '<h4 style="margin-bottom:8px;">解析预览(共 ' + lines.length + ' 行)</h4>';
html += '<div class="table-wrap"><table><thead><tr><th>名称</th><th>URL</th><th>描述</th></tr></thead><tbody>';
let validCount = 0;
lines.forEach((line, i) => {
let parts;
if (format === 'csv') {
parts = parseCSVLine(line);
} else {
parts = line.split('|').map(s => s.trim());
}
const name = parts[0] || '';
const url = parts[1] || '';
const desc = parts[2] || '';
if (name || url) validCount++;
html += '<tr>' +
'<td>' + escapeHtml(name) + '</td>' +
'<td style="word-break:break-all;font-size:0.9em;">' + escapeHtml(url) + '</td>' +
'<td style="color:var(--text-sec);font-size:0.9em;">' + escapeHtml(desc) + '</td>' +
'</tr>';
});
html += '</tbody></table></div>';
html += '<p style="margin-top:8px;color:var(--text-sec);">解析有效条目: ' + validCount + ' 行</p>';
preview.innerHTML = html;
}
// 简单CSV解析支持引号
function parseCSVLine(line) {
const result = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const c = line[i];
if (c === '"') {
if (inQuotes && i + 1 < line.length && line[i + 1] === '"') {
current += '"';
i++;
} else {
inQuotes = !inQuotes;
}
} else if (c === ',' && !inQuotes) {
result.push(current.trim());
current = '';
} else {
current += c;
}
}
result.push(current.trim());
return result;
}
function escapeHtml(text) {
const d = document.createElement('div');
d.textContent = text;
return d.innerHTML;
}
// ======================== 拖拽排序(保留原有功能) ========================
(function() {
const tbody = document.getElementById('sortable-tbody');
if (!tbody) return;
let draggedRow = null;
tbody.addEventListener('dragstart', function(e) {
draggedRow = e.target.closest('.draggable-row');
if (draggedRow) {
draggedRow.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', draggedRow.innerHTML);
}
});
tbody.addEventListener('dragend', function(e) {
if (draggedRow) {
draggedRow.classList.remove('dragging');
draggedRow = null;
updateOrderNumbers();
}
});
tbody.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const afterElement = getDragAfterElement(tbody, e.clientY);
if (afterElement == null) {
tbody.appendChild(draggedRow);
} else {
tbody.insertBefore(draggedRow, afterElement);
}
});
function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.draggable-row:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
function updateOrderNumbers() {
const rows = tbody.querySelectorAll('.draggable-row');
rows.forEach((row, index) => {
const orderSpan = row.querySelector('.order-num');
if (orderSpan) orderSpan.textContent = index + 1;
});
}
// 提交前生成排序字段
const form = tbody?.closest('form');
if (form) {
form.addEventListener('submit', function(e) {
if (!e.target.querySelector('[name="save_order"]')) return;
const orderInputs = document.getElementById('order-inputs');
orderInputs.innerHTML = '';
const rows = tbody.querySelectorAll('.draggable-row');
rows.forEach((row) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'orders[]';
input.value = row.getAttribute('data-filename');
orderInputs.appendChild(input);
});
});
}
})();
</script>
</body>
</html>
<?php
$database->close();
?>