Files
nexus-terminal/packages/backend/src/index.ts
T
Baobhan Sith c026a42d06 update
2025-04-15 18:59:56 +08:00

178 lines
8.0 KiB
TypeScript

import express = require('express');
// import express = require('express'); // 移除重复导入
import { Request, Response, NextFunction, RequestHandler } from 'express'; // 添加 RequestHandler
import http from 'http'; // 引入 http 模块
import session from 'express-session';
import connectSqlite3 from 'connect-sqlite3';
import path from 'path'; // 需要 path 模块
import bcrypt from 'bcrypt'; // 引入 bcrypt 用于哈希密码
import { getDb } from './database';
import { runMigrations } from './migrations';
import authRouter from './auth/auth.routes'; // 导入认证路由
import connectionsRouter from './connections/connections.routes';
import sftpRouter from './sftp/sftp.routes';
import proxyRoutes from './proxies/proxies.routes'; // 导入代理路由
import tagsRouter from './tags/tags.routes'; // 导入标签路由
import settingsRoutes from './settings/settings.routes'; // 导入设置路由
import notificationRoutes from './notifications/notification.routes'; // 导入通知路由
import auditRoutes from './audit/audit.routes'; // 导入审计路由
import { initializeWebSocket } from './websocket';
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware'; // 导入 IP 白名单中间件
// 基础 Express 应用设置 (后续会扩展)
const app = express();
const server = http.createServer(app); // 创建 HTTP 服务器实例
// --- 信任代理设置 (用于正确获取 req.ip) ---
// 如果应用部署在反向代理后面,需要设置此项
// 'true' 信任直接连接的代理;更安全的做法是配置具体的代理 IP 或子网
app.set('trust proxy', true);
// --- 结束信任代理设置 ---
// --- 会话存储设置 ---
const SQLiteStore = connectSqlite3(session);
const dbPath = path.resolve(__dirname, '../../data'); // 数据库目录路径
// --- 中间件 ---
// !! 重要:IP 白名单应尽可能早地应用,通常在其他中间件之前 !!
app.use(ipWhitelistMiddleware as RequestHandler); // 应用 IP 白名单中间件 (使用类型断言)
app.use(express.json()); // 添加此行以解析 JSON 请求体
// 会话中间件配置
// TODO: 将 secret 移到环境变量中,不要硬编码在代码里!
const sessionSecret = process.env.SESSION_SECRET || 'a-very-insecure-secret-for-dev';
if (sessionSecret === 'a-very-insecure-secret-for-dev') {
console.warn('警告:正在使用默认的不安全会话密钥,请在生产环境中设置 SESSION_SECRET 环境变量!');
}
app.use(session({
// 使用类型断言 (as any) 来解决 @types/connect-sqlite3 和 @types/express-session 的类型冲突
store: new SQLiteStore({
db: 'nexus-terminal.db', // 数据库文件名
dir: dbPath, // 数据库文件所在目录
table: 'sessions' // 存储会话的表名 (会自动创建)
}) as any,
secret: sessionSecret,
resave: false, // 强制保存 session 即使它没有变化 (通常为 false)
saveUninitialized: false, // 强制将未初始化的 session 存储 (通常为 false)
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7, // Cookie 有效期:7天 (毫秒)
httpOnly: true, // 防止客户端脚本访问 cookie
secure: process.env.NODE_ENV === 'production' // 仅在 HTTPS 下发送 cookie (生产环境)
}
}));
// 将 session 中间件保存到一个变量,以便传递给 WebSocket 初始化函数
const sessionMiddleware = session({
// 使用类型断言 (as any) 来解决 @types/connect-sqlite3 和 @types/express-session 的类型冲突
store: new SQLiteStore({
db: 'nexus-terminal.db', // 数据库文件名
dir: dbPath, // 数据库文件所在目录
table: 'sessions' // 存储会话的表名 (会自动创建)
}) as any,
secret: sessionSecret,
resave: false, // 强制保存 session 即使它没有变化 (通常为 false)
saveUninitialized: false, // 强制将未初始化的 session 存储 (通常为 false)
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7, // Cookie 有效期:7天 (毫秒)
httpOnly: true, // 防止客户端脚本访问 cookie
secure: process.env.NODE_ENV === 'production' // 仅在 HTTPS 下发送 cookie (生产环境)
}
});
app.use(sessionMiddleware); // 应用会话中间件
// 扩展 Express Request 类型以包含 session 数据 (如果需要更明确的类型提示)
declare module 'express-session' {
interface SessionData {
userId?: number; // 存储登录用户的 ID
username?: string;
}
}
const port = process.env.PORT || 3001; // 示例端口,可配置
// --- API 路由 ---
app.use('/api/v1/auth', authRouter);
app.use('/api/v1/connections', connectionsRouter);
app.use('/api/v1/sftp', sftpRouter);
app.use('/api/v1/proxies', proxyRoutes); // 挂载代理相关的路由
app.use('/api/v1/tags', tagsRouter); // 挂载标签相关的路由
app.use('/api/v1/settings', settingsRoutes); // 挂载设置相关的路由
app.use('/api/v1/notifications', notificationRoutes); // 挂载通知相关的路由
app.use('/api/v1/audit-logs', auditRoutes); // 挂载审计日志相关的路由
// 状态检查接口
app.get('/api/v1/status', (req: Request, res: Response) => {
res.json({ status: '后端服务运行中!' }); // 响应也改为中文
});
// 在服务器启动前初始化数据库并执行迁移
const initializeDatabase = async () => {
try {
const db = getDb(); // 获取数据库实例 (同时会建立连接)
await runMigrations(db); // 执行数据库迁移 (创建表)
// console.log('数据库迁移执行成功。'); // 日志已移至 migrations.ts
// 检查管理员用户是否存在
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(err);
}
resolve(row.count);
});
});
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) {
console.error('数据库初始化失败:', error);
process.exit(1); // 如果数据库初始化失败,则退出进程
}
};
// 启动 HTTP 服务器 (而不是直接 app.listen)
const startServer = () => {
server.listen(port, () => { // 使用 server.listen
console.log(`后端服务器正在监听 http://localhost:${port}`);
// 初始化 WebSocket 服务器,并传入 HTTP 服务器实例和会话解析器
initializeWebSocket(server, sessionMiddleware as RequestHandler);
});
};
// 先执行数据库初始化,成功后再启动服务器
initializeDatabase().then(startServer);