feat: Token 支持多项目关联且全部为 MCP 令牌;配置操作全量审计,支持按项目/键查询
build-and-deploy / build-and-deploy (push) Successful in 40s

This commit is contained in:
jif2.zhang
2026-09-21 09:40:33 +08:00
parent be2e4d9cdf
commit 6bc738d359
13 changed files with 833 additions and 249 deletions
+69 -39
View File
@@ -117,7 +117,7 @@
<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>Token</th>
<th>状态</th><th>过期时间</th><th>创建时间</th><th>操作</th>
</tr>
</thead>
@@ -211,8 +211,13 @@
<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>
<label>关联项目 <span class="hint">(可勾选全部或多个已存在项目)</span></label>
<label class="checkbox-label" style="display:flex;align-items:center;gap:8px;cursor:pointer;margin-bottom:6px">
<input type="checkbox" id="input-token-all" style="width:16px;height:16px">
<span>全部项目</span>
</label>
<div id="token-project-list" style="display:flex;flex-direction:column;gap:6px;max-height:160px;overflow:auto">
</div>
</div>
<div class="form-row">
<div class="form-group form-half">
@@ -229,13 +234,7 @@
</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>
<p class="hint">所有 Token 均可作为 MCP 访问令牌用于 /mcp 端点鉴权。</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('modal-token')">取消</button>
@@ -480,19 +479,61 @@
}
}
var tokenProjects = [];
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("");
tokenProjects = await API.getProjects();
var list = document.getElementById("token-project-list");
list.innerHTML = tokenProjects.map(function (p) {
return '<label class="checkbox-label" style="display:flex;align-items:center;gap:8px;cursor:pointer">' +
'<input type="checkbox" class="token-proj-check" value="' + p.id + '" style="width:14px;height:14px"> ' +
'<span>' + esc(p.name) + "</span></label>";
}).join("");
} catch (e) {}
}
function getSelectedProjectIds() {
if (document.getElementById("input-token-all").checked) return ["all"];
var ids = [];
document.querySelectorAll(".token-proj-check:checked").forEach(function (c) { ids.push(c.value); });
return ids.length ? ids : ["all"];
}
function setSelectedProjectIds(ids) {
var allCheck = document.getElementById("input-token-all");
if (!ids || !ids.length || ids.indexOf("all") !== -1) {
allCheck.checked = true;
document.querySelectorAll(".token-proj-check").forEach(function (c) { c.checked = false; c.disabled = true; });
} else {
allCheck.checked = false;
document.querySelectorAll(".token-proj-check").forEach(function (c) {
c.checked = ids.indexOf(c.value) !== -1;
c.disabled = false;
});
}
}
document.getElementById("input-token-all").addEventListener("change", function () {
var checked = this.checked;
document.querySelectorAll(".token-proj-check").forEach(function (c) {
c.checked = checked ? false : c.checked;
c.disabled = checked;
});
});
function projectNames(projectIds) {
if (!projectIds || !projectIds.length || projectIds.indexOf("all") !== -1) return "全部项目";
return projectIds.map(function (id) {
var p = tokenProjects.find(function (x) { return x.id === id; });
return p ? p.name : id;
}).join("、");
}
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>';
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:#999;padding:30px">暂无 Token</td></tr>';
return;
}
tbody.innerHTML = tokens.map(function (t) {
@@ -503,11 +544,8 @@
var masked = t.value.substring(0, 6) + "••••••••••••";
return "<tr>" +
"<td>" + esc(t.name) + "</td>" +
"<td>" + esc(t.projectId === "all" ? "全部项目" : t.projectId) + "</td>" +
"<td>" + esc(projectNames(t.projectIds)) + "</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>' +
@@ -517,7 +555,7 @@
"<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> " +
'<button class="row-btn" onclick=\'editToken(' + JSON.stringify({ id: t.id, name: t.name, projectIds: t.projectIds || ["all"], permission: t.permission }).replace(/'/g, "&#39;") + ")'>编辑</button> " +
(t.enabled
? '<button class="row-btn danger" onclick="revokeToken(\'' + t.id + "')\">吊销</button>"
: "") +
@@ -565,12 +603,10 @@
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();
loadProjectsForSelect().then(function () { setSelectedProjectIds(["all"]); });
openModal("modal-token");
});
@@ -578,11 +614,10 @@
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();
var ids = t.projectIds;
loadProjectsForSelect().then(function () { setSelectedProjectIds(ids); });
openModal("modal-token");
};
@@ -590,25 +625,20 @@
var id = document.getElementById("edit-token-id").value;
var name = document.getElementById("input-token-name").value.trim();
if (!name) { toast("请输入 Token 名称", "error"); return; }
var payload = {
name: name,
projectIds: getSelectedProjectIds(),
permission: document.getElementById("input-token-permission").value,
};
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,
});
await API.updateToken(id, payload);
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,
});
payload.expiresInDays = parseInt(document.getElementById("input-token-expiry").value) || 0;
var data = await API.createToken(payload);
closeModal("modal-token");
document.getElementById("reveal-token-value").textContent = data.value;
openModal("modal-token-reveal");