- 删除文章:先删引用它的发布记录和实体关联再删文章(之前顺序反了,数据库外键拒绝删除),并放在一个事务里,失败时不会只删掉一半 - 管理员登录状态存到数据库(admin_sessions,只存令牌哈希),服务重启或部署后不用重新登录;登录过期时后台回到登录页并提示,而不是操作一直失败 - 内容系统发布的文章可修改知识中心列表里的摘要 - 文章缺少扩展记录时,修改分类和发布时间也能保存 - 提示文字改为“约 1 分钟内在网站生效”(前台页面缓存 60 秒) - 新增不连数据库的单元测试 tests/adminFixes.unit.test.js(其他测试会写 .env 里的库) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
2.7 KiB
JavaScript
59 lines
2.7 KiB
JavaScript
const crypto = require('crypto');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const { getDb } = require('../config/database');
|
|
|
|
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');
|
|
return `${salt}:${hash}`;
|
|
};
|
|
|
|
const verifyPassword = (password, stored) => {
|
|
const [salt, expected] = String(stored || '').split(':');
|
|
if (!salt || !expected) return false;
|
|
const actual = crypto.scryptSync(password, salt, 64).toString('hex');
|
|
return crypto.timingSafeEqual(Buffer.from(actual, 'hex'), Buffer.from(expected, 'hex'));
|
|
};
|
|
|
|
class AdminAuthService {
|
|
static login(username, password) {
|
|
const user = getDb().prepare('SELECT * FROM admin_users WHERE username = ?').get(username);
|
|
if (!user || !verifyPassword(password, user.password_hash)) {
|
|
throw new Error('用户名或密码错误');
|
|
}
|
|
const token = crypto.randomBytes(32).toString('hex');
|
|
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) {
|
|
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) {
|
|
if (!newPassword || newPassword.length < 8) throw new Error('新密码至少需要8位');
|
|
const db = getDb();
|
|
const user = db.prepare('SELECT * FROM admin_users WHERE username = ?').get(username);
|
|
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);
|
|
db.prepare('DELETE FROM admin_sessions WHERE username = ?').run(username);
|
|
return { success: true };
|
|
}
|
|
|
|
static logout(token) {
|
|
if (token) getDb().prepare('DELETE FROM admin_sessions WHERE token_hash = ?').run(tokenHash(token));
|
|
}
|
|
}
|
|
|
|
module.exports = AdminAuthService;
|