feat(admin): user management - list + high-risk delete with full cascade (lsky, user db, attachments, auth)

This commit is contained in:
root
2026-10-17 13:37:15 +08:00
parent 68043d309e
commit 5452611a63
20 changed files with 181 additions and 55 deletions
+42 -1
View File
@@ -159,7 +159,7 @@ const server = http.createServer(async (req, res) => {
}
// ---- 管理(仅 is_admin 登录会话) ----
if (route === '/admin/users' || route === '/admin/invites' || route === '/admin/demo-seed' || route === '/admin/attachments/sync-lsky' || route.startsWith('/admin/invites/')) {
if (route === '/admin/users' || route === '/admin/users/delete' || route === '/admin/invites' || route === '/admin/demo-seed' || route === '/admin/attachments/sync-lsky' || route.startsWith('/admin/invites/')) {
let auth;
try { auth = authUser(req); }
catch (e) { return send(e.code || 401, false, e.message); }
@@ -177,6 +177,47 @@ const server = http.createServer(async (req, res) => {
.map(u => ({ ...u, is_admin: !!u.is_admin }));
return send(200, true, { total: users.length, users });
}
// 用户删除:高危操作,级联清理(图床文件 → 用户库 → 附件目录 → 认证数据)
if (req.method === 'POST' && route === '/admin/users/delete') {
const body = await readJson(req);
const uid = Number(body.user_id);
const target = db.prepare('SELECT id, username, is_admin FROM users WHERE id=?').get(uid);
if (!target) return send(404, false, '用户不存在');
if (target.id === auth.user_id) return send(400, false, '不能删除当前登录的账号自己');
if (target.is_admin && db.prepare('SELECT COUNT(*) AS c FROM users WHERE is_admin=1').get().c <= 1) {
return send(400, false, '不能删除最后一位管理员');
}
// 图床远端文件(尽力而为,不阻塞删除)
let remote = 0;
try {
const atts = commandsFor(target.id).getDb().prepare('SELECT remote_key, remote_thumb_key FROM attachment WHERE deleted_at IS NULL').all();
for (const a of atts) {
for (const k of [a.remote_key, a.remote_thumb_key]) {
if (k && LSKY_API && LSKY_TOKEN) { try { await lskyDelete(k); remote++; } catch {} }
}
}
} catch {}
// 关闭库句柄并删除用户库 + 附件目录
try { commandsFor(target.id).close(); } catch {}
userCommands.delete(target.id);
if (demoUserId === target.id) demoUserId = undefined;
for (const f of [target.id + '.db', target.id + '.db-wal', target.id + '.db-shm']) {
try { fs.rmSync(path.join(USERS_DIR, f), { force: true }); } catch {}
}
try { fs.rmSync(path.join(USERS_DIR, String(target.id)), { recursive: true, force: true }); } catch {}
// 认证库级联清理
db.exec('BEGIN');
try {
db.prepare('DELETE FROM current WHERE user_id=?').run(target.id);
db.prepare('DELETE FROM attachments WHERE user_id=?').run(target.id);
db.prepare('DELETE FROM devices WHERE user_id=?').run(target.id);
db.prepare('DELETE FROM users WHERE id=?').run(target.id);
db.exec('COMMIT');
} catch (e) { db.exec('ROLLBACK'); throw e; }
console.log('[footprints-sync] 用户已删除:', target.username, '(图床文件 ' + remote + ' 个)');
return send(200, true, { ok: true, username: target.username, remote_deleted: remote });
}
if (req.method === 'GET' && route === '/admin/invites') {
const invites = db.prepare(`
SELECT i.id, i.code, i.remark, i.created_at, i.used_by, i.used_at, i.valid,