feat(admin): user management - list + high-risk delete with full cascade (lsky, user db, attachments, auth)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user