registry/public/admin.html
jif2.zhang 1765caba50
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 21s
feat: 账号认证、用户管理与 API Token 管理
- 后端: Auth(登录/登出/me + session 中间件)、Users、Tokens 路由与 Service,
  scrypt 哈希+timingSafeEqual 校验、requireAdmin 保护、admin 用户自动初始化
- 前端: login.html(登录页, 无账号密码提示)、admin.html(用户+Token 管理,
  令牌明文显隐/复制/吊销/重置密码)、标题栏用户信息+系统管理入口
- 持久化: jsonAdapter 支持 users/api-tokens,运行时数据忽略入库
- 文档: 需求.md/原型设计.md 同步
2026-09-11 17:12:37 +08:00

668 lines
30 KiB
HTML
Raw Permalink 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>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">
<span id="status-text">系统管理 · 用户管理</span>
<span id="status-user"></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>
<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>
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="8" 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><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 }).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("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("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,
});
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,
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>