修复后台删不掉内容系统发布的文章,登录状态重启后不再失效

- 删除文章:先删引用它的发布记录和实体关联再删文章(之前顺序反了,数据库外键拒绝删除),并放在一个事务里,失败时不会只删掉一半
- 管理员登录状态存到数据库(admin_sessions,只存令牌哈希),服务重启或部署后不用重新登录;登录过期时后台回到登录页并提示,而不是操作一直失败
- 内容系统发布的文章可修改知识中心列表里的摘要
- 文章缺少扩展记录时,修改分类和发布时间也能保存
- 提示文字改为“约 1 分钟内在网站生效”(前台页面缓存 60 秒)
- 新增不连数据库的单元测试 tests/adminFixes.unit.test.js(其他测试会写 .env 里的库)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
amb
2026-10-19 16:11:12 +08:00
co-authored by Claude Opus 5.5
parent 8da2962bfa
commit b07394eed2
8 changed files with 145 additions and 28 deletions
+14 -13
View File
@@ -2,7 +2,9 @@ const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const { getDb } = require('../config/database');
const sessions = new Map();
const SESSION_MS = 8 * 60 * 60 * 1000;
// Only a hash of the token is stored, so the table never holds a usable login.
const tokenHash = (token) => crypto.createHash('sha256').update(String(token)).digest('hex');
const hashPassword = (password, salt = crypto.randomBytes(16).toString('hex')) => {
const hash = crypto.scryptSync(password, salt, 64).toString('hex');
@@ -23,17 +25,18 @@ class AdminAuthService {
throw new Error('用户名或密码错误');
}
const token = crypto.randomBytes(32).toString('hex');
sessions.set(token, { userId: user.user_id, username: user.username, expiresAt: Date.now() + 8 * 60 * 60 * 1000 });
return { token, username: user.username, expiresIn: 8 * 60 * 60 };
const db = getDb();
db.prepare('DELETE FROM admin_sessions WHERE expires_at < ?').run(Date.now());
db.prepare('INSERT INTO admin_sessions (token_hash, user_id, username, expires_at) VALUES (?, ?, ?, ?)')
.run(tokenHash(token), user.user_id, user.username, Date.now() + SESSION_MS);
return { token, username: user.username, expiresIn: SESSION_MS / 1000 };
}
static verify(token) {
const session = sessions.get(token);
if (!session || session.expiresAt < Date.now()) {
if (token) sessions.delete(token);
return null;
}
return session;
if (!token) return null;
const row = getDb().prepare('SELECT user_id, username, expires_at FROM admin_sessions WHERE token_hash = ?').get(tokenHash(token));
if (!row || Number(row.expires_at) < Date.now()) return null;
return { userId: row.user_id, username: row.username, expiresAt: Number(row.expires_at) };
}
static changePassword(username, currentPassword, newPassword) {
@@ -43,14 +46,12 @@ class AdminAuthService {
if (!user || !verifyPassword(currentPassword, user.password_hash)) throw new Error('当前密码错误');
db.prepare('UPDATE admin_users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE username = ?')
.run(hashPassword(newPassword), username);
for (const [token, session] of sessions.entries()) {
if (session.username === username) sessions.delete(token);
}
db.prepare('DELETE FROM admin_sessions WHERE username = ?').run(username);
return { success: true };
}
static logout(token) {
sessions.delete(token);
if (token) getDb().prepare('DELETE FROM admin_sessions WHERE token_hash = ?').run(tokenHash(token));
}
}