feat: 实现 Token 认证系统
- 后端:Lua 实现 Token 验证、权限控制、限流、审计日志 - API:创建/列出/查看/更新/删除 Token - Web UI:Token 管理界面(列表、创建、详情、编辑、删除弹框) - 向后兼容:保留 X-Password 超级管理员认证 - 安全增强:失败次数限制、过期检测、双因素验证(敏感操作) 需求文档:TOKEN_AUTH_REQUIREMENT.md
This commit is contained in:
1 parent
1567a3dea3
commit
2cf807b59c
4 files changed
+1870
-15
No files matched your search
+633
-2
@@ -479,6 +479,40 @@
|
||||
.method-get { background: #28a745; color: white; }
|
||||
.method-post { background: #007bff; color: white; }
|
||||
.method-delete { background: #dc3545; color: white; }
|
||||
|
||||
/* Navigation tabs */
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: 0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
padding: 12px 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.nav-tab:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
color: #667eea;
|
||||
border-bottom-color: #667eea;
|
||||
}
|
||||
|
||||
#token-page {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -487,8 +521,16 @@
|
||||
<div class="header">
|
||||
<h1>🔄 通用数据同步服务</h1>
|
||||
<p class="subtitle">Cookie / Token / Credential 管理面板</p>
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="nav-tabs" style="margin-top: 20px;">
|
||||
<button class="nav-tab active" onclick="switchPage('data')">📋 数据管理</button>
|
||||
<button class="nav-tab" onclick="switchPage('tokens')">🔑 Token 管理</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Management Page -->
|
||||
<div id="data-page">
|
||||
<!-- Stats -->
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
@@ -555,6 +597,55 @@
|
||||
<code>/api/data/copy/:tag</code> - 获取复制配置
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Data Management Page -->
|
||||
|
||||
<!-- Token Management Page -->
|
||||
<div id="token-page" style="display: none;">
|
||||
<!-- Stats -->
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🔑</div>
|
||||
<div class="stat-label">总 Token</div>
|
||||
<div class="stat-value" id="token-total-count">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-label">启用</div>
|
||||
<div class="stat-value" id="token-active-count">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🚫</div>
|
||||
<div class="stat-label">禁用</div>
|
||||
<div class="stat-value" id="token-disabled-count">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">⏰</div>
|
||||
<div class="stat-label">已过期</div>
|
||||
<div class="stat-value" id="token-expired-count">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token List -->
|
||||
<div class="data-list">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 style="margin: 0;">🔑 Token 列表</h2>
|
||||
<button class="btn btn-primary" onclick="openCreateTokenModal()">+ 创建 Token</button>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="search-box">
|
||||
<input type="text" class="search-input" id="token-search-input" placeholder="🔍 搜索 Token 名称..." oninput="filterTokens()">
|
||||
<span class="search-icon">🔍</span>
|
||||
</div>
|
||||
|
||||
<div id="token-container">
|
||||
<div class="loading">正在加载...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Token Management Page -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Refresh Button -->
|
||||
@@ -594,6 +685,172 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Token Modal -->
|
||||
<div class="modal-overlay" id="create-token-modal">
|
||||
<div class="modal" style="width: 500px;">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">创建 Token</div>
|
||||
<button class="modal-close" onclick="closeModal('create-token-modal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500;">Token 名称 *</label>
|
||||
<input type="text" id="token-name" placeholder="如:生产服务器A" style="width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 6px; font-size: 14px;">
|
||||
<p style="color: #999; font-size: 12px; margin-top: 5px;">用于识别客户端</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500;">权限范围 *</label>
|
||||
<label style="display: block; margin-bottom: 8px; cursor: pointer;">
|
||||
<input type="radio" name="token-permission" value="readonly" checked> 只读(仅查询数据)
|
||||
</label>
|
||||
<label style="display: block; cursor: pointer;">
|
||||
<input type="radio" name="token-permission" value="readwrite"> 读写(可上传、删除数据)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500;">过期设置 *</label>
|
||||
<label style="display: block; margin-bottom: 8px; cursor: pointer;">
|
||||
<input type="radio" name="token-expiry" value="0" checked> 永久有效
|
||||
</label>
|
||||
<label style="display: block; margin-bottom: 8px; cursor: pointer;">
|
||||
<input type="radio" name="token-expiry" value="2592000"> 30天后过期
|
||||
</label>
|
||||
<label style="display: block; cursor: pointer;">
|
||||
<input type="radio" name="token-expiry" value="7776000"> 90天后过期
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal('create-token-modal')">取消</button>
|
||||
<button class="btn btn-primary" onclick="submitCreateToken()">创建</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token Details Modal -->
|
||||
<div class="modal-overlay" id="token-details-modal">
|
||||
<div class="modal" style="width: 900px;">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">Token 详情</div>
|
||||
<button class="modal-close" onclick="closeModal('token-details-modal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
|
||||
<div style="margin-bottom: 25px;">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500;">Token 值</label>
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<input type="password" id="token-value-display" readonly style="flex: 1; padding: 10px; border: 2px solid #e0e0e0; border-radius: 6px; font-family: monospace; background: #f8f9fa;">
|
||||
<button class="btn btn-secondary" onclick="toggleTokenVisibility()" id="toggle-token-btn">👁 显示</button>
|
||||
<button class="btn btn-success" onclick="copyTokenValue()">📋 复制</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 25px;">
|
||||
<h3 style="margin-bottom: 10px;">基本信息</h3>
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px;">
|
||||
<p style="margin-bottom: 8px;"><strong>名称:</strong> <span id="token-detail-name">-</span></p>
|
||||
<p style="margin-bottom: 8px;"><strong>权限:</strong> <span id="token-detail-permission">-</span></p>
|
||||
<p style="margin-bottom: 8px;"><strong>状态:</strong> <span id="token-detail-status">-</span></p>
|
||||
<p style="margin-bottom: 8px;"><strong>创建时间:</strong> <span id="token-detail-created">-</span></p>
|
||||
<p><strong>过期时间:</strong> <span id="token-detail-expires">-</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 25px;">
|
||||
<h3 style="margin-bottom: 10px;">使用统计</h3>
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px;">
|
||||
<p style="margin-bottom: 8px;"><strong>总请求数:</strong> <span id="token-detail-requests">-</span></p>
|
||||
<p style="margin-bottom: 8px;"><strong>最后使用:</strong> <span id="token-detail-last-used">-</span></p>
|
||||
<p style="margin-bottom: 8px;"><strong>最后 IP:</strong> <span id="token-detail-last-ip">-</span></p>
|
||||
<p><strong>失败次数:</strong> <span id="token-detail-failed">-</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style="margin-bottom: 10px;">访问日志(最近 20 条)</h3>
|
||||
<div style="overflow-x: auto;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
|
||||
<thead>
|
||||
<tr style="background: #f8f9fa; border-bottom: 2px solid #e0e0e0;">
|
||||
<th style="padding: 10px; text-align: left;">时间</th>
|
||||
<th style="padding: 10px; text-align: left;">IP</th>
|
||||
<th style="padding: 10px; text-align: left;">方法</th>
|
||||
<th style="padding: 10px; text-align: left;">路径</th>
|
||||
<th style="padding: 10px; text-align: left;">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="token-access-log">
|
||||
<tr><td colspan="5" style="padding: 20px; text-align: center; color: #999;">暂无访问记录</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal('token-details-modal')">关闭</button>
|
||||
<button class="btn btn-primary" onclick="openEditTokenModal()">修改</button>
|
||||
<button class="btn btn-danger" onclick="confirmDeleteToken()">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Token Modal -->
|
||||
<div class="modal-overlay" id="edit-token-modal">
|
||||
<div class="modal" style="width: 500px;">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">编辑 Token</div>
|
||||
<button class="modal-close" onclick="closeModal('edit-token-modal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500;">Token 名称 *</label>
|
||||
<input type="text" id="edit-token-name" style="width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 6px; font-size: 14px;">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500;">状态</label>
|
||||
<label style="display: block; margin-bottom: 8px; cursor: pointer;">
|
||||
<input type="radio" name="edit-token-status" value="active" checked> 启用
|
||||
</label>
|
||||
<label style="display: block; cursor: pointer;">
|
||||
<input type="radio" name="edit-token-status" value="disabled"> 禁用
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal('edit-token-modal')">取消</button>
|
||||
<button class="btn btn-primary" onclick="submitEditToken()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Token Confirmation Modal -->
|
||||
<div class="modal-overlay confirm-modal" id="delete-token-modal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">⚠️ 删除 Token</div>
|
||||
<button class="modal-close" onclick="closeModal('delete-token-modal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="confirm-message" style="margin-bottom: 15px;">确认要删除以下 Token 吗?</p>
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
|
||||
<p style="margin-bottom: 8px;"><strong>名称:</strong> <span id="delete-token-name">-</span></p>
|
||||
<p><strong>Token:</strong> <span id="delete-token-value">-</span></p>
|
||||
</div>
|
||||
<p style="color: #dc3545; margin-bottom: 15px;">⚠️ 此操作不可恢复,使用此 Token 的客户端将无法访问</p>
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500;">输入主密码以确认:</label>
|
||||
<input type="password" id="delete-token-password" placeholder="admin123123123" style="width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 6px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal('delete-token-modal')">取消</button>
|
||||
<button class="btn btn-danger" onclick="submitDeleteToken()">确认删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentViewData = null;
|
||||
let deleteCallback = null;
|
||||
@@ -993,10 +1250,384 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// ============= Token Management Functions =============
|
||||
let allTokens = [];
|
||||
let currentToken = null;
|
||||
let tokenVisible = false;
|
||||
|
||||
// Page switching
|
||||
function switchPage(page) {
|
||||
document.querySelectorAll('.nav-tab').forEach(tab => tab.classList.remove('active'));
|
||||
|
||||
if (page === 'data') {
|
||||
document.querySelector('[onclick*="data"]').classList.add('active');
|
||||
document.getElementById('data-page').style.display = 'block';
|
||||
document.getElementById('token-page').style.display = 'none';
|
||||
loadData();
|
||||
} else if (page === 'tokens') {
|
||||
document.querySelector('[onclick*="tokens"]').classList.add('active');
|
||||
document.getElementById('data-page').style.display = 'none';
|
||||
document.getElementById('token-page').style.display = 'block';
|
||||
loadTokens();
|
||||
}
|
||||
}
|
||||
|
||||
// Load tokens
|
||||
async function loadTokens() {
|
||||
try {
|
||||
const response = await fetch('/api/tokens', {
|
||||
headers: { 'X-Password': 'admin123123123' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load tokens');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
allTokens = data.tokens || [];
|
||||
renderTokenList(allTokens);
|
||||
updateTokenStats(allTokens);
|
||||
} catch (error) {
|
||||
console.error('Load tokens error:', error);
|
||||
showToast('加载 Token 失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Update token stats
|
||||
function updateTokenStats(tokens) {
|
||||
let activeCount = 0;
|
||||
let disabledCount = 0;
|
||||
let expiredCount = 0;
|
||||
|
||||
tokens.forEach(token => {
|
||||
if (token.status === 'active') activeCount++;
|
||||
else if (token.status === 'disabled') disabledCount++;
|
||||
else if (token.status === 'expired') expiredCount++;
|
||||
});
|
||||
|
||||
document.getElementById('token-total-count').textContent = tokens.length;
|
||||
document.getElementById('token-active-count').textContent = activeCount;
|
||||
document.getElementById('token-disabled-count').textContent = disabledCount;
|
||||
document.getElementById('token-expired-count').textContent = expiredCount;
|
||||
}
|
||||
|
||||
// Render token list
|
||||
function renderTokenList(tokens) {
|
||||
const container = document.getElementById('token-container');
|
||||
|
||||
if (tokens.length === 0) {
|
||||
container.innerHTML = '<div class="empty">📭 暂无 Token</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
tokens.forEach(token => {
|
||||
const statusClass = token.status === 'active' ? 'status-valid' :
|
||||
token.status === 'disabled' ? 'status-expired' : 'status-warning';
|
||||
const statusText = token.status === 'active' ? '启用' :
|
||||
token.status === 'disabled' ? '禁用' : '已过期';
|
||||
const permissionText = token.permission === 'readonly' ? '只读' : '读写';
|
||||
|
||||
html += `
|
||||
<div class="data-item">
|
||||
<div class="data-info">
|
||||
<div class="data-tag">
|
||||
${token.name}
|
||||
<span class="status-badge ${statusClass}">${statusText}</span>
|
||||
</div>
|
||||
<div class="data-meta">
|
||||
Token: ${token.token} | 权限: ${permissionText} |
|
||||
创建: ${token.created_at} |
|
||||
请求数: ${token.stats.total_requests || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary" onclick='viewTokenDetails("${token.token_full}")'>详情</button>
|
||||
${token.status === 'active'
|
||||
? `<button class="btn btn-secondary" onclick='toggleTokenStatus("${token.token_full}", "disabled")'>禁用</button>`
|
||||
: `<button class="btn btn-success" onclick='toggleTokenStatus("${token.token_full}", "active")'>启用</button>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Filter tokens
|
||||
function filterTokens() {
|
||||
const searchTerm = document.getElementById('token-search-input').value.trim().toLowerCase();
|
||||
|
||||
if (!searchTerm) {
|
||||
renderTokenList(allTokens);
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = allTokens.filter(token =>
|
||||
token.name.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
|
||||
renderTokenList(filtered);
|
||||
}
|
||||
|
||||
// Open create token modal
|
||||
function openCreateTokenModal() {
|
||||
document.getElementById('token-name').value = '';
|
||||
document.querySelector('input[name="token-permission"][value="readonly"]').checked = true;
|
||||
document.querySelector('input[name="token-expiry"][value="0"]').checked = true;
|
||||
openModal('create-token-modal');
|
||||
}
|
||||
|
||||
// Submit create token
|
||||
async function submitCreateToken() {
|
||||
const name = document.getElementById('token-name').value.trim();
|
||||
const permission = document.querySelector('input[name="token-permission"]:checked').value;
|
||||
const expiresIn = parseInt(document.querySelector('input[name="token-expiry"]:checked').value);
|
||||
|
||||
if (!name) {
|
||||
showToast('请输入 Token 名称', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/tokens', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Password': 'admin123123123',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
permission: permission,
|
||||
expires_in: expiresIn || null
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showToast('✅ Token 创建成功');
|
||||
closeModal('create-token-modal');
|
||||
await loadTokens();
|
||||
viewTokenDetails(result.token);
|
||||
} else {
|
||||
showToast('❌ ' + (result.error || 'Token 创建失败'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('❌ 请求失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// View token details
|
||||
async function viewTokenDetails(token) {
|
||||
try {
|
||||
const response = await fetch(`/api/tokens/${token}`, {
|
||||
headers: { 'X-Password': 'admin123123123' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load token details');
|
||||
}
|
||||
|
||||
const tokenData = await response.json();
|
||||
currentToken = tokenData;
|
||||
tokenVisible = false;
|
||||
|
||||
document.getElementById('token-value-display').value = tokenData.token;
|
||||
document.getElementById('token-value-display').type = 'password';
|
||||
document.getElementById('toggle-token-btn').textContent = '👁 显示';
|
||||
|
||||
document.getElementById('token-detail-name').textContent = tokenData.name;
|
||||
document.getElementById('token-detail-permission').textContent =
|
||||
tokenData.permission === 'readonly' ? '只读' : '读写';
|
||||
document.getElementById('token-detail-status').textContent =
|
||||
tokenData.status === 'active' ? '🟢 启用中' :
|
||||
tokenData.status === 'disabled' ? '🔴 已禁用' : '⏰ 已过期';
|
||||
document.getElementById('token-detail-created').textContent = tokenData.created_at;
|
||||
document.getElementById('token-detail-expires').textContent = tokenData.expires_at || '永久有效';
|
||||
|
||||
const stats = tokenData.stats || {};
|
||||
document.getElementById('token-detail-requests').textContent = stats.total_requests || 0;
|
||||
document.getElementById('token-detail-last-used').textContent = stats.last_used_at || '从未使用';
|
||||
document.getElementById('token-detail-last-ip').textContent = stats.last_ip || '-';
|
||||
document.getElementById('token-detail-failed').textContent = stats.failed_attempts || 0;
|
||||
|
||||
// Render access log
|
||||
const logBody = document.getElementById('token-access-log');
|
||||
if (!tokenData.access_log || tokenData.access_log.length === 0) {
|
||||
logBody.innerHTML = '<tr><td colspan="5" style="padding: 20px; text-align: center; color: #999;">暂无访问记录</td></tr>';
|
||||
} else {
|
||||
logBody.innerHTML = tokenData.access_log.map(log => `
|
||||
<tr style="border-bottom: 1px solid #e0e0e0;">
|
||||
<td style="padding: 10px;">${log.timestamp}</td>
|
||||
<td style="padding: 10px;">${log.ip}</td>
|
||||
<td style="padding: 10px;">${log.method}</td>
|
||||
<td style="padding: 10px; font-size: 12px;">${log.path}</td>
|
||||
<td style="padding: 10px;">${log.status}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
openModal('token-details-modal');
|
||||
} catch (error) {
|
||||
showToast('加载 Token 详情失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle token visibility
|
||||
function toggleTokenVisibility() {
|
||||
const input = document.getElementById('token-value-display');
|
||||
const btn = document.getElementById('toggle-token-btn');
|
||||
|
||||
if (tokenVisible) {
|
||||
input.type = 'password';
|
||||
btn.textContent = '👁 显示';
|
||||
tokenVisible = false;
|
||||
} else {
|
||||
input.type = 'text';
|
||||
btn.textContent = '🙈 隐藏';
|
||||
tokenVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy token value
|
||||
function copyTokenValue() {
|
||||
const tokenValue = document.getElementById('token-value-display').value;
|
||||
copyToClipboard(tokenValue).then(success => {
|
||||
if (success) {
|
||||
showToast('Token 已复制到剪贴板');
|
||||
} else {
|
||||
showToast('复制失败,请手动选择复制', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Open edit token modal
|
||||
function openEditTokenModal() {
|
||||
if (!currentToken) return;
|
||||
|
||||
document.getElementById('edit-token-name').value = currentToken.name;
|
||||
document.querySelector(`input[name="edit-token-status"][value="${currentToken.status}"]`).checked = true;
|
||||
|
||||
closeModal('token-details-modal');
|
||||
openModal('edit-token-modal');
|
||||
}
|
||||
|
||||
// Submit edit token
|
||||
async function submitEditToken() {
|
||||
if (!currentToken) return;
|
||||
|
||||
const name = document.getElementById('edit-token-name').value.trim();
|
||||
const status = document.querySelector('input[name="edit-token-status"]:checked').value;
|
||||
|
||||
if (!name) {
|
||||
showToast('请输入 Token 名称', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tokens/${currentToken.token}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'X-Password': 'admin123123123',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ name, status })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showToast('✅ Token 已更新');
|
||||
closeModal('edit-token-modal');
|
||||
await loadTokens();
|
||||
viewTokenDetails(currentToken.token);
|
||||
} else {
|
||||
showToast('❌ ' + (result.error || '更新失败'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('❌ 请求失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle token status
|
||||
async function toggleTokenStatus(token, newStatus) {
|
||||
try {
|
||||
const response = await fetch(`/api/tokens/${token}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'X-Password': 'admin123123123',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ status: newStatus })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast(newStatus === 'active' ? '✅ Token 已启用' : '✅ Token 已禁用');
|
||||
loadTokens();
|
||||
} else {
|
||||
showToast('❌ 操作失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('❌ 请求失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm delete token
|
||||
function confirmDeleteToken() {
|
||||
if (!currentToken) return;
|
||||
|
||||
document.getElementById('delete-token-name').textContent = currentToken.name;
|
||||
document.getElementById('delete-token-value').textContent = currentToken.token.substring(0, 12) + '...';
|
||||
document.getElementById('delete-token-password').value = '';
|
||||
|
||||
closeModal('token-details-modal');
|
||||
openModal('delete-token-modal');
|
||||
}
|
||||
|
||||
// Submit delete token
|
||||
async function submitDeleteToken() {
|
||||
if (!currentToken) return;
|
||||
|
||||
const password = document.getElementById('delete-token-password').value;
|
||||
|
||||
if (!password) {
|
||||
showToast('请输入主密码', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tokens/${currentToken.token}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-Password': 'admin123123123',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ confirm_password: password })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showToast('✅ Token 已删除');
|
||||
closeModal('delete-token-modal');
|
||||
currentToken = null;
|
||||
loadTokens();
|
||||
} else {
|
||||
showToast('❌ ' + (result.error || '删除失败'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('❌ 请求失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============= End Token Management Functions =============
|
||||
|
||||
// 页面加载时获取数据
|
||||
loadData();
|
||||
|
||||
|
||||
// 每30秒自动刷新
|
||||
setInterval(loadData, 30000);
|
||||
</script>
|
||||
|
||||
Reference in new issue
Block a user