update
This commit is contained in:
@@ -622,3 +622,140 @@ export const disable2FA = async (req: Request, res: Response): Promise<void> =>
|
||||
res.status(500).json({ message: '禁用两步验证时发生错误。', error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否需要进行初始设置 (GET /api/v1/auth/needs-setup)
|
||||
* 如果数据库中没有用户,则需要设置。
|
||||
*/
|
||||
export const needsSetup = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const userCount = await new Promise<number>((resolve, reject) => {
|
||||
db.get('SELECT COUNT(*) as count FROM users', (err, row: { count: number }) => {
|
||||
if (err) {
|
||||
console.error('检查 users 表时出错:', err.message);
|
||||
return reject(new Error('数据库查询失败'));
|
||||
}
|
||||
resolve(row ? row.count : 0); // 如果表为空,row 可能为 undefined
|
||||
});
|
||||
});
|
||||
|
||||
res.status(200).json({ needsSetup: userCount === 0 });
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查设置状态时发生内部错误:', error);
|
||||
// 如果检查失败,保守起见返回 false,避免用户卡在设置页面
|
||||
res.status(500).json({ message: '检查设置状态时发生错误。', needsSetup: false });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理初始管理员账号设置请求 (POST /api/v1/auth/setup)
|
||||
*/
|
||||
export const setupAdmin = async (req: Request, res: Response): Promise<void> => {
|
||||
const { username, password, confirmPassword } = req.body;
|
||||
|
||||
// 1. 基本输入验证
|
||||
if (!username || !password || !confirmPassword) {
|
||||
res.status(400).json({ message: '用户名、密码和确认密码不能为空。' });
|
||||
return;
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
res.status(400).json({ message: '两次输入的密码不匹配。' });
|
||||
return;
|
||||
}
|
||||
if (password.length < 8) {
|
||||
res.status(400).json({ message: '密码长度至少需要 8 位。' });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 2. 检查数据库中是否已存在用户 (关键安全检查)
|
||||
const userCount = await new Promise<number>((resolve, reject) => {
|
||||
db.get('SELECT COUNT(*) as count FROM users', (err, row: { count: number }) => {
|
||||
if (err) {
|
||||
console.error('检查 users 表时出错 (setupAdmin):', err.message);
|
||||
return reject(new Error('数据库查询失败'));
|
||||
}
|
||||
resolve(row ? row.count : 0);
|
||||
});
|
||||
});
|
||||
|
||||
if (userCount > 0) {
|
||||
console.warn('尝试在已有用户的情况下执行初始设置。');
|
||||
res.status(403).json({ message: '设置已完成,无法重复执行。' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 哈希密码
|
||||
const saltRounds = 10;
|
||||
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
// 4. 插入新用户
|
||||
const newUser = await new Promise<{ id: number }>((resolveInsert, rejectInsert) => {
|
||||
const stmt = db.prepare(
|
||||
`INSERT INTO users (username, hashed_password, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
);
|
||||
// 使用 function(this: RunResult) 来获取 lastID
|
||||
stmt.run(username, hashedPassword, now, now, function (this: RunResult, err: Error | null) {
|
||||
if (err) {
|
||||
console.error('创建初始管理员时出错:', err.message);
|
||||
// 检查是否是唯一约束错误
|
||||
if (err.message.includes('UNIQUE constraint failed: users.username')) {
|
||||
return rejectInsert(new Error('用户名已存在。')); // 虽然理论上不应发生,但以防万一
|
||||
}
|
||||
return rejectInsert(new Error('创建初始管理员失败'));
|
||||
}
|
||||
// 获取新插入用户的 ID
|
||||
resolveInsert({ id: this.lastID });
|
||||
});
|
||||
stmt.finalize((finalizeErr) => {
|
||||
if (finalizeErr) {
|
||||
console.error('Finalizing statement failed:', finalizeErr.message);
|
||||
// 如果 finalize 失败,可能插入已完成,但最好还是通知错误
|
||||
rejectInsert(new Error('创建初始管理员时发生错误 (finalize)'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
console.log(`初始管理员账号 '${username}' (ID: ${newUser.id}) 已成功创建。`);
|
||||
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP
|
||||
// 记录审计日志 (添加 IP)
|
||||
auditLogService.logAction('ADMIN_SETUP_COMPLETE', { userId: newUser.id, username, ip: clientIp });
|
||||
|
||||
res.status(201).json({ message: '初始管理员账号创建成功!' });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('初始设置过程中发生内部错误:', error);
|
||||
res.status(500).json({ message: error.message || '初始设置过程中发生内部服务器错误。' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理用户登出请求 (POST /api/v1/auth/logout)
|
||||
*/
|
||||
export const logout = (req: Request, res: Response): void => {
|
||||
const userId = req.session.userId; // 获取用户 ID 用于日志记录
|
||||
const username = req.session.username;
|
||||
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
console.error(`销毁用户 ${userId} (${username}) 的会话时出错:`, err);
|
||||
// 即使销毁失败,也尝试让前端认为已登出
|
||||
res.status(500).json({ message: '登出时发生服务器内部错误。' });
|
||||
} else {
|
||||
console.log(`用户 ${userId} (${username}) 已成功登出。`);
|
||||
// 清除客户端的 session cookie (通常 connect-sqlite3 会处理,但显式设置更保险)
|
||||
res.clearCookie('connect.sid'); // 'connect.sid' 是 express-session 的默认 cookie 名称
|
||||
// 记录审计日志
|
||||
if (userId) { // 仅在能获取到 userId 时记录
|
||||
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown';
|
||||
auditLogService.logAction('LOGOUT', { userId, username, ip: clientIp });
|
||||
}
|
||||
res.status(200).json({ message: '已成功登出。' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -8,13 +8,22 @@ import {
|
||||
disable2FA,
|
||||
getAuthStatus, // 导入获取状态的方法
|
||||
generatePasskeyRegistrationOptions, // 导入 Passkey 方法
|
||||
verifyPasskeyRegistration // 导入 Passkey 方法
|
||||
verifyPasskeyRegistration, // 导入 Passkey 方法
|
||||
needsSetup, // 导入 needsSetup 控制器
|
||||
setupAdmin, // 导入 setupAdmin 控制器
|
||||
logout // *** 新增:导入 logout 控制器 ***
|
||||
} from './auth.controller';
|
||||
import { isAuthenticated } from './auth.middleware';
|
||||
import { ipBlacklistCheckMiddleware } from './ipBlacklistCheck.middleware'; // 导入 IP 黑名单检查中间件
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/v1/auth/needs-setup - 检查是否需要初始设置 (公开访问)
|
||||
router.get('/needs-setup', needsSetup);
|
||||
|
||||
// POST /api/v1/auth/setup - 执行初始管理员设置 (公开访问,控制器内部检查)
|
||||
router.post('/setup', setupAdmin);
|
||||
|
||||
// POST /api/v1/auth/login - 用户登录接口 (添加黑名单检查)
|
||||
router.post('/login', ipBlacklistCheckMiddleware, login);
|
||||
|
||||
@@ -46,9 +55,11 @@ router.post('/passkey/register-options', isAuthenticated, generatePasskeyRegistr
|
||||
router.post('/passkey/verify-registration', isAuthenticated, verifyPasskeyRegistration);
|
||||
|
||||
|
||||
// POST /api/v1/auth/logout - 用户登出接口 (公开访问)
|
||||
router.post('/logout', logout);
|
||||
|
||||
// 未来可以添加的其他认证相关路由
|
||||
// router.post('/logout', logout); // 登出
|
||||
// router.get('/status', getStatus); // 获取当前登录状态
|
||||
// router.post('/setup', setupAdmin); // 用于首次创建管理员账号的接口
|
||||
// router.post('/setup', setupAdmin); // 已移到上面
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -141,36 +141,7 @@ const initializeDatabase = async () => {
|
||||
});
|
||||
});
|
||||
|
||||
if (userCount === 0) {
|
||||
console.warn('------------------------------------------------------');
|
||||
console.warn('警告: 数据库中未找到任何用户。正在创建默认管理员...');
|
||||
// 创建默认管理员
|
||||
const defaultAdminUsername = 'admin';
|
||||
const defaultAdminPassword = 'adminpassword'; // 仅用于首次创建
|
||||
const saltRounds = 10;
|
||||
const hashedPassword = await bcrypt.hash(defaultAdminPassword, saltRounds);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
await new Promise<void>((resolveInsert, rejectInsert) => {
|
||||
const stmt = db.prepare(
|
||||
`INSERT INTO users (username, hashed_password, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
);
|
||||
stmt.run(defaultAdminUsername, hashedPassword, now, now, function (err: Error | null) {
|
||||
if (err) {
|
||||
console.error('创建默认管理员时出错:', err.message);
|
||||
return rejectInsert(new Error('创建默认管理员失败'));
|
||||
}
|
||||
console.log(`默认管理员 '${defaultAdminUsername}' (密码: '${defaultAdminPassword}') 已创建。请尽快修改密码!`);
|
||||
resolveInsert();
|
||||
});
|
||||
stmt.finalize();
|
||||
});
|
||||
console.warn('------------------------------------------------------');
|
||||
|
||||
} else {
|
||||
console.log(`数据库中找到 ${userCount} 个用户。`);
|
||||
}
|
||||
// 检查用户数量后不再执行任何操作 (移除了自动创建和日志记录)
|
||||
|
||||
console.log('数据库初始化检查完成。');
|
||||
} catch (error) {
|
||||
|
||||
@@ -52,7 +52,8 @@ export type AuditLogActionType =
|
||||
// System/Error
|
||||
| 'SERVER_STARTED'
|
||||
| 'SERVER_ERROR' // Log significant backend errors
|
||||
| 'DATABASE_MIGRATION';
|
||||
| 'DATABASE_MIGRATION'
|
||||
| 'ADMIN_SETUP_COMPLETE'; // *** 新增:初始管理员设置完成 ***
|
||||
|
||||
// 审计日志条目的结构 (从数据库读取时)
|
||||
export interface AuditLogEntry {
|
||||
|
||||
Reference in New Issue
Block a user