feat: 账号认证、用户管理与 API Token 管理
build-and-deploy / build-and-deploy (push) Successful in 21s

- 后端: Auth(登录/登出/me + session 中间件)、Users、Tokens 路由与 Service,
  scrypt 哈希+timingSafeEqual 校验、requireAdmin 保护、admin 用户自动初始化
- 前端: login.html(登录页, 无账号密码提示)、admin.html(用户+Token 管理,
  令牌明文显隐/复制/吊销/重置密码)、标题栏用户信息+系统管理入口
- 持久化: jsonAdapter 支持 users/api-tokens,运行时数据忽略入库
- 文档: 需求.md/原型设计.md 同步
This commit is contained in:
jif2.zhang
2026-09-11 17:12:37 +08:00
parent 963046edd9
commit 1765caba50
16 changed files with 2132 additions and 141 deletions
+315
View File
@@ -0,0 +1,315 @@
<!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; }
/* 内容区 */
.page-content { flex: 1; overflow: auto; padding: 18px; }
.page-heading { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 14px; }
.page-heading h2 { font-size: 16px; color: #003a70; }
.page-heading .sub { font-size: 12px; color: #888; }
.content-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
.content-toolbar .spacer { flex: 1; }
.search-input { height: 30px; width: 240px; 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; }
/* 项目卡片 */
.projects-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 14px; }
.project-card { background: #fff; border: 1px solid #dcdcdc; border-radius: 6px; padding: 14px 16px 12px; cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; display: flex; flex-direction: column; }
.project-card:hover { border-color: #5b9bd5; box-shadow: 0 4px 14px rgba(0,120,212,0.15); transform: translateY(-1px); }
.project-card-top { display: flex; align-items: flex-start; gap: 10px; }
.project-icon { width: 34px; height: 34px; border-radius: 6px; background: #e5f1fb; color: #0078d4; display: flex; align-items: center; justify-content: center; font-size: 17px; flex-shrink: 0; }
.project-card-title { font-size: 15px; font-weight: 600; color: #003a70; line-height: 1.3; }
.project-card-id { font-family: Consolas, monospace; font-size: 11px; color: #999; margin-top: 2px; }
.project-card-desc { margin-top: 10px; font-size: 12px; color: #666; line-height: 1.5; min-height: 36px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.project-card-badges { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
.dim-chip { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; background: #eef4fb; color: #0a4a86; border: 1px solid #d0dced; }
.project-card-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; padding-top: 10px; border-top: 1px solid #f0f0f0; font-size: 11px; color: #999; }
.project-card-actions { display: flex; gap: 6px; opacity: 0; transition: opacity 0.15s; }
.project-card:hover .project-card-actions { opacity: 1; }
.empty-state { text-align: center; padding: 60px 20px; color: #999; }
.empty-state .empty-icon { font-size: 36px; opacity: 0.4; }
.empty-state p { margin-top: 8px; font-size: 13px; }
</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" id="user-role"></span>
</span>
<a class="titlebar-link" id="admin-entry" href="/admin.html" style="display:none">⚙ 系统管理</a>
<a class="titlebar-link" id="logout-link" href="#">退出登录</a>
</div>
</header>
<div class="toolbar">
<div class="tool-group">
<span class="group-label">项目</span>
<button class="toolbar-btn" id="btn-refresh" title="刷新列表">🔄 刷新</button>
<button class="toolbar-btn" id="btn-new-project" title="新建项目">📁 新建</button>
</div>
<div class="tool-group">
<span class="group-label" id="project-count">共 0 个项目</span>
</div>
</div>
<div class="page-content">
<div class="page-heading">
<h2>我的项目</h2>
<span class="sub">选择一个项目进入配置管理</span>
</div>
<div class="content-toolbar">
<input type="text" class="search-input" id="project-search" placeholder="🔍 搜索项目名称或描述">
<span class="spacer"></span>
<button class="btn btn-primary" style="height:30px" id="btn-new-project-2">➕ 新建项目</button>
</div>
<div class="projects-grid" id="projects-grid"></div>
</div>
<footer class="statusbar">
<span id="status-text">加载中...</span>
<span id="status-user"></span>
</footer>
<!-- 新建项目模态框(复用 index.html 中的结构) -->
<div class="modal-overlay" id="modal-project" style="display:none">
<div class="modal">
<div class="modal-header">
<h3>新建项目</h3>
<button class="modal-close" onclick="closeModal('modal-project')">✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>项目名称 <span class="required">*</span></label>
<input type="text" id="project-name" placeholder="例如:用户服务">
</div>
<div class="form-group">
<label>项目描述</label>
<textarea id="project-description" rows="2" placeholder="项目配置说明"></textarea>
</div>
<div class="form-group">
<label>维度配置 <span class="hint">(创建后不可修改)</span></label>
<div class="dim-config-row">
<label class="checkbox-label"><input type="checkbox" id="project-enable-idc" checked> 机房</label>
<select id="project-idc-mapping" class="mapping-select"><option value="path">映射为路径</option><option value="label">仅作标签</option></select>
</div>
<div class="dim-config-row">
<label class="checkbox-label"><input type="checkbox" id="project-enable-env" checked> 环境</label>
<select id="project-env-mapping" class="mapping-select"><option value="path">映射为路径</option><option value="label">仅作标签</option></select>
</div>
<div class="dim-config-row">
<label class="checkbox-label"><input type="checkbox" id="project-enable-group" checked> 分组</label>
<select id="project-group-mapping" class="mapping-select"><option value="path">映射为路径</option><option value="label">仅作标签</option></select>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('modal-project')">取消</button>
<button class="btn btn-primary" id="btn-save-project">保存</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"; }
function closeModal(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;
});
// 初始化
async function init() {
try {
var data = await API.getMe();
currentUser = data;
} catch (e) {
window.location.href = "/login.html";
return;
}
document.getElementById("user-name").textContent = currentUser.displayName || currentUser.username;
var roleEl = document.getElementById("user-role");
if (currentUser.role === "admin") {
roleEl.textContent = "管理员";
roleEl.className = "role-badge admin";
document.getElementById("admin-entry").style.display = "";
}
document.getElementById("status-user").textContent = currentUser.username;
await loadProjects();
}
async function loadProjects() {
try {
var projects = await API.getProjects();
renderProjects(projects);
} catch (err) {
toast("加载项目失败: " + err.message, "error");
}
}
function renderProjects(projects) {
var grid = document.getElementById("projects-grid");
if (!projects.length) {
grid.innerHTML = '<div class="empty-state"><div class="empty-icon">📁</div><p>暂无项目,点击「新建项目」创建第一个</p></div>';
document.getElementById("project-count").textContent = "共 0 个项目";
document.getElementById("status-text").textContent = "已加载 0 个项目";
return;
}
var dims = [];
grid.innerHTML = projects.map(function (p) {
dims = [];
if (p.enableIdc) dims.push("🏢 机房");
if (p.enableEnvironment) dims.push("🌐 环境");
if (p.enableGroup) dims.push("🗂 分组");
var badges = dims.map(function (d) { return '<span class="dim-chip">' + d + "</span>"; }).join("");
var time = p.updatedAt ? new Date(p.updatedAt).toLocaleDateString("zh-CN") : "";
return '<div class="project-card" data-id="' + p.id + '" onclick="openProject(\'' + p.id + "')\">" +
'<div class="project-card-top">' +
'<div class="project-icon">📦</div>' +
'<div><div class="project-card-title">' + esc(p.name) + '</div>' +
'<div class="project-card-id">' + esc(p.id) + "</div></div>" +
"</div>" +
'<div class="project-card-desc">' + esc(p.description || "") + "</div>" +
'<div class="project-card-badges">' + badges + "</div>" +
'<div class="project-card-footer">' +
"<span>" + time + "</span>" +
'<div class="project-card-actions">' +
'<button class="row-btn" onclick="event.stopPropagation();openProject(\'' + p.id + "')\">进入</button>" +
'<button class="row-btn danger" onclick="event.stopPropagation();confirmDelete(\'' + p.id + "', '" + esc(p.name) + "')\">删除</button>" +
"</div>" +
"</div>" +
"</div>";
}).join("");
document.getElementById("project-count").textContent = "共 " + projects.length + " 个项目";
document.getElementById("status-text").textContent = "已加载 " + projects.length + " 个项目";
}
function esc(s) {
var d = document.createElement("div");
d.textContent = s;
return d.innerHTML;
}
function openProject(id) {
window.location.href = "/index.html?project=" + encodeURIComponent(id);
}
function confirmDelete(id, name) {
openConfirm("确认删除项目「" + name + "」?此操作将同时删除该项目下的所有配置,且不可恢复。", async function () {
try {
await API.deleteProject(id);
toast("项目已删除", "success");
loadProjects();
} catch (err) {
toast("删除失败: " + err.message, "error");
}
});
}
// 新建项目
document.getElementById("btn-new-project").addEventListener("click", function () { openModal("modal-project"); });
document.getElementById("btn-new-project-2").addEventListener("click", function () { openModal("modal-project"); });
document.getElementById("btn-save-project").addEventListener("click", async function () {
var name = document.getElementById("project-name").value.trim();
var desc = document.getElementById("project-description").value.trim();
if (!name) { toast("请输入项目名称", "error"); return; }
try {
await API.createProject({
name: name,
description: desc,
enableIdc: document.getElementById("project-enable-idc").checked,
enableEnvironment: document.getElementById("project-enable-env").checked,
enableGroup: document.getElementById("project-enable-group").checked,
idcMapping: document.getElementById("project-idc-mapping").value,
environmentMapping: document.getElementById("project-env-mapping").value,
groupMapping: document.getElementById("project-group-mapping").value,
});
closeModal("modal-project");
document.getElementById("project-name").value = "";
document.getElementById("project-description").value = "";
toast("项目创建成功", "success");
loadProjects();
} catch (err) {
toast("创建失败: " + err.message, "error");
}
});
// 搜索
document.getElementById("project-search").addEventListener("input", function () {
var kw = this.value.trim().toLowerCase();
var cards = document.querySelectorAll(".project-card");
var visible = 0;
cards.forEach(function (card) {
var text = card.textContent.toLowerCase();
var show = !kw || text.indexOf(kw) !== -1;
card.style.display = show ? "" : "none";
if (show) visible++;
});
document.getElementById("project-count").textContent = "共 " + visible + " 个项目";
});
// 退出登录
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";
});
document.getElementById("btn-refresh").addEventListener("click", loadProjects);
init();
</script>
</body>
</html>