New Database
This commit is contained in:
+111
-35
@@ -1060,10 +1060,115 @@
|
||||
var tab = $('users-tab');
|
||||
if (tab) tab.style.display = '';
|
||||
loadUsers();
|
||||
// 数据库管理仅 Root (id 0) 可见
|
||||
if (status.user.id === 0) {
|
||||
var dbSec = $('db-admin-section');
|
||||
if (dbSec) dbSec.style.display = '';
|
||||
loadDbStatus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Database administration (Root only) ───
|
||||
function loadDbStatus() {
|
||||
var el = $('db-status');
|
||||
if (!el) return;
|
||||
api('GET', '/api/admin/db/status', null, function (d) {
|
||||
if (d && d.database) {
|
||||
el.textContent = 'PostgreSQL · schema v' + d.db_version + ' · 表 ' + (d.tables ? d.tables.length : '?') + ' 张';
|
||||
el.style.color = '#2e7d32';
|
||||
} else {
|
||||
el.textContent = (d && d.error) || '获取状态失败';
|
||||
el.style.color = '#E05555';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function dbMsg(text, isErr) {
|
||||
var el = $('db-admin-msg');
|
||||
if (!el) return;
|
||||
el.textContent = text;
|
||||
el.style.color = isErr ? '#E05555' : '#2e7d32';
|
||||
}
|
||||
|
||||
function initDbAdmin() {
|
||||
var exportBtn = $('db-export-btn');
|
||||
if (exportBtn) {
|
||||
exportBtn.addEventListener('click', function () {
|
||||
exportBtn.disabled = true;
|
||||
dbMsg('正在导出...', false);
|
||||
_reauthThen(function () {
|
||||
// 用 fetch 携带 Authorization 下载备份文件
|
||||
var token = Auth.getAccess && Auth.getAccess();
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/api/admin/db/export');
|
||||
if (token) xhr.setRequestHeader('Authorization', 'Bearer ' + token);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.onload = function () {
|
||||
exportBtn.disabled = false;
|
||||
if (xhr.status === 200) {
|
||||
var blob = xhr.response;
|
||||
var disp = xhr.getResponseHeader('Content-Disposition') || '';
|
||||
var m = disp.match(/filename="?([^";]+)"?/);
|
||||
var name = m ? m[1] : 'iot-dboard-backup.json';
|
||||
var a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
|
||||
dbMsg('导出成功,文件已开始下载', false);
|
||||
} else if (xhr.status === 403) {
|
||||
dbMsg('需要重新验证身份,请重试', true);
|
||||
} else {
|
||||
dbMsg('导出失败 (HTTP ' + xhr.status + ')', true);
|
||||
}
|
||||
};
|
||||
xhr.onerror = function () { exportBtn.disabled = false; dbMsg('导出失败:网络错误', true); };
|
||||
xhr.send();
|
||||
}, function () {
|
||||
exportBtn.disabled = false;
|
||||
dbMsg('身份验证未通过,导出已取消', true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var form = $('db-import-form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var fileInput = $('db-import-file');
|
||||
var file = fileInput && fileInput.files && fileInput.files[0];
|
||||
if (!file) { dbMsg('请先选择文件', true); return; }
|
||||
var btn = $('db-import-btn');
|
||||
btn.disabled = true;
|
||||
dbMsg('正在导入(大文件可能需要一些时间)...', false);
|
||||
_reauthThen(function () {
|
||||
var fd = new FormData();
|
||||
fd.append('file', file);
|
||||
api('POST', '/api/admin/db/import', fd, function (res) {
|
||||
btn.disabled = false;
|
||||
if (res && res.ok) {
|
||||
dbMsg(res.message || '导入成功', false);
|
||||
// 导入会重建全部数据并清空会话(含当前设备),提示后跳转登录页
|
||||
setTimeout(function () {
|
||||
try { localStorage.removeItem('dashboard_access_token'); } catch (e) {}
|
||||
location.href = '/login?view=control&next=%2Fcontrol';
|
||||
}, 1800);
|
||||
} else {
|
||||
dbMsg((res && res.error) || '导入失败', true);
|
||||
}
|
||||
});
|
||||
}, function () {
|
||||
btn.disabled = false;
|
||||
dbMsg('身份验证未通过,导入已取消', true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
initDbAdmin();
|
||||
|
||||
// ─── User Management (admin only) ─────────
|
||||
var _totpTimer = null;
|
||||
|
||||
@@ -1137,30 +1242,13 @@
|
||||
function loadUsers() {
|
||||
if (!$('users-tab') || $('users-tab').style.display === 'none') return;
|
||||
api('GET', '/api/admin/users', null, function (users) {
|
||||
var pending = users.filter(function (u) { return u.status === 'pending'; });
|
||||
var active = users.filter(function (u) { return u.status === 'active'; });
|
||||
|
||||
// Pending requests
|
||||
var pendHtml = '';
|
||||
if (pending.length > 0) {
|
||||
pendHtml = '<h4 style="margin:0 0 8px;font-size:14px;">⏳ 待确认申请(用户自助填写信息)</h4>';
|
||||
pending.forEach(function (u) {
|
||||
pendHtml += '<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border:1px solid var(--border);border-radius:8px;margin-bottom:6px;background:var(--warn-bg);">' +
|
||||
'<div><strong>' + _esc(u.remark) + '</strong> <span style="font-size:12px;color:var(--text-dim);">' + _esc(u.created_at || '') + '</span></div>' +
|
||||
'<div style="display:flex;gap:6px;">' +
|
||||
'<button class="small-btn" data-action="confirm-request" data-id="' + u.id + '" style="color:#2e7d32;">确认并生成临时账户</button>' +
|
||||
'<button class="small-btn" data-action="delete-request" data-id="' + u.id + '" style="color:#E05555;">拒绝</button>' +
|
||||
'</div></div>';
|
||||
});
|
||||
}
|
||||
$('pending-requests').innerHTML = pendHtml;
|
||||
|
||||
// Active users
|
||||
// 统一渲染全部用户(含历史 pending 申请用户,标记后可删除)
|
||||
var html = '<h4 style="margin:0 0 8px;font-size:14px;">👥 用户列表</h4>';
|
||||
if (active.length === 0) html += '<div style="color:var(--text-dim);font-size:13px;">暂无用户</div>';
|
||||
active.forEach(function (u) {
|
||||
if (users.length === 0) html += '<div style="color:var(--text-dim);font-size:13px;">暂无用户</div>';
|
||||
users.forEach(function (u) {
|
||||
var badges = [];
|
||||
if (u.role === 'admin') badges.push('<span class="badge badge-on">管理员</span>');
|
||||
if (u.status === 'pending') badges.push('<span class="badge badge-off">待确认申请</span>');
|
||||
if (u.is_temp) badges.push('<span class="badge badge-off">临时账户</span>');
|
||||
if (u.has_mfa) badges.push('<span class="badge badge-on">已配置 MFA</span>');
|
||||
html += '<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border:1px solid var(--border);border-radius:8px;margin-bottom:6px;flex-wrap:wrap;gap:6px;">' +
|
||||
@@ -1183,23 +1271,11 @@
|
||||
}
|
||||
|
||||
function bindUserActions() {
|
||||
document.querySelectorAll('#pending-requests [data-action], #user-list [data-action]').forEach(function (btn) {
|
||||
document.querySelectorAll('#user-list [data-action]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var action = btn.dataset.action;
|
||||
var id = parseInt(btn.dataset.id, 10);
|
||||
if (action === 'confirm-request') {
|
||||
_reauthThen(function () {
|
||||
api('POST', '/api/admin/users/confirm', { request_id: id }, function (res) {
|
||||
if (res.ok) { toast('已生成临时账户'); loadUsers(); } else { toast(res.error || '操作失败'); }
|
||||
});
|
||||
});
|
||||
} else if (action === 'delete-request') {
|
||||
_reauthThen(function () {
|
||||
api('DELETE', '/api/admin/users/' + id, null, function (res) {
|
||||
if (res.ok) { toast('已拒绝该申请'); loadUsers(); } else { toast(res.error || '操作失败'); }
|
||||
});
|
||||
});
|
||||
} else if (action === 'edit-remark') {
|
||||
if (action === 'edit-remark') {
|
||||
_reauthThen(function () {
|
||||
showPrompt('编辑用户备注', '新的备注', '', function (newRemark) {
|
||||
if (newRemark === null || !newRemark.trim()) return;
|
||||
|
||||
Reference in New Issue
Block a user