Files
registry/public/admin.html
T
jif2.zhang be2e4d9cdf
build-and-deploy / build-and-deploy (push) Successful in 42s
feat: MCP 端点鉴权改用后台 Token 管理,废弃 mcp_kv 配置
- tokenService 支持 mcpEnabled 标记,新增 getMcpTokenValues 与迁移逻辑
- /mcp 鉴权改为校验 Token 表中标记为 MCP 访问的生效令牌
- 启动时自动把旧 mcp_token 配置迁成一条 MCP Token,保证旧客户端不中断
- admin 页面 Token 表新增 MCP 访问列与生成/编辑开关
- 新增 tokenService 测试(58 项全部通过)
2026-09-20 21:45:18 +08:00

685 lines
31 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统管理 - Registry 配置管理中心</title>
<link rel="stylesheet" href="/css/style.css">
<style>
.titlebar-right { margin-left: auto; display: flex; align-items: center; gap: 14px; font-size: 12px; }
.titlebar-user { color: #e8f1fb; display: flex; align-items: center; gap: 6px; }
.role-badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; background: rgba(255,255,255,0.18); border: 1px solid rgba(255,255,255,0.35); }
.role-badge.admin { background: #ffb900; border-color: #ffd666; color: #4a3200; font-weight: 600; }
.titlebar-link { color: #cfe3f7; text-decoration: none; cursor: pointer; padding-left: 14px; border-left: 1px solid rgba(255,255,255,0.3); }
.titlebar-link:hover { color: #fff; text-decoration: underline; }
.admin-container { flex: 1; display: flex; overflow: hidden; }
.admin-nav { width: 200px; background: #fff; border-right: 1px solid #c0c0c0; padding: 10px 0; flex-shrink: 0; }
.admin-nav-title { padding: 4px 18px 10px; font-size: 11px; color: #999; letter-spacing: 1px; }
.admin-nav-item { display: flex; align-items: center; gap: 8px; height: 38px; padding: 0 18px; cursor: pointer; color: #333; font-size: 13px; border-left: 3px solid transparent; }
.admin-nav-item:hover { background: #e5f1fb; }
.admin-nav-item.active { background: #cce8ff; color: #003a70; font-weight: 600; border-left-color: #0078d4; }
.admin-content { flex: 1; overflow: auto; padding: 18px; }
.admin-panel { background: #fff; border: 1px solid #e0e0e0; border-radius: 6px; overflow: hidden; }
.admin-panel-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: #f5f5f5; border-bottom: 1px solid #e0e0e0; flex-wrap: wrap; gap: 8px; }
.admin-panel-header h2 { font-size: 14px; color: #003a70; }
.admin-panel-header .panel-sub { font-weight: normal; font-size: 11px; color: #999; margin-left: 8px; }
.admin-toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.search-input { height: 30px; width: 200px; padding: 0 10px; border: 1px solid #adadad; border-radius: 3px; font-size: 13px; font-family: inherit; }
.filter-select { height: 30px; padding: 0 8px; border: 1px solid #adadad; border-radius: 3px; background: #fff; font-size: 12px; }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
.data-table { width: 100%; border-collapse: collapse; }
.data-table th { background: #f0f0f0; border-bottom: 1px solid #c0c0c0; border-right: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; font-weight: 600; color: #555; font-size: 12px; white-space: nowrap; }
.data-table td { padding: 8px 12px; border-bottom: 1px solid #f0f0f0; font-size: 12px; vertical-align: middle; }
.data-table tr:hover td { background: #f8fbff; }
.data-table .mono { font-family: Consolas, monospace; color: #0451a5; }
.role-tag { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; border: 1px solid transparent; }
.role-tag.admin { background: #fff3e0; color: #b45309; border-color: #f0c98f; }
.role-tag.user { background: #e5f1fb; color: #0078d4; border-color: #bcdcf7; }
.status-badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; border: 1px solid transparent; }
.badge-active { background: #e8f5e9; color: #2e7d32; border-color: #b5dfb8; }
.badge-disabled { background: #fdecea; color: #c62828; border-color: #f2b8b5; }
.badge-expired { background: #f0f0f0; color: #777; border-color: #d5d5d5; }
.dim-chip { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; background: #eef4fb; color: #0a4a86; border: 1px solid #d0dced; }
.token-reveal { display: flex; align-items: center; gap: 6px; }
.token-value { font-family: Consolas, monospace; color: #1e1e1e; letter-spacing: 1px; }
.token-mask { color: #999; letter-spacing: 2px; }
.token-btn { border: 1px solid #adadad; background: #fff; border-radius: 3px; padding: 1px 6px; font-size: 11px; cursor: pointer; white-space: nowrap; }
.token-btn:hover { background: #e5f1fb; border-color: #5b9bd5; }
.form-row { display: flex; gap: 12px; }
.form-half { flex: 1; }
.form-group label.hint { font-weight: normal; color: #999; font-size: 11px; }
.hint { font-weight: normal; color: #999; font-size: 11px; }
</style>
</head>
<body>
<header class="titlebar">
<span class="app-title">📋 Registry 配置管理中心</span>
<span class="current-project">系统管理</span>
<div class="titlebar-right">
<span class="titlebar-user">
👤 <span id="user-name"></span>
<span class="role-badge admin">管理员</span>
</span>
<a class="titlebar-link" href="/projects.html">← 返回项目列表</a>
<a class="titlebar-link" id="logout-link" href="#">退出登录</a>
</div>
</header>
<div class="admin-container">
<aside class="admin-nav">
<div class="admin-nav-title">系统设置</div>
<div class="admin-nav-item active" data-tab="users" onclick="switchTab('users')">👥 用户管理</div>
<div class="admin-nav-item" data-tab="tokens" onclick="switchTab('tokens')">🔑 Token 管理</div>
</aside>
<div class="admin-content">
<!-- 用户管理 -->
<div class="tab-panel active" id="panel-users">
<div class="admin-panel">
<div class="admin-panel-header">
<h2>用户管理 <span class="panel-sub">管理系统账号与角色权限</span></h2>
<div class="admin-toolbar">
<input type="text" class="search-input" id="user-search" placeholder="🔍 搜索用户名">
<button class="btn btn-primary" style="height:30px" id="btn-add-user">➕ 新增用户</button>
</div>
</div>
<div style="overflow:auto">
<table class="data-table" id="user-table">
<thead>
<tr>
<th>用户名</th><th>显示名称</th><th>角色</th><th>状态</th>
<th>创建时间</th><th>最后登录</th><th>操作</th>
</tr>
</thead>
<tbody id="user-tbody"></tbody>
</table>
</div>
</div>
</div>
<!-- Token 管理 -->
<div class="tab-panel" id="panel-tokens">
<div class="admin-panel">
<div class="admin-panel-header">
<h2>Token 管理 <span class="panel-sub">MCP / API 访问令牌</span></h2>
<div class="admin-toolbar">
<input type="text" class="search-input" id="token-search" placeholder="🔍 搜索 Token 名称">
<button class="btn btn-primary" style="height:30px" id="btn-add-token">🔑 生成 Token</button>
</div>
</div>
<div style="overflow:auto">
<table class="data-table" id="token-table">
<thead>
<tr>
<th>名称</th><th>关联项目</th><th>权限</th><th>MCP 访问</th><th>Token</th>
<th>状态</th><th>过期时间</th><th>创建时间</th><th>操作</th>
</tr>
</thead>
<tbody id="token-tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<footer class="statusbar">
<div class="statusbar-left">
<span id="status-text">系统管理 · 用户管理</span>
<span id="status-user"></span>
</div>
<span class="statusbar-version" id="version-text"></span>
</footer>
<!-- 新增/编辑用户模态框 -->
<div class="modal-overlay" id="modal-user" style="display:none">
<div class="modal">
<div class="modal-header">
<h3 id="modal-user-title">新增用户</h3>
<button class="modal-close" onclick="closeModal('modal-user')">✕</button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-user-id">
<div class="form-group">
<label>用户名 <span class="required">*</span></label>
<input type="text" id="input-username" placeholder="登录账号,例如 zhangsan">
</div>
<div class="form-group">
<label>显示名称</label>
<input type="text" id="input-display-name" placeholder="用于界面展示的名称">
</div>
<div class="form-row">
<div class="form-group form-half">
<label>角色</label>
<select id="input-role"><option value="user">普通用户</option><option value="admin">管理员</option></select>
</div>
<div class="form-group form-half">
<label>状态</label>
<select id="input-enabled"><option value="true">启用</option><option value="false">停用</option></select>
</div>
</div>
<div class="form-group" id="password-group">
<label>密码 <span class="required">*</span></label>
<input type="password" id="input-password" placeholder="首次登录后建议修改">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('modal-user')">取消</button>
<button class="btn btn-primary" id="btn-save-user">保存</button>
</div>
</div>
</div>
<!-- 重置密码模态框 -->
<div class="modal-overlay" id="modal-reset-pw" style="display:none">
<div class="modal" style="width:360px">
<div class="modal-header">
<h3>重置密码</h3>
<button class="modal-close" onclick="closeModal('modal-reset-pw')">✕</button>
</div>
<div class="modal-body">
<input type="hidden" id="reset-user-id">
<div class="form-group">
<label>新密码 <span class="required">*</span></label>
<input type="password" id="input-new-password" placeholder="请输入新密码">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('modal-reset-pw')">取消</button>
<button class="btn btn-primary" id="btn-save-reset-pw">确认重置</button>
</div>
</div>
</div>
<!-- 生成/编辑 Token 模态框 -->
<div class="modal-overlay" id="modal-token" style="display:none">
<div class="modal">
<div class="modal-header">
<h3 id="modal-token-title">生成 Token</h3>
<button class="modal-close" onclick="closeModal('modal-token')">✕</button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-token-id">
<div class="form-group">
<label>名称 <span class="required">*</span></label>
<input type="text" id="input-token-name" placeholder="例如:CI 自动发布">
</div>
<div class="form-group">
<label>关联项目</label>
<select id="input-token-project"><option value="all">全部项目</option></select>
</div>
<div class="form-row">
<div class="form-group form-half">
<label>权限范围</label>
<select id="input-token-permission"><option value="readonly">只读</option><option value="readwrite">读写</option></select>
</div>
<div class="form-group form-half">
<label>过期时间</label>
<select id="input-token-expiry">
<option value="30">30 天</option>
<option value="90" selected>90 天</option>
<option value="365">1 年</option>
<option value="0">永不过期</option>
</select>
</div>
</div>
<div class="form-group">
<label class="checkbox-label" style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="input-token-mcp" style="width:16px;height:16px">
<span>作为 MCP 访问令牌(用于 /mcp 端点鉴权)</span>
</label>
</div>
<p class="hint">生成后 Token 明文仅展示一次,请及时复制并妥善保管。</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('modal-token')">取消</button>
<button class="btn btn-primary" id="btn-save-token">生成</button>
</div>
</div>
</div>
<!-- Token 明文展示模态框 -->
<div class="modal-overlay" id="modal-token-reveal" style="display:none">
<div class="modal" style="width:480px">
<div class="modal-header">
<h3>Token 已生成</h3>
<button class="modal-close" onclick="closeModal('modal-token-reveal')">✕</button>
</div>
<div class="modal-body">
<p style="margin-bottom:12px;color:#666;font-size:12px">请立即复制保存,关闭后将无法再查看完整 Token。</p>
<div style="display:flex;gap:8px;align-items:center;background:#f5f5f5;padding:10px 12px;border-radius:4px;border:1px solid #e0e0e0">
<code id="reveal-token-value" style="flex:1;word-break:break-all;font-family:Consolas,monospace;font-size:13px;color:#0451a5"></code>
<button class="token-btn" id="btn-copy-reveal">复制</button>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeModal('modal-token-reveal')">我已保存</button>
</div>
</div>
</div>
<!-- 确认删除模态框 -->
<div class="modal-overlay" id="modal-confirm" style="display:none">
<div class="modal" style="width:360px">
<div class="modal-header">
<h3>确认操作</h3>
<button class="modal-close" onclick="closeModal('modal-confirm')">✕</button>
</div>
<div class="modal-body"><p id="confirm-message"></p></div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('modal-confirm')">取消</button>
<button class="btn btn-danger" id="btn-confirm-ok">确认</button>
</div>
</div>
</div>
<script src="/js/api.js"></script>
<script src="/js/version.js"></script>
<script>
var currentUser = null;
var confirmCallback = null;
function toast(message, type) {
var el = document.createElement("div");
el.className = "toast toast-" + (type || "info");
el.textContent = message;
document.body.appendChild(el);
setTimeout(function () { el.remove(); }, 2500);
}
function openModal(id) { document.getElementById(id).style.display = "flex"; }
window.closeModal = function (id) { document.getElementById(id).style.display = "none"; };
function openConfirm(message, callback) {
document.getElementById("confirm-message").textContent = message;
confirmCallback = callback;
openModal("modal-confirm");
}
document.getElementById("btn-confirm-ok").addEventListener("click", function () {
closeModal("modal-confirm");
if (confirmCallback) confirmCallback();
confirmCallback = null;
});
function esc(s) { var d = document.createElement("div"); d.textContent = s; return d.innerHTML; }
function fmtDate(iso) {
if (!iso) return "—";
var d = new Date(iso);
var pad = function (n) { return n < 10 ? "0" + n : n; };
return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + " " + pad(d.getHours()) + ":" + pad(d.getMinutes());
}
window.switchTab = function (tab) {
document.querySelectorAll(".admin-nav-item").forEach(function (el) { el.classList.toggle("active", el.dataset.tab === tab); });
document.querySelectorAll(".tab-panel").forEach(function (el) { el.classList.remove("active"); });
document.getElementById("panel-" + tab).classList.add("active");
document.getElementById("status-text").textContent = "系统管理 · " + (tab === "users" ? "用户管理" : "Token 管理");
}
// ========== 用户管理 ==========
var usersCache = [];
async function loadUsers() {
try {
usersCache = await API.getUsers();
renderUsers(usersCache);
} catch (err) {
toast("加载用户失败: " + err.message, "error");
}
}
function renderUsers(users) {
var tbody = document.getElementById("user-tbody");
if (!users.length) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#999;padding:30px">暂无用户</td></tr>';
return;
}
tbody.innerHTML = users.map(function (u) {
return "<tr>" +
'<td class="mono">' + esc(u.username) + "</td>" +
"<td>" + esc(u.displayName || "") + "</td>" +
'<td><span class="role-tag ' + u.role + '">' + (u.role === "admin" ? "管理员" : "普通用户") + "</span></td>" +
'<td><span class="status-badge ' + (u.enabled ? "badge-active" : "badge-disabled") + '">' + (u.enabled ? "启用" : "停用") + "</span></td>" +
"<td>" + fmtDate(u.createdAt) + "</td>" +
"<td>" + fmtDate(u.lastLoginAt) + "</td>" +
'<td style="white-space:nowrap">' +
'<button class="row-btn" onclick="editUserById(\'' + u.id + '\')">编辑</button> ' +
'<button class="row-btn" onclick="resetPassword(\'' + u.id + '\')">重置密码</button> ' +
(u.enabled
? '<button class="row-btn danger" onclick="toggleUser(\'' + u.id + "', false)\">停用</button>"
: '<button class="row-btn" onclick="toggleUser(\'' + u.id + "', true)\">启用</button>") +
(u.username !== "admin"
? ' <button class="row-btn danger" onclick="confirmDeleteUser(\'' + u.id + '\')">删除</button>'
: "") +
"</td>" +
"</tr>";
}).join("");
}
document.getElementById("user-search").addEventListener("input", function () {
var kw = this.value.trim().toLowerCase();
document.querySelectorAll("#user-tbody tr").forEach(function (tr) {
tr.style.display = kw && tr.textContent.toLowerCase().indexOf(kw) === -1 ? "none" : "";
});
});
document.getElementById("btn-add-user").addEventListener("click", function () {
document.getElementById("modal-user-title").textContent = "新增用户";
document.getElementById("edit-user-id").value = "";
document.getElementById("input-username").value = "";
document.getElementById("input-username").disabled = false;
document.getElementById("input-display-name").value = "";
document.getElementById("input-role").value = "user";
document.getElementById("input-enabled").value = "true";
document.getElementById("input-password").value = "";
document.getElementById("password-group").style.display = "";
openModal("modal-user");
});
window.editUserById = function (id) {
var u = usersCache.find(function (x) { return x.id === id; });
if (!u) return;
document.getElementById("modal-user-title").textContent = "编辑用户";
document.getElementById("edit-user-id").value = u.id;
document.getElementById("input-username").value = u.username;
document.getElementById("input-username").disabled = true;
document.getElementById("input-display-name").value = u.displayName || "";
document.getElementById("input-role").value = u.role;
document.getElementById("input-enabled").value = String(u.enabled);
document.getElementById("password-group").style.display = "none";
openModal("modal-user");
};
document.getElementById("btn-save-user").addEventListener("click", async function () {
var id = document.getElementById("edit-user-id").value;
var isNew = !id;
try {
if (isNew) {
var data = {
username: document.getElementById("input-username").value,
displayName: document.getElementById("input-display-name").value,
password: document.getElementById("input-password").value,
role: document.getElementById("input-role").value,
enabled: document.getElementById("input-enabled").value === "true",
};
await API.createUser(data);
toast("用户创建成功", "success");
} else {
await API.updateUser(id, {
displayName: document.getElementById("input-display-name").value,
role: document.getElementById("input-role").value,
enabled: document.getElementById("input-enabled").value === "true",
});
toast("用户已更新", "success");
}
closeModal("modal-user");
loadUsers();
} catch (err) {
toast(err.message, "error");
}
});
window.resetPassword = function (id) {
document.getElementById("reset-user-id").value = id;
document.getElementById("input-new-password").value = "";
openModal("modal-reset-pw");
};
document.getElementById("btn-save-reset-pw").addEventListener("click", async function () {
var id = document.getElementById("reset-user-id").value;
var pw = document.getElementById("input-new-password").value;
try {
await API.resetPassword(id, pw);
closeModal("modal-reset-pw");
toast("密码已重置", "success");
} catch (err) {
toast(err.message, "error");
}
});
window.toggleUser = async function (id, enabled) {
try {
await API.updateUser(id, { enabled: enabled });
toast(enabled ? "用户已启用" : "用户已停用", "success");
loadUsers();
} catch (err) {
toast(err.message, "error");
}
};
window.confirmDeleteUser = function (id) {
var u = usersCache.find(function (x) { return x.id === id; });
var name = u ? u.username : id;
openConfirm("确认删除用户「" + name + "」?", async function () {
try {
await API.deleteUser(id);
toast("用户已删除", "success");
loadUsers();
} catch (err) {
toast(err.message, "error");
}
});
}
// ========== Token 管理 ==========
async function loadTokens() {
try {
var tokens = await API.getTokens();
renderTokens(tokens);
} catch (err) {
toast("加载 Token 失败: " + err.message, "error");
}
}
async function loadProjectsForSelect() {
try {
var projects = await API.getProjects();
var sel = document.getElementById("input-token-project");
sel.innerHTML = '<option value="all">全部项目</option>' +
projects.map(function (p) { return '<option value="' + p.id + '">' + esc(p.name) + "</option>"; }).join("");
} catch (e) {}
}
function renderTokens(tokens) {
var tbody = document.getElementById("token-tbody");
if (!tokens.length) {
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;color:#999;padding:30px">暂无 Token</td></tr>';
return;
}
tbody.innerHTML = tokens.map(function (t) {
var status = "badge-active";
var statusText = "有效";
if (!t.enabled) { status = "badge-disabled"; statusText = "已吊销"; }
else if (t.expiresAt && new Date(t.expiresAt) < new Date()) { status = "badge-expired"; statusText = "已过期"; }
var masked = t.value.substring(0, 6) + "••••••••••••";
return "<tr>" +
"<td>" + esc(t.name) + "</td>" +
"<td>" + esc(t.projectId === "all" ? "全部项目" : t.projectId) + "</td>" +
'<td><span class="dim-chip">' + (t.permission === "readwrite" ? "读写" : "只读") + "</span></td>" +
'<td>' + (t.mcpEnabled
? '<span class="dim-chip" style="color:#0a7a3c;border-color:#0a7a3c">MCP</span>'
: '<span style="color:#aaa">—</span>') + "</td>" +
'<td><div class="token-reveal">' +
'<span class="token-value token-mask" data-id="' + t.id + '">' + masked + "</span>" +
'<button class="token-btn" onclick="toggleTokenVis(this)">显示</button>' +
'<button class="token-btn" onclick="copyTokenVal(this, \'' + t.value + "')\">复制</button>" +
"</div></td>" +
'<td><span class="status-badge ' + status + '">' + statusText + "</span></td>" +
"<td>" + fmtDate(t.expiresAt) + "</td>" +
"<td>" + fmtDate(t.createdAt) + "</td>" +
'<td style="white-space:nowrap">' +
'<button class="row-btn" onclick=\'editToken(' + JSON.stringify({ id: t.id, name: t.name, projectId: t.projectId, permission: t.permission, mcpEnabled: !!t.mcpEnabled }).replace(/'/g, "&#39;") + ")'>编辑</button> " +
(t.enabled
? '<button class="row-btn danger" onclick="revokeToken(\'' + t.id + "')\">吊销</button>"
: "") +
' <button class="row-btn danger" onclick="confirmDeleteToken(\'' + t.id + "', '" + esc(t.name) + "')\">删除</button>" +
"</td>" +
"</tr>";
}).join("");
}
window.toggleTokenVis = function (btn) {
var span = btn.parentElement.querySelector(".token-value");
if (btn.textContent === "显示") {
btn.dataset.masked = span.textContent;
// 显示完整值需要从 tbody 原始数据中获取,这里通过 API 重新加载
span.classList.remove("token-mask");
span.textContent = span.dataset.full || btn.dataset.masked;
btn.textContent = "隐藏";
// 尝试从隐藏的 data 属性获取完整值
var id = span.dataset.id;
API.getTokens().then(function (tokens) {
var t = tokens.find(function (tk) { return tk.id === id; });
if (t) { span.dataset.full = t.value; span.textContent = t.value; }
});
} else {
span.classList.add("token-mask");
span.textContent = btn.dataset.masked;
btn.textContent = "显示";
}
};
window.copyTokenVal = function (btn, val) {
if (navigator.clipboard) navigator.clipboard.writeText(val);
btn.textContent = "已复制";
setTimeout(function () { btn.textContent = "复制"; }, 1200);
};
document.getElementById("token-search").addEventListener("input", function () {
var kw = this.value.trim().toLowerCase();
document.querySelectorAll("#token-tbody tr").forEach(function (tr) {
tr.style.display = kw && tr.textContent.toLowerCase().indexOf(kw) === -1 ? "none" : "";
});
});
document.getElementById("btn-add-token").addEventListener("click", function () {
document.getElementById("modal-token-title").textContent = "生成 Token";
document.getElementById("edit-token-id").value = "";
document.getElementById("input-token-name").value = "";
document.getElementById("input-token-project").value = "all";
document.getElementById("input-token-permission").value = "readonly";
document.getElementById("input-token-expiry").value = "90";
document.getElementById("input-token-mcp").checked = false;
document.getElementById("btn-save-token").textContent = "生成";
loadProjectsForSelect();
openModal("modal-token");
});
window.editToken = function (t) {
document.getElementById("modal-token-title").textContent = "编辑 Token";
document.getElementById("edit-token-id").value = t.id;
document.getElementById("input-token-name").value = t.name;
document.getElementById("input-token-project").value = t.projectId;
document.getElementById("input-token-permission").value = t.permission;
document.getElementById("input-token-mcp").checked = !!(t.mcpEnabled);
document.getElementById("btn-save-token").textContent = "保存";
loadProjectsForSelect();
openModal("modal-token");
};
document.getElementById("btn-save-token").addEventListener("click", async function () {
var id = document.getElementById("edit-token-id").value;
var name = document.getElementById("input-token-name").value.trim();
if (!name) { toast("请输入 Token 名称", "error"); return; }
try {
if (id) {
await API.updateToken(id, {
name: name,
projectId: document.getElementById("input-token-project").value,
permission: document.getElementById("input-token-permission").value,
mcpEnabled: document.getElementById("input-token-mcp").checked,
});
closeModal("modal-token");
toast("Token 已更新", "success");
loadTokens();
} else {
var data = await API.createToken({
name: name,
projectId: document.getElementById("input-token-project").value,
permission: document.getElementById("input-token-permission").value,
mcpEnabled: document.getElementById("input-token-mcp").checked,
expiresInDays: parseInt(document.getElementById("input-token-expiry").value) || 0,
});
closeModal("modal-token");
document.getElementById("reveal-token-value").textContent = data.value;
openModal("modal-token-reveal");
toast("Token 已生成", "success");
loadTokens();
}
} catch (err) {
toast(err.message, "error");
}
});
document.getElementById("btn-copy-reveal").addEventListener("click", function () {
var val = document.getElementById("reveal-token-value").textContent;
if (navigator.clipboard) navigator.clipboard.writeText(val);
this.textContent = "已复制";
setTimeout(function () { document.getElementById("btn-copy-reveal").textContent = "复制"; }, 1200);
});
window.revokeToken = function (id) {
openConfirm("确认吊销此 Token?吊销后使用该 Token 的服务将无法继续访问。", async function () {
try {
await API.revokeToken(id);
toast("Token 已吊销", "success");
loadTokens();
} catch (err) {
toast(err.message, "error");
}
});
};
window.confirmDeleteToken = function (id, name) {
openConfirm("确认删除 Token「" + name + "」?", async function () {
try {
await API.deleteToken(id);
toast("Token 已删除", "success");
loadTokens();
} catch (err) {
toast(err.message, "error");
}
});
}
// ========== 初始化 ==========
async function init() {
try {
var data = await API.getMe();
currentUser = data;
} catch (e) {
window.location.href = "/login.html";
return;
}
if (currentUser.role !== "admin") {
window.location.href = "/projects.html";
return;
}
document.getElementById("user-name").textContent = currentUser.displayName || currentUser.username;
document.getElementById("status-user").textContent = currentUser.username;
await Promise.all([loadUsers(), loadTokens()]);
}
document.getElementById("logout-link").addEventListener("click", async function (e) {
e.preventDefault();
try { await API.logout(); } catch (e) {}
localStorage.removeItem("registry_token");
localStorage.removeItem("registry_user");
window.location.href = "/login.html";
});
init();
</script>
</body>
</html>