新增访问记录(仅超管): 登录成功/失败/退出/账号操作审计含IP, 顶栏入口在账号管理旁, 弹框展示+可清空
This commit is contained in:
@@ -43,8 +43,24 @@ udb.exec(`CREATE TABLE IF NOT EXISTS users (
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
||||
last_login_at TEXT
|
||||
)`);
|
||||
udb.exec(`CREATE TABLE IF NOT EXISTS access_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
detail TEXT DEFAULT '',
|
||||
ip TEXT DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
||||
)`);
|
||||
const userCount = () => udb.prepare('SELECT COUNT(*) c FROM users').get().c;
|
||||
|
||||
// 访问/审计日志(仅超管可查)
|
||||
function logAccess(req, username, action, detail = '') {
|
||||
try {
|
||||
const ip = String(req.headers['x-forwarded-for'] || '').split(',')[0].trim() || req.socket.remoteAddress || '';
|
||||
udb.prepare('INSERT INTO access_log (username, action, detail, ip) VALUES (?,?,?,?)').run(username, action, detail, ip);
|
||||
} catch (_) { /* 记日志失败不影响业务 */ }
|
||||
}
|
||||
|
||||
// ---------- 招聘库(读快照,不直读活库) ----------
|
||||
// 活库是 WAL 模式且被 Hermes 持续写入,直接单文件挂载读会在 checkpoint 期间报
|
||||
// "database disk image is malformed"。改为定期 VACUUM INTO 生成一致性快照再读。
|
||||
@@ -111,19 +127,26 @@ app.post('/api/register', (req, res) => { // 仅无任何账号时开
|
||||
return res.status(400).json({ error: '用户名至少2位,密码至少6位' });
|
||||
const info = udb.prepare('INSERT INTO users (username, password, role) VALUES (?, ?, ?)')
|
||||
.run(String(username).trim(), hashPassword(String(password)), 'super');
|
||||
logAccess(req, String(username).trim(), 'register_super', '首个账号成为超管');
|
||||
res.json({ ok: true, role: 'super', id: info.lastInsertRowid });
|
||||
});
|
||||
app.post('/api/login', (req, res) => {
|
||||
const { username, password } = req.body || {};
|
||||
const u = udb.prepare('SELECT * FROM users WHERE username = ?').get(String(username || '').trim());
|
||||
if (!u || !verifyPassword(String(password || ''), u.password)) return res.status(401).json({ error: '用户名或密码错误' });
|
||||
if (u.disabled) return res.status(403).json({ error: '账号已停用' });
|
||||
if (!u || !verifyPassword(String(password || ''), u.password)) {
|
||||
logAccess(req, String(username || ''), 'login_fail', '用户名或密码错误');
|
||||
return res.status(401).json({ error: '用户名或密码错误' });
|
||||
}
|
||||
if (u.disabled) { logAccess(req, u.username, 'login_fail', '账号已停用'); return res.status(403).json({ error: '账号已停用' }); }
|
||||
udb.prepare("UPDATE users SET last_login_at = datetime('now','localtime') WHERE id = ?").run(u.id);
|
||||
logAccess(req, u.username, 'login_ok');
|
||||
res.setHeader('Set-Cookie', `session=${signSid(newSession(u.id, u.role))}; HttpOnly; Path=/; SameSite=Lax; Max-Age=${SESSION_HOURS * 3600}`);
|
||||
res.json({ ok: true, username: u.username, role: u.role });
|
||||
});
|
||||
app.post('/api/logout', (req, res) => {
|
||||
const sid = parseSid(req.headers.cookie);
|
||||
const me = authUser(req);
|
||||
if (me) logAccess(req, me.username, 'logout');
|
||||
if (sid) sessions.delete(sid);
|
||||
res.setHeader('Set-Cookie', 'session=; HttpOnly; Path=/; Max-Age=0');
|
||||
res.json({ ok: true });
|
||||
@@ -237,8 +260,9 @@ app.post('/api/users', requireAuth, requireSuper, (req, res) => {
|
||||
const { username, password } = req.body || {};
|
||||
if (!username || !password || String(password).length < 6) return res.status(400).json({ error: '用户名必填,密码至少6位' });
|
||||
try {
|
||||
const info = udb.prepare("INSERT INTO users (username, password, role) VALUES (?, ?, 'hr')")
|
||||
const info = udb.prepare('INSERT INTO users (username, password, role) VALUES (?, ?, "hr")')
|
||||
.run(String(username).trim(), hashPassword(String(password)));
|
||||
logAccess(req, req.user.username, 'user_create', `创建 ${String(username).trim()}`);
|
||||
res.json({ ok: true, id: info.lastInsertRowid });
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: '用户名已存在' });
|
||||
@@ -249,13 +273,26 @@ app.post('/api/users/:id/toggle', requireAuth, requireSuper, (req, res) => {
|
||||
if (!u) return res.status(404).json({ error: '不存在' });
|
||||
if (u.role === 'super' && !u.disabled) return res.status(400).json({ error: '不能停用自己的超管账号' });
|
||||
udb.prepare('UPDATE users SET disabled = ? WHERE id = ?').run(u.disabled ? 0 : 1, u.id);
|
||||
logAccess(req, req.user.username, 'user_toggle', `${u.disabled ? '启用' : '停用'} ${u.username}`);
|
||||
res.json({ ok: true, disabled: u.disabled ? 0 : 1 });
|
||||
});
|
||||
app.post('/api/users/:id/resetpwd', requireAuth, requireSuper, (req, res) => {
|
||||
const { password } = req.body || {};
|
||||
if (!password || String(password).length < 6) return res.status(400).json({ error: '密码至少6位' });
|
||||
const u = udb.prepare('SELECT username FROM users WHERE id = ?').get(req.params.id);
|
||||
const info = udb.prepare('UPDATE users SET password = ? WHERE id = ?').run(hashPassword(String(password)), req.params.id);
|
||||
if (!info.changes) return res.status(404).json({ error: '不存在' });
|
||||
logAccess(req, req.user.username, 'user_resetpwd', `重置 ${u ? u.username : req.params.id} 密码`);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// ---------- 访问记录(超管) ----------
|
||||
app.get('/api/access-log', requireAuth, requireSuper, (req, res) => {
|
||||
res.json(udb.prepare('SELECT id, username, action, detail, ip, created_at FROM access_log ORDER BY id DESC LIMIT 500').all());
|
||||
});
|
||||
app.post('/api/access-log/clear', requireAuth, requireSuper, (req, res) => {
|
||||
udb.prepare('DELETE FROM access_log').run();
|
||||
logAccess(req, req.user.username, 'log_clear', '清空访问记录');
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user