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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:11:12 +08:00

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;