1224 lines
35 KiB
JavaScript
1224 lines
35 KiB
JavaScript
const express = require('express');
|
||
const mysql = require('mysql2/promise');
|
||
const bcrypt = require('bcryptjs');
|
||
const jwt = require('jsonwebtoken');
|
||
const cors = require('cors');
|
||
const crypto = require('crypto');
|
||
const fs = require('fs'); // 使用同步版本的 fs
|
||
const fsPromises = require('fs').promises; // 使用 promises 版本的 fs
|
||
const path = require('path');
|
||
require('dotenv').config({ path: path.join(__dirname, '.env') });
|
||
const CryptoJS = require('crypto-js');
|
||
const util = require('util');
|
||
const fileUpload = require('express-fileupload');
|
||
|
||
const app = express();
|
||
|
||
// 创建日志文件流
|
||
const logStream = fs.createWriteStream(path.join(__dirname, 'server.log'), { flags: 'a' });
|
||
|
||
// 创建管理员日志文件流
|
||
const adminLogStream = fs.createWriteStream(path.join(__dirname, 'admin.log'), { flags: 'a' });
|
||
|
||
// 自定义日志函数
|
||
function log(message) {
|
||
const timestamp = new Date().toISOString();
|
||
const logMessage = `${timestamp} - ${message}\n`;
|
||
console.log(logMessage);
|
||
logStream.write(logMessage);
|
||
}
|
||
|
||
|
||
// 允许所有源的 CORS 请求
|
||
app.use(cors());
|
||
|
||
app.use(express.json());
|
||
app.use(fileUpload({
|
||
limits: { fileSize: 5 * 1024 * 1024 }, // 限制文件大小为5MB
|
||
abortOnLimit: true
|
||
}));
|
||
|
||
// 创建数据库连接池
|
||
const pool = mysql.createPool({
|
||
host: process.env.DB_HOST,
|
||
port: process.env.DB_PORT,
|
||
user: process.env.DB_USER,
|
||
password: process.env.DB_PASSWORD,
|
||
database: process.env.DB_NAME,
|
||
charset: 'utf8mb4'
|
||
});
|
||
|
||
// 创建 SurveyKing 数据库连接池
|
||
const surveyKingPool = mysql.createPool({
|
||
host: process.env.SurveyKing_DB_HOST,
|
||
port: process.env.SurveyKing_DB_PORT,
|
||
user: process.env.SurveyKing_DB_USER,
|
||
password: process.env.SurveyKing_DB_PASSWORD,
|
||
database: process.env.SurveyKing_DB_NAME,
|
||
charset: 'utf8mb4'
|
||
});
|
||
|
||
// 在文件顶部的导入语句之后添加
|
||
pool.getConnection()
|
||
.then(connection => {
|
||
log('Successfully connected to the database.');
|
||
connection.release();
|
||
})
|
||
.catch(err => {
|
||
log(`Error connecting to the database: ${err}`);
|
||
});
|
||
|
||
// 在文件顶部的导入语句之后添加
|
||
surveyKingPool.getConnection()
|
||
.then(connection => {
|
||
log('Successfully connected to the SurveyKing database.');
|
||
connection.release();
|
||
})
|
||
.catch(err => {
|
||
log(`Error connecting to the SurveyKing database: ${err}`);
|
||
});
|
||
|
||
|
||
// 验证令牌的中间件
|
||
const authenticateToken = async (req, res, next) => {
|
||
const authHeader = req.headers['authorization'];
|
||
const token = authHeader && authHeader.split(' ')[1];
|
||
|
||
if (!token) return res.sendStatus(401);
|
||
|
||
try {
|
||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||
const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [decoded.userId]);
|
||
|
||
if (rows.length === 0 || new Date() > new Date(rows[0].expiration_date)) {
|
||
return res.status(403).json({ error: '无效的令牌或账号已过期' });
|
||
}
|
||
|
||
// 检查token是否是最新的
|
||
if (token !== rows[0].active_token) {
|
||
return res.status(403).json({ error: '您的账号已在其他设备登录' });
|
||
}
|
||
|
||
req.user = rows[0];
|
||
next();
|
||
} catch (error) {
|
||
return res.status(403).json({ error: '无效的令牌' });
|
||
}
|
||
};
|
||
|
||
// 生成一个简单的密钥对
|
||
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
|
||
modulusLength: 2048,
|
||
publicKeyEncoding: {
|
||
type: 'spki',
|
||
format: 'pem'
|
||
},
|
||
privateKeyEncoding: {
|
||
type: 'pkcs8',
|
||
format: 'pem'
|
||
}
|
||
});
|
||
|
||
// 添加获取公钥的路由
|
||
app.get('/public-key', (req, res) => {
|
||
res.json({ publicKey });
|
||
});
|
||
|
||
|
||
// 登录路由
|
||
app.post('/login', async (req, res) => {
|
||
const { data, key, iv } = req.body;
|
||
|
||
try {
|
||
// 解密数据
|
||
const decrypted = CryptoJS.AES.decrypt(
|
||
data,
|
||
CryptoJS.enc.Base64.parse(key),
|
||
{
|
||
iv: CryptoJS.enc.Base64.parse(iv),
|
||
mode: CryptoJS.mode.CBC,
|
||
padding: CryptoJS.pad.Pkcs7
|
||
}
|
||
);
|
||
|
||
// 将解密后的数据转换为字符串
|
||
const decryptedString = decrypted.toString(CryptoJS.enc.Utf8);
|
||
|
||
// 解析 JSON 数据
|
||
const { student_id_or_username, password } = JSON.parse(decryptedString);
|
||
|
||
log(`Login attempt for: ${student_id_or_username}`);
|
||
|
||
try {
|
||
const [rows] = await pool.query('SELECT * FROM users WHERE username = ? OR student_id = ?', [student_id_or_username, student_id_or_username]);
|
||
|
||
log(`Database query result: ${rows}`);
|
||
|
||
if (rows.length === 0) {
|
||
log('User not found');
|
||
return res.status(401).json({ error: '用户名/邮箱或密码错误' });
|
||
}
|
||
|
||
const user = rows[0];
|
||
|
||
if (new Date() > new Date(user.expiration_date)) {
|
||
log(`Account expired for user: ${user.username}`);
|
||
return res.status(403).json({ error: '账户已过期,请联系系统管理员xxx' });
|
||
}
|
||
|
||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||
|
||
log(`Password validation result: ${isPasswordValid}`);
|
||
|
||
if (!isPasswordValid) {
|
||
log('Invalid password');
|
||
return res.status(401).json({ error: '用户名/学号或密码错误' });
|
||
}
|
||
|
||
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '24h' });
|
||
|
||
// 更新用户的active_token
|
||
await pool.query('UPDATE users SET active_token = ?, last_login = ? WHERE id = ?', [token, new Date(), user.id]);
|
||
|
||
log(`Login successful for user: ${user.username}`);
|
||
|
||
res.json({
|
||
success: true,
|
||
username: user.username,
|
||
token,
|
||
level: user.level,
|
||
model:LICENSE_INFO.model
|
||
});
|
||
} catch (error) {
|
||
log(`登录失败: ${error}`);
|
||
res.status(500).json({ error: '登录失败', details: error.message });
|
||
}
|
||
} catch (error) {
|
||
log(`登录解密失败: ${error}`);
|
||
res.status(500).json({
|
||
error: '登录失败',
|
||
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||
});
|
||
}
|
||
});
|
||
|
||
// 登入路由
|
||
app.post('/logout', authenticateToken, (req, res) => {
|
||
res.json({ success: true });
|
||
});
|
||
|
||
// 检认证状态
|
||
app.get('/check-auth', authenticateToken, async (req, res) => {
|
||
try {
|
||
const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [req.user.id]);
|
||
if (rows.length === 0 || new Date() > new Date(rows[0].expiration_date)) {
|
||
return res.status(403).json({ error: '账户已过期或无效' });
|
||
}
|
||
res.json({
|
||
isAuthenticated: true,
|
||
username: req.user.username,
|
||
level: req.user.level,
|
||
organization: req.user.organization,
|
||
});
|
||
} catch (error) {
|
||
log(`检查认证状态失败: ${error}`);
|
||
res.status(500).json({ error: '检查认证状态失败' });
|
||
}
|
||
});
|
||
|
||
// 验证令牌
|
||
app.post('/verify-token', async (req, res) => {
|
||
const { token } = req.body;
|
||
|
||
if (!token) {
|
||
return res.json({ valid: false });
|
||
}
|
||
|
||
try {
|
||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||
const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [decoded.userId]);
|
||
|
||
if (rows.length === 0 || new Date() > new Date(rows[0].expiration_date)) {
|
||
return res.json({ valid: false });
|
||
}
|
||
|
||
res.json({ valid: true, username: rows[0].username, level: rows[0].level });
|
||
} catch (error) {
|
||
res.json({ valid: false });
|
||
}
|
||
});
|
||
|
||
// 修改获取用户信息路由
|
||
app.get('/user-info', authenticateToken, async (req, res) => {
|
||
try {
|
||
const [rows] = await pool.query(
|
||
'SELECT username, class_name, student_id, organization, created_at, last_login, level FROM users WHERE id = ?',
|
||
[req.user.id]
|
||
);
|
||
|
||
if (rows.length > 0) {
|
||
res.json({
|
||
class_name: rows[0].class_name,
|
||
username: rows[0].username,
|
||
student_id: rows[0].student_id,
|
||
organization: LICENSE_INFO.user,
|
||
created_at: rows[0].created_at,
|
||
last_login: rows[0].last_login,
|
||
level: rows[0].level,
|
||
model:LICENSE_INFO.model
|
||
});
|
||
} else {
|
||
res.status(404).json({ error: '用户不存在' });
|
||
}
|
||
} catch (error) {
|
||
log(`获取用户信息失败: ${error}`);
|
||
res.status(500).json({ error: '获取用户信息失败' });
|
||
}
|
||
});
|
||
|
||
// 验证管理员权限
|
||
app.get('/verify-admin', authenticateToken, async (req, res) => {
|
||
try {
|
||
const [rows] = await pool.query('SELECT level FROM users WHERE id = ?', [req.user.id]);
|
||
if (rows.length > 0 && rows[0].level >= 7) {
|
||
res.json({ isAdmin: true });
|
||
} else {
|
||
res.json({ isAdmin: false });
|
||
}
|
||
} catch (error) {
|
||
log(`验证管理员权限失败: ${error}`);
|
||
res.status(500).json({ error: '验证管理员权限失败' });
|
||
}
|
||
});
|
||
|
||
// 查询用户
|
||
app.get('/admin/users', authenticateToken, async (req, res) => {
|
||
try {
|
||
const [userRows] = await pool.query('SELECT level FROM users WHERE id = ?', [req.user.id]);
|
||
if (userRows.length === 0 || userRows[0].level < 7) {
|
||
return res.status(403).json({ error: '没有权限访问此资源' });
|
||
}
|
||
|
||
const [rows] = await pool.query(
|
||
'SELECT id, username, student_id, class_name, organization, created_at, last_login, level FROM users'
|
||
);
|
||
res.json(rows);
|
||
} catch (error) {
|
||
log(`获取用户列表失败: ${error}`);
|
||
res.status(500).json({ error: '获取用户列表失败' });
|
||
}
|
||
});
|
||
|
||
// 批量创建用户
|
||
app.post('/admin/users', authenticateToken, async (req, res) => {
|
||
try {
|
||
// 权限校验:仅管理员(level >= 3)可执行
|
||
const [adminCheck] = await pool.query('SELECT level FROM users WHERE id = ?', [req.user.id]);
|
||
if (adminCheck.length === 0 || adminCheck[0].level < 7) {
|
||
return res.status(403).json({ success: false, error: '没有权限执行此操作' });
|
||
}
|
||
|
||
const { class_name, student_ids } = req.body;
|
||
if (!class_name || !student_ids) {
|
||
return res.status(400).json({ success: false, error: '请提供班级和学号' });
|
||
}
|
||
|
||
// 支持多种分隔符:空格、英文逗号、中文逗号、换行
|
||
const idList = student_ids
|
||
.split(/[\s,,\n]+/)
|
||
.map(id => id.trim())
|
||
.filter(id => id.length > 0);
|
||
|
||
if (idList.length === 0) {
|
||
return res.status(400).json({ success: false, error: '未检测到有效学号' });
|
||
}
|
||
|
||
// 根据LICENSE_INFO.model确定最大用户数量
|
||
let maxUsers = 0; // 默认值
|
||
const licenseModel = LICENSE_INFO.model || '';
|
||
|
||
if (licenseModel.includes('EST-05E')) {
|
||
maxUsers = 10;
|
||
} else if (licenseModel.includes('EST-10E')) {
|
||
maxUsers = 60;
|
||
} else if (licenseModel.includes('EST-100E')) {
|
||
maxUsers = 100;
|
||
} else if (licenseModel.includes('EST-05C')) {
|
||
maxUsers = 10;
|
||
} else if (licenseModel.includes('EST-10C')) {
|
||
maxUsers = 60;
|
||
} else if (licenseModel.includes('EST-100C')) {
|
||
maxUsers = 100;
|
||
} else if (licenseModel.includes('EST-10A')) {
|
||
maxUsers = 60;
|
||
} else if (licenseModel.includes('EST-100A')) {
|
||
maxUsers = 100;
|
||
}else if (licenseModel.includes('EST-100D')) {
|
||
maxUsers = 100;
|
||
}
|
||
|
||
// 检查当前用户数量
|
||
const [currentUsers] = await pool.query('SELECT COUNT(*) as count FROM users');
|
||
const currentUserCount = currentUsers[0].count -1;
|
||
|
||
// 计算可以创建的最大新用户数量
|
||
const maxNewUsers = maxUsers - currentUserCount;
|
||
|
||
// 如果新用户数量超过限制,返回错误
|
||
if (idList.length > maxNewUsers) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: `超出许可证用户数量限制,当前许可证(${licenseModel})最多允许${maxUsers}个用户,已有${currentUserCount}个用户,最多可创建${maxNewUsers}个新用户`
|
||
});
|
||
}
|
||
|
||
let createdCount = 0;
|
||
let skipped = [];
|
||
|
||
for (const sid of idList) {
|
||
// 检查是否已存在
|
||
const [exists] = await pool.query('SELECT id FROM users WHERE student_id = ?', [sid]);
|
||
if (exists.length > 0) {
|
||
skipped.push(sid);
|
||
continue;
|
||
}
|
||
|
||
const plainPassword = sid;
|
||
const password = await bcrypt.hash(plainPassword, 10); // saltRounds = 10
|
||
|
||
const organization = LICENSE_INFO.user;
|
||
const level = 0
|
||
|
||
await pool.query(
|
||
`INSERT INTO users (username, student_id, class_name, organization, level, password, created_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, NOW())`,
|
||
[sid, sid, class_name, organization, level, password]
|
||
);
|
||
|
||
createdCount++;
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
createdCount,
|
||
skipped,
|
||
message: `成功创建 ${createdCount} 个用户,跳过 ${skipped.length} 个已存在的用户`,
|
||
licenseInfo: {
|
||
model: licenseModel,
|
||
maxUsers: maxUsers,
|
||
currentUsers: currentUserCount + createdCount,
|
||
remainingSlots: maxUsers - (currentUserCount + createdCount)
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('批量创建用户失败:', error);
|
||
res.status(500).json({ success: false, error: '批量创建用户失败', details: error.message });
|
||
}
|
||
});
|
||
|
||
// 删除用户
|
||
app.delete('/admin/users/:student_id', authenticateToken, async (req, res) => {
|
||
try {
|
||
const [adminCheck] = await pool.query('SELECT level FROM users WHERE id = ?', [req.user.id]);
|
||
if (adminCheck.length === 0 || adminCheck[0].level < 7) {
|
||
return res.status(403).json({ success: false, error: '没有权限执行此操作' });
|
||
}
|
||
|
||
const studentId = req.params.student_id;
|
||
const [result] = await pool.query('DELETE FROM users WHERE student_id = ?', [studentId]);
|
||
if (result.affectedRows === 0) {
|
||
return res.status(404).json({ success: false, error: '未找到该用户' });
|
||
}
|
||
|
||
res.json({ success: true, message: `已删除用户 ${studentId}` });
|
||
} catch (error) {
|
||
console.error('删除用户失败:', error);
|
||
res.status(500).json({ success: false, error: '删除用户失败' });
|
||
}
|
||
});
|
||
|
||
|
||
let onlineUsers = new Map();
|
||
let onlineHistory = [];
|
||
|
||
const ONLINE_DATA_FILE = path.join(__dirname, 'online_data.json');
|
||
const LONG_TERM_HISTORY_FILE = path.join(__dirname, 'long_term_history.json');
|
||
|
||
// 加载在线用户数据
|
||
async function loadOnlineData() {
|
||
try {
|
||
const data = await fsPromises.readFile(ONLINE_DATA_FILE, 'utf8');
|
||
const parsedData = JSON.parse(data);
|
||
onlineHistory = parsedData.history;
|
||
onlineUsers = new Map(parsedData.users.map(([id, user]) => [parseInt(id), user]));
|
||
} catch (error) {
|
||
if (error.code !== 'ENOENT') {
|
||
console.error('加载在线用户数据失败:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 保存在线用户数据
|
||
async function saveOnlineData() {
|
||
try {
|
||
const data = JSON.stringify({
|
||
history: onlineHistory,
|
||
users: Array.from(onlineUsers.entries())
|
||
});
|
||
await fsPromises.writeFile(ONLINE_DATA_FILE, data, 'utf8');
|
||
} catch (error) {
|
||
console.error('保存在线用户数据失败:', error);
|
||
}
|
||
}
|
||
|
||
|
||
// 更新在线用户并记录历史
|
||
async function updateOnlineUsers() {
|
||
const now = new Date();
|
||
// 清理超时的用户(例如,15分钟无活动)
|
||
for (const [userId, userData] of onlineUsers.entries()) {
|
||
if (now - new Date(userData.lastActivity) > 5 * 60 * 1000) {
|
||
onlineUsers.delete(userId);
|
||
}
|
||
}
|
||
|
||
const currentOnlineUsers = {
|
||
time: now.toISOString(),
|
||
count: onlineUsers.size,
|
||
users: Array.from(onlineUsers.values()).map(u => u.username)
|
||
};
|
||
|
||
onlineHistory.push(currentOnlineUsers);
|
||
|
||
// 将超过24小时的记录移动到长期历史记录
|
||
const oneDayAgo = new Date(now - 24 * 60 * 60 * 1000);
|
||
const oldRecords = onlineHistory.filter(item => new Date(item.time) <= oneDayAgo);
|
||
onlineHistory = onlineHistory.filter(item => new Date(item.time) > oneDayAgo);
|
||
|
||
|
||
await saveOnlineData();
|
||
}
|
||
|
||
// 每10秒更新一次在线用户状态
|
||
setInterval(updateOnlineUsers, 10000);
|
||
|
||
// 在服务器启动时加载在线用户数据
|
||
loadOnlineData().then(() => {
|
||
console.log('在线用户数据加载完成');
|
||
});
|
||
|
||
// 登录路由
|
||
app.post('/login', async (req, res) => {
|
||
// ... 现有的登录逻辑 ...
|
||
|
||
if (response.data.success) {
|
||
// 添加用户到在线列表
|
||
onlineUsers.set(user.id, {
|
||
username: user.username,
|
||
lastActivity: new Date().toISOString()
|
||
});
|
||
await saveOnlineData();
|
||
}
|
||
|
||
res.json({ success: true });
|
||
});
|
||
|
||
// 获取在线用户和历史记录
|
||
app.get('/online-users', authenticateToken, (req, res) => {
|
||
res.json({
|
||
currentOnline: {
|
||
count: onlineUsers.size,
|
||
users: Array.from(onlineUsers.values()).map(u => u.username)
|
||
},
|
||
history: onlineHistory
|
||
});
|
||
});
|
||
|
||
// 更新用户活动时间
|
||
app.post('/update-activity', authenticateToken, async (req, res) => {
|
||
if (onlineUsers.has(req.user.id)) {
|
||
onlineUsers.get(req.user.id).lastActivity = new Date().toISOString();
|
||
} else {
|
||
onlineUsers.set(req.user.id, {
|
||
username: req.user.username,
|
||
lastActivity: new Date().toISOString()
|
||
});
|
||
}
|
||
await saveOnlineData();
|
||
res.sendStatus(200);
|
||
});
|
||
|
||
// 登出路由
|
||
app.post('/logout', authenticateToken, async (req, res) => {
|
||
onlineUsers.delete(req.user.id);
|
||
await saveOnlineData();
|
||
res.json({ success: true });
|
||
});
|
||
|
||
|
||
|
||
// --------------------------LIC验证-------------------
|
||
// 全局许可证信息
|
||
const LICENSE_INFO = {
|
||
isValid: false,
|
||
model: "",
|
||
user: "",
|
||
serial: "",
|
||
activation_code: "",
|
||
activated_at: "",
|
||
expires_at: "",
|
||
gold_service_expires_at: "",
|
||
issued_at: "",
|
||
issuer: "",
|
||
hardware_id: "" // 新增硬件码字段
|
||
};
|
||
|
||
// 获取系统硬件序列号
|
||
async function getHardwareSerial() {
|
||
try {
|
||
const hardwareSerial = await fsPromises.readFile('/hardware_serial', 'utf8');
|
||
return hardwareSerial.trim();
|
||
} catch (error) {
|
||
log(`读取硬件序列号失败: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// License 验证相关功能
|
||
// 获取目录下所有 .lic 文件
|
||
async function getLicenseFiles() {
|
||
try {
|
||
const licenseDir = path.join(__dirname, 'license');
|
||
const files = await fsPromises.readdir(licenseDir);
|
||
return files.filter(file => file.endsWith('.lic'));
|
||
} catch (error) {
|
||
if (error.code === 'ENOENT') {
|
||
// 如果目录不存在,返回空数组
|
||
return [];
|
||
}
|
||
log(`获取 License 文件失败: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 清空 LICENSE_INFO
|
||
function clearLicenseInfo() {
|
||
Object.keys(LICENSE_INFO).forEach(key => {
|
||
LICENSE_INFO[key] = typeof LICENSE_INFO[key] === 'boolean' ? false : "";
|
||
});
|
||
}
|
||
|
||
// 读取公钥文件
|
||
async function readPublicKey() {
|
||
try {
|
||
const pubKeyPath = path.join(__dirname, 'pub.pem');
|
||
const publicKeyData = await fsPromises.readFile(pubKeyPath, 'utf8');
|
||
return publicKeyData;
|
||
} catch (error) {
|
||
log(`读取公钥文件失败: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 验证 License 文件
|
||
async function verifyLicense(licenseFile, publicKey) {
|
||
try {
|
||
const licenseDir = path.join(__dirname, 'license');
|
||
const licenseData = await fsPromises.readFile(path.join(licenseDir, licenseFile), 'utf8');
|
||
const license = JSON.parse(licenseData);
|
||
|
||
// 解码 payload
|
||
const payloadStr = Buffer.from(license.payload, 'base64').toString('utf8');
|
||
const payload = JSON.parse(payloadStr);
|
||
|
||
// 验证签名
|
||
const verify = crypto.createVerify('SHA256');
|
||
verify.update(payloadStr);
|
||
const isValid = verify.verify(publicKey, license.signature, 'base64');
|
||
|
||
// 验证硬件码匹配
|
||
const hardwareSerial = await getHardwareSerial();
|
||
const hardwareMatches = payload.hardware_id === hardwareSerial;
|
||
|
||
// 只有在签名验证成功且硬件码匹配时才更新许可证信息
|
||
if (isValid && hardwareMatches) {
|
||
LICENSE_INFO.isValid = true;
|
||
LICENSE_INFO.model = payload.model || "";
|
||
LICENSE_INFO.user = payload.user || "";
|
||
LICENSE_INFO.serial = payload.serial || "";
|
||
LICENSE_INFO.activation_code = payload.activation_code || "";
|
||
LICENSE_INFO.activated_at = payload.activated_at || "";
|
||
LICENSE_INFO.expires_at = payload.expires_at || "";
|
||
LICENSE_INFO.gold_service_expires_at = payload.gold_service_expires_at || "";
|
||
LICENSE_INFO.issued_at = payload.issued_at || "";
|
||
LICENSE_INFO.issuer = payload.issuer || "";
|
||
LICENSE_INFO.hardware_id = payload.hardware_id || "";
|
||
} else {
|
||
clearLicenseInfo();
|
||
}
|
||
|
||
return {
|
||
isValid,
|
||
hardwareMatches,
|
||
licenseFile,
|
||
payload
|
||
};
|
||
} catch (error) {
|
||
log(`验证 License 文件失败 (${licenseFile}): ${error.message}`);
|
||
clearLicenseInfo();
|
||
return {
|
||
isValid: false,
|
||
hardwareMatches: false,
|
||
licenseFile,
|
||
error: error.message
|
||
};
|
||
}
|
||
}
|
||
|
||
// 验证所有 License 文件
|
||
async function verifyAllLicenses() {
|
||
try {
|
||
const licenseFiles = await getLicenseFiles();
|
||
if (licenseFiles.length === 0) {
|
||
log('未找到任何 License 文件');
|
||
clearLicenseInfo();
|
||
return [];
|
||
}
|
||
|
||
// 只验证最新的 license 文件
|
||
const latestLicenseFile = licenseFiles[licenseFiles.length - 1];
|
||
log(`验证最新的 License 文件: ${latestLicenseFile}`);
|
||
|
||
try {
|
||
const publicKey = await readPublicKey();
|
||
log('成功读取公钥文件 pub.pem');
|
||
|
||
const result = await verifyLicense(latestLicenseFile, publicKey);
|
||
|
||
// 输出验证结果
|
||
if (result.isValid && result.hardwareMatches) {
|
||
log(`License 验证成功: ${latestLicenseFile}`);
|
||
log(`License 信息: ${JSON.stringify(result.payload, null, 2)}`);
|
||
} else {
|
||
log(`License 验证失败: ${latestLicenseFile}`);
|
||
if (!result.isValid) {
|
||
log('签名验证失败');
|
||
}
|
||
if (!result.hardwareMatches) {
|
||
log('硬件码不匹配');
|
||
}
|
||
if (result.error) {
|
||
log(`错误信息: ${result.error}`);
|
||
}
|
||
clearLicenseInfo();
|
||
}
|
||
|
||
return [result];
|
||
} catch (error) {
|
||
log(`读取公钥失败: ${error.message}`);
|
||
clearLicenseInfo();
|
||
return [{
|
||
isValid: false,
|
||
hardwareMatches: false,
|
||
licenseFile: latestLicenseFile,
|
||
error: '无法读取公钥文件'
|
||
}];
|
||
}
|
||
} catch (error) {
|
||
log(`验证所有 License 文件失败: ${error.message}`);
|
||
clearLicenseInfo();
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 获取许可证信息
|
||
function getLicenseInfo() {
|
||
return LICENSE_INFO;
|
||
}
|
||
|
||
// 添加获取许可证信息的API端点
|
||
app.get('/license-info', authenticateToken, (req, res) => {
|
||
res.json({
|
||
success: true,
|
||
licenseInfo: LICENSE_INFO
|
||
});
|
||
});
|
||
|
||
// 添加获取产品型号的API端点
|
||
app.get('/product-model', (req, res) => {
|
||
res.json({
|
||
success: true,
|
||
isValid: LICENSE_INFO.isValid,
|
||
model: LICENSE_INFO.model
|
||
});
|
||
});
|
||
|
||
// 添加上传许可证文件的API端点
|
||
app.post('/upload-license', async (req, res) => {
|
||
if (!req.files || Object.keys(req.files).length === 0) {
|
||
return res.status(400).json({ success: false, error: '未上传文件' });
|
||
}
|
||
|
||
const licenseFile = req.files.license;
|
||
|
||
// 验证文件扩展名
|
||
if (!licenseFile.name.endsWith('.lic')) {
|
||
return res.status(400).json({ success: false, error: '文件必须是.lic格式' });
|
||
}
|
||
|
||
try {
|
||
// 验证硬件码匹配
|
||
const hardwareSerial = await getHardwareSerial();
|
||
|
||
// 读取并解析上传的 license 文件内容
|
||
const licenseContent = licenseFile.data.toString('utf8');
|
||
const license = JSON.parse(licenseContent);
|
||
const payloadStr = Buffer.from(license.payload, 'base64').toString('utf8');
|
||
const payload = JSON.parse(payloadStr);
|
||
|
||
// 检查硬件码是否匹配
|
||
if (payload.hardware_id !== hardwareSerial) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: '硬件码不匹配,无法使用此许可证'
|
||
});
|
||
}
|
||
|
||
// 确保 license 目录存在
|
||
const licenseDir = path.join(__dirname, 'license');
|
||
await fsPromises.mkdir(licenseDir, { recursive: true });
|
||
|
||
// 删除现有的所有 license 文件
|
||
const existingFiles = await getLicenseFiles();
|
||
for (const file of existingFiles) {
|
||
await fsPromises.unlink(path.join(licenseDir, file));
|
||
log(`删除旧的许可证文件: ${file}`);
|
||
}
|
||
|
||
// 保存新的 license 文件
|
||
await licenseFile.mv(path.join(licenseDir, licenseFile.name));
|
||
log(`新的许可证文件 ${licenseFile.name} 上传成功到 license 目录`);
|
||
|
||
// 重新验证许可证
|
||
await verifyAllLicenses();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: '许可证文件上传并验证成功',
|
||
licenseInfo: LICENSE_INFO
|
||
});
|
||
} catch (error) {
|
||
log(`许可证文件上传失败: ${error.message}`);
|
||
res.status(500).json({ success: false, error: '许可证文件上传失败' });
|
||
}
|
||
});
|
||
|
||
// 检查许可证状态的API端点
|
||
app.get('/license-status', (req, res) => {
|
||
res.json({
|
||
success: true,
|
||
isValid: LICENSE_INFO.isValid
|
||
});
|
||
});
|
||
|
||
// 监视 license 目录变化
|
||
const licenseDir = path.join(__dirname, 'license');
|
||
fs.mkdir(licenseDir, { recursive: true }, (err) => {
|
||
if (err) {
|
||
log(`创建 license 目录失败: ${err.message}`);
|
||
return;
|
||
}
|
||
|
||
fs.watch(licenseDir, async (eventType, filename) => {
|
||
if (filename && filename.endsWith('.lic')) {
|
||
log(`检测到 license 目录变化: ${eventType} - ${filename}`);
|
||
try {
|
||
await verifyAllLicenses();
|
||
} catch (error) {
|
||
log(`处理 license 目录变化时发生错误: ${error.message}`);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// 在服务器启动时验证 License 文件
|
||
(async function checkLicensesOnStartup() {
|
||
log('正在验证 License 文件...');
|
||
try {
|
||
await verifyAllLicenses();
|
||
log('License 验证完成');
|
||
} catch (error) {
|
||
log(`License 验证过程中发生错误: ${error.message}`);
|
||
}
|
||
})();
|
||
|
||
// ---------------------------------------------------------------
|
||
|
||
// -----------------添加查询 SurveyKing 答案数据的接口----------
|
||
app.get('/survey-answers', async (req, res) => {
|
||
try {
|
||
const { org } = req.query;
|
||
|
||
if (!org) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: '请提供组织名称参数 (org)'
|
||
});
|
||
}
|
||
|
||
// 基础查询
|
||
const query = `
|
||
SELECT
|
||
project_id,
|
||
answer,
|
||
exam_score,
|
||
update_at
|
||
FROM t_answer
|
||
WHERE 1=1
|
||
`;
|
||
|
||
// 执行查询
|
||
const [rows] = await surveyKingPool.query(query);
|
||
|
||
// 处理结果,解析 answer 列中的 JSON 数据并只返回匹配组织的数据
|
||
const processedData = rows.map(row => {
|
||
try {
|
||
const answerData = JSON.parse(row.answer);
|
||
|
||
// 查找包含 estorg 的键
|
||
let organization = '';
|
||
let username = '';
|
||
|
||
// 遍历对象查找 estorg
|
||
for (const key in answerData) {
|
||
if (answerData[key].estorg) {
|
||
organization = answerData[key].estorg;
|
||
}
|
||
if (answerData[key].estuser) {
|
||
username = answerData[key].estuser;
|
||
}
|
||
}
|
||
|
||
// 只返回匹配组织的数据
|
||
if (organization && organization.toLowerCase() === org.toLowerCase()) {
|
||
return {
|
||
projectId: row.project_id,
|
||
organization: organization,
|
||
username: username,
|
||
score: row.exam_score,
|
||
submitTime: row.update_at
|
||
};
|
||
}
|
||
return null;
|
||
} catch (error) {
|
||
console.error(`解析答案数据失败: ${error}`);
|
||
return null;
|
||
}
|
||
}).filter(item => item !== null); // 移除不匹配的数据
|
||
|
||
// 按提交时间降序排序
|
||
processedData.sort((a, b) => new Date(b.submitTime) - new Date(a.submitTime));
|
||
|
||
res.json({
|
||
success: true,
|
||
total: processedData.length,
|
||
data: processedData
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error(`获取答案数据失败: ${error}`);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: '获取答案数据失败',
|
||
message: error.message
|
||
});
|
||
}
|
||
});
|
||
// --------------------------------------------------
|
||
|
||
// -----------场景化功能切换API-----------------------
|
||
app.post('/admin/toggle-scenario', authenticateToken, async (req, res) => {
|
||
try {
|
||
// 验证管理员权限
|
||
const [adminCheck] = await pool.query('SELECT level FROM users WHERE id = ?', [req.user.id]);
|
||
if (adminCheck.length === 0 || adminCheck[0].level < 7) {
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: '没有权限执行此操作'
|
||
});
|
||
}
|
||
|
||
const { student_id, new_level } = req.body;
|
||
|
||
// 如果是要关闭场景化功能,直接执行
|
||
if (new_level === 0) {
|
||
const [updateResult] = await pool.query(
|
||
'UPDATE users SET level = ? WHERE student_id = ?',
|
||
[0, student_id]
|
||
);
|
||
|
||
if (updateResult.affectedRows === 0) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: '未找到该用户'
|
||
});
|
||
}
|
||
|
||
return res.json({
|
||
success: true,
|
||
message: '已关闭场景化功能',
|
||
new_level: 0
|
||
});
|
||
}
|
||
|
||
// 如果是要开启场景化功能,需要检查许可证和当前已开启的用户数量
|
||
// 根据LICENSE_INFO.model确定最大用户数
|
||
let maxScenarioUsers = 0;
|
||
const model = LICENSE_INFO.model;
|
||
|
||
switch (model) {
|
||
case 'EST-05E':
|
||
maxScenarioUsers = 5;
|
||
break;
|
||
case 'EST-10E':
|
||
maxScenarioUsers = 30;
|
||
break;
|
||
case 'EST-100E':
|
||
maxScenarioUsers = 50;
|
||
break;
|
||
case 'EST-05C':
|
||
maxScenarioUsers = 5;
|
||
break;
|
||
case 'EST-10C':
|
||
maxScenarioUsers = 30;
|
||
break;
|
||
case 'EST-100C':
|
||
maxScenarioUsers = 50;
|
||
break;
|
||
case 'EST-10A':
|
||
maxScenarioUsers = 30;
|
||
break;
|
||
case 'EST-100A':
|
||
maxScenarioUsers = 50;
|
||
break;
|
||
case 'EST-100D':
|
||
maxScenarioUsers = 50;
|
||
break;
|
||
default:
|
||
maxScenarioUsers = 0;
|
||
}
|
||
|
||
// 如果没有有效的许可证或型号不支持
|
||
if (!LICENSE_INFO.isValid || maxScenarioUsers === 0) {
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: '无有效许可证或当前型号不支持场景化功能'
|
||
});
|
||
}
|
||
|
||
// 查询当前已开启场景化功能的用户数量(level=1或level=4)
|
||
const [countResult] = await pool.query(
|
||
'SELECT COUNT(*) as count FROM users WHERE level = 1 OR level = 4'
|
||
);
|
||
|
||
const currentCount = countResult[0].count;
|
||
|
||
// 如果当前数量已达到最大值,拒绝请求
|
||
if (currentCount >= maxScenarioUsers) {
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: `已达到最大场景化用户数量限制${maxScenarioUsers}位用户`
|
||
});
|
||
}
|
||
|
||
// 更新用户等级为1(开启场景化功能)
|
||
const [updateResult] = await pool.query(
|
||
'UPDATE users SET level = ? WHERE student_id = ?',
|
||
[1, student_id]
|
||
);
|
||
|
||
if (updateResult.affectedRows === 0) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: '未找到该用户'
|
||
});
|
||
}
|
||
|
||
return res.json({
|
||
success: true,
|
||
message: '已开启场景化功能',
|
||
new_level: 1
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('切换场景化功能失败:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: '操作失败,请稍后再试',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// ---------------------------------------------------
|
||
|
||
|
||
|
||
// ---------------------宿主机网络配置----------------------------
|
||
// 网络配置相关 API
|
||
const NETWORK_CONFIG_FILE = path.join(__dirname, 'network');
|
||
|
||
// 读取网络配置
|
||
async function readNetworkConfig() {
|
||
try {
|
||
const config = await fsPromises.readFile(NETWORK_CONFIG_FILE, 'utf8');
|
||
const configObj = {};
|
||
config.split('\n').forEach(line => {
|
||
const [key, value] = line.split('=').map(part => part.trim());
|
||
if (key && value) {
|
||
// 移除引号
|
||
configObj[key] = value.replace(/^"(.*)"$/, '$1');
|
||
}
|
||
});
|
||
return configObj;
|
||
} catch (error) {
|
||
log(`读取网络配置失败: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 写入网络配置
|
||
async function writeNetworkConfig(config) {
|
||
try {
|
||
let configContent = '';
|
||
for (const [key, value] of Object.entries(config)) {
|
||
// DNS 需要特殊处理,添加引号
|
||
if (key === 'DNS') {
|
||
configContent += `${key}="${value}"\n`;
|
||
} else {
|
||
configContent += `${key}=${value}\n`;
|
||
}
|
||
}
|
||
await fsPromises.writeFile(NETWORK_CONFIG_FILE, configContent);
|
||
} catch (error) {
|
||
log(`写入网络配置失败: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 获取网络配置 API
|
||
app.get('/network-config', authenticateToken, async (req, res) => {
|
||
try {
|
||
const config = await readNetworkConfig();
|
||
res.json({
|
||
success: true,
|
||
config
|
||
});
|
||
} catch (error) {
|
||
res.status(500).json({
|
||
success: false,
|
||
error: '获取网络配置失败'
|
||
});
|
||
}
|
||
});
|
||
|
||
// 更新网络配置 API
|
||
app.post('/network-config', authenticateToken, async (req, res) => {
|
||
try {
|
||
const { config } = req.body;
|
||
|
||
// 基本验证
|
||
if (!config || typeof config !== 'object') {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: '无效的配置数据'
|
||
});
|
||
}
|
||
|
||
if (!config.BOOTPROTO) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: '缺少必需字段: BOOTPROTO'
|
||
});
|
||
}
|
||
// 限定 BOOTPROTO 的取值
|
||
if (!['dhcp', 'static'].includes(config.BOOTPROTO)) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: 'BOOTPROTO 仅支持 dhcp 或 static'
|
||
});
|
||
}
|
||
|
||
// 如果是静态 IP,验证额外字段
|
||
if (config.BOOTPROTO === 'static') {
|
||
const staticFields = ['IPADDR', 'NETMASK', 'GATEWAY', 'DNS'];
|
||
for (const field of staticFields) {
|
||
if (!config[field]) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: `静态 IP 配置缺少必需字段: ${field}`
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
const existingConfig = await readNetworkConfig();
|
||
const allowedUpdateKeys = ['BOOTPROTO', 'IPADDR', 'NETMASK', 'GATEWAY', 'DNS'];
|
||
const updatedConfig = { ...existingConfig };
|
||
for (const key of allowedUpdateKeys) {
|
||
if (config[key] !== undefined) {
|
||
updatedConfig[key] = config[key];
|
||
}
|
||
}
|
||
|
||
if (config.BOOTPROTO === 'dhcp') {
|
||
for (const key of ['IPADDR', 'NETMASK', 'GATEWAY', 'DNS']) {
|
||
delete updatedConfig[key];
|
||
}
|
||
}
|
||
|
||
await writeNetworkConfig(updatedConfig);
|
||
res.json({
|
||
success: true,
|
||
message: '网络配置已更新'
|
||
});
|
||
} catch (error) {
|
||
res.status(500).json({
|
||
success: false,
|
||
error: '更新网络配置失败'
|
||
});
|
||
}
|
||
});
|
||
|
||
// ------------------------------------------------------
|
||
|
||
// -------------------------API启动设置-----------------
|
||
const PORT = process.env.PORT || 3000;
|
||
// 如果作为主模块运行(独立启动),才开启监听;作为子服务被挂载时不监听端口
|
||
if (require.main === module) {
|
||
const PORT = process.env.PORT || 3000;
|
||
app.listen(PORT, () => {
|
||
log(`Server running on port ${PORT}`);
|
||
});
|
||
}
|
||
|
||
// 导出 Express 应用,供主入口统一挂载
|
||
module.exports = app;
|
||
|
||
// 在程序退出时关闭日志流
|
||
process.on('exit', () => {
|
||
logStream.end();
|
||
});
|
||
|
||
// 捕获未捕获的异常并记录日志
|
||
process.on('uncaughtException', (error) => {
|
||
log(`Uncaught Exception: ${error.message}`);
|
||
process.exit(1);
|
||
});
|
||
|
||
// 捕获未处理的 Promise 拒绝并记录日志
|
||
process.on('unhandledRejection', (reason, promise) => {
|
||
log(`Unhandled Rejection at: ${promise}, reason: ${reason}`);
|
||
});
|