feat: 管理员跨租户管理 + 注册开关

- 管理员可管理任意用户笔记(读取/修改/删除/回收站/标签/图谱/FTS 全平台)
- 普通用户仍数据隔离, 越权返回404
- 新增注册开关: 管理员后台⚙设置可开/关, 支持REGISTRATION_ENABLED环境变量
- 注册关闭时前台/登录页隐藏注册入口, 注册接口返回400
- 冒烟测试扩展到91用例全过
This commit is contained in:
Your Name
2026-08-11 13:07:56 +08:00
parent d9793300f9
commit 13c53fea0a
12 changed files with 741 additions and 89 deletions
+88
View File
@@ -634,6 +634,21 @@
body.dark .tag-row .tag-name { color: #e6e6e6; }
body.dark .tag-row .tag-count { background: #20242e; color: #9aa0aa; }
body.dark .tags-empty { color: #9aa0aa; }
/* 平台设置:注册开关 */
.switch { position: relative; display: inline-block; width: 48px; height: 26px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider {
position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
background: #ccc; transition: .3s; border-radius: 26px;
}
.slider:before {
position: absolute; content: ""; height: 20px; width: 20px; left: 3px; bottom: 3px;
background: #fff; transition: .3s; border-radius: 50%;
}
.switch input:checked + .slider { background: #1976d2; }
.switch input:checked + .slider:before { transform: translateX(22px); }
body.dark .slider { background: #2a3040; }
body.dark .setting-row { border-bottom-color: #232733; }
</style>
</head>
<body>
@@ -649,6 +664,7 @@
<button class="btn btn-secondary" onclick="toggleDarkMode()" title="切换深色模式" id="darkModeBtn">🌙 深色</button>
<button class="btn btn-secondary" onclick="exportAll()" title="批量导出全部笔记为 zip">📦 批量导出</button>
<button class="btn btn-secondary" onclick="openTrash()" title="回收站">🗑 回收站</button>
<button class="btn btn-secondary" id="settingsBtn" onclick="openSettings()" title="平台设置(仅管理员)" style="display:none;">⚙ 设置</button>
<a href="/" class="btn btn-secondary" target="_blank">前台预览</a>
<button class="btn btn-secondary" onclick="logout()">退出登录</button>
</div>
@@ -805,6 +821,27 @@
</div>
</div>
<!-- 平台设置弹窗(仅管理员) -->
<div class="modal" id="settingsModal">
<div class="modal-content" style="min-width: 460px;">
<h3 style="margin-bottom: 16px;">⚙ 平台设置</h3>
<div class="setting-row" style="display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-bottom:1px solid #eee;">
<div>
<div style="font-weight:600;">开放注册</div>
<div style="font-size:12px;color:#999;">关闭后新用户无法注册,仅已有账号可登录</div>
</div>
<label class="switch">
<input type="checkbox" id="regSwitch" onchange="toggleRegistration(this)">
<span class="slider"></span>
</label>
</div>
<div class="modal-actions" style="margin-top:16px;">
<button class="btn btn-secondary" onclick="closeSettings()">关闭</button>
</div>
</div>
</div>
<!-- 分享弹窗 -->
<div class="modal" id="shareModal">
<div class="modal-content" style="min-width: 520px;">
@@ -875,10 +912,61 @@
// 初始化
async function init() {
await checkAdmin();
await loadTree();
setupResize();
setupImagePaste();
}
// 检查当前用户是否为管理员(决定是否显示「设置」按钮)
async function checkAdmin() {
try {
const res = await fetch('/api/auth/me');
const data = await res.json();
const me = data.data;
if (me && me.role === 'admin') {
const btn = document.getElementById('settingsBtn');
if (btn) btn.style.display = '';
}
} catch (e) { /* 非管理员不显示 */ }
}
// ── 平台设置(注册开关,管理员) ──
async function openSettings() {
document.getElementById('settingsModal').style.display = 'flex';
try {
const res = await fetch(`${ADMIN_API}/settings/registration`);
const data = await res.json();
if (data.code === 0) {
document.getElementById('regSwitch').checked = !!data.data.registration_enabled;
}
} catch (e) {
showToast('读取设置失败', 'error');
}
}
function closeSettings() {
document.getElementById('settingsModal').style.display = 'none';
}
async function toggleRegistration(el) {
const enabled = el.checked;
try {
const res = await fetch(`${ADMIN_API}/settings/registration`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: enabled })
});
const data = await res.json();
if (data.code === 0) {
showToast(enabled ? '已开放注册' : '已关闭注册', 'success');
} else {
el.checked = !enabled;
showToast(data.message || '操作失败', 'error');
}
} catch (e) {
el.checked = !enabled;
showToast('操作失败', 'error');
}
}
// 设置粘贴图片监听
function setupImagePaste() {
+11
View File
@@ -166,6 +166,17 @@
}
switchLink.addEventListener('click', () => setMode(!isRegister));
// 若注册已关闭,隐藏「去注册」链接(从 /api/auth/me 读取)
(async () => {
try {
const res = await fetch('/api/auth/me');
const data = await res.json();
if (data.data && data.data.registration_enabled === false) {
switchLink.style.display = 'none';
}
} catch (e) { /* 保留默认 */ }
})();
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
errorMsg.style.display = 'none';