server.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. const express = require('express');
  2. const fs = require('fs').promises;
  3. const path = require('path');
  4. const bodyParser = require('body-parser');
  5. const session = require('express-session');
  6. const bcrypt = require('bcrypt');
  7. const crypto = require('crypto');
  8. const logger = require('morgan'); // 引入 morgan 作为日志工具
  9. const axios = require('axios'); // 用于发送 HTTP 请求
  10. const Docker = require('dockerode');
  11. const app = express();
  12. const cors = require('cors');
  13. let docker = null;
  14. async function initDocker() {
  15. if (docker === null) {
  16. docker = new Docker();
  17. try {
  18. await docker.ping();
  19. console.log('成功连接到 Docker 守护进程');
  20. } catch (err) {
  21. console.error('无法连接到 Docker 守护进程:', err);
  22. docker = null;
  23. }
  24. }
  25. return docker;
  26. }
  27. app.use(cors());
  28. app.use(express.json());
  29. app.use(express.static('web'));
  30. app.use(bodyParser.urlencoded({ extended: true }));
  31. app.use(session({
  32. secret: 'OhTq3faqSKoxbV%NJV',
  33. resave: false,
  34. saveUninitialized: true,
  35. cookie: { secure: false } // 设置为true如果使用HTTPS
  36. }));
  37. app.use(logger('dev')); // 使用 morgan 记录请求日志
  38. app.get('/admin', (req, res) => {
  39. res.sendFile(path.join(__dirname, 'web', 'admin.html'));
  40. });
  41. // 新增:Docker Hub 搜索 API
  42. app.get('/api/search', async (req, res) => {
  43. const searchTerm = req.query.term;
  44. if (!searchTerm) {
  45. return res.status(400).json({ error: 'Search term is required' });
  46. }
  47. try {
  48. const response = await axios.get(`https://hub.docker.com/v2/search/repositories/?query=${encodeURIComponent(searchTerm)}`);
  49. res.json(response.data);
  50. } catch (error) {
  51. console.error('Error searching Docker Hub:', error);
  52. res.status(500).json({ error: 'Failed to search Docker Hub' });
  53. }
  54. });
  55. const CONFIG_FILE = path.join(__dirname, 'config.json');
  56. const USERS_FILE = path.join(__dirname, 'users.json');
  57. const DOCUMENTATION_DIR = path.join(__dirname, 'documentation');
  58. const DOCUMENTATION_FILE = path.join(__dirname, 'documentation.md');
  59. // 读取配置
  60. async function readConfig() {
  61. try {
  62. const data = await fs.readFile(CONFIG_FILE, 'utf8');
  63. // 确保 data 不为空或不完整
  64. if (!data.trim()) {
  65. console.warn('Config file is empty, returning default config');
  66. return {
  67. logo: '',
  68. menuItems: [],
  69. adImages: []
  70. };
  71. }
  72. console.log('Config read successfully');
  73. return JSON.parse(data);
  74. } catch (error) {
  75. console.error('Failed to read config:', error);
  76. if (error.code === 'ENOENT') {
  77. return {
  78. logo: '',
  79. menuItems: [],
  80. adImages: []
  81. };
  82. }
  83. throw error;
  84. }
  85. }
  86. // 写入配置
  87. async function writeConfig(config) {
  88. try {
  89. await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
  90. console.log('Config saved successfully');
  91. } catch (error) {
  92. console.error('Failed to save config:', error);
  93. throw error;
  94. }
  95. }
  96. // 读取用户
  97. async function readUsers() {
  98. try {
  99. const data = await fs.readFile(USERS_FILE, 'utf8');
  100. return JSON.parse(data);
  101. } catch (error) {
  102. if (error.code === 'ENOENT') {
  103. console.warn('Users file does not exist, creating default user');
  104. const defaultUser = { username: 'root', password: bcrypt.hashSync('admin', 10) };
  105. await writeUsers([defaultUser]);
  106. return { users: [defaultUser] };
  107. }
  108. throw error;
  109. }
  110. }
  111. // 写入用户
  112. async function writeUsers(users) {
  113. await fs.writeFile(USERS_FILE, JSON.stringify({ users }, null, 2), 'utf8');
  114. }
  115. // 确保 documentation 目录存在
  116. async function ensureDocumentationDir() {
  117. try {
  118. await fs.access(DOCUMENTATION_DIR);
  119. } catch (error) {
  120. if (error.code === 'ENOENT') {
  121. await fs.mkdir(DOCUMENTATION_DIR);
  122. } else {
  123. throw error;
  124. }
  125. }
  126. }
  127. // 读取文档
  128. async function readDocumentation() {
  129. try {
  130. await ensureDocumentationDir();
  131. const files = await fs.readdir(DOCUMENTATION_DIR);
  132. const documents = await Promise.all(files.map(async file => {
  133. const filePath = path.join(DOCUMENTATION_DIR, file);
  134. const content = await fs.readFile(filePath, 'utf8');
  135. const doc = JSON.parse(content);
  136. return {
  137. id: path.parse(file).name,
  138. title: doc.title,
  139. content: doc.content,
  140. published: doc.published
  141. };
  142. }));
  143. const publishedDocuments = documents.filter(doc => doc.published);
  144. return publishedDocuments;
  145. } catch (error) {
  146. console.error('Error reading documentation:', error);
  147. throw error;
  148. }
  149. }
  150. // 写入文档
  151. async function writeDocumentation(content) {
  152. await fs.writeFile(DOCUMENTATION_FILE, content, 'utf8');
  153. }
  154. // 登录验证
  155. app.post('/api/login', async (req, res) => {
  156. const { username, password, captcha } = req.body;
  157. console.log(`Received login request for user: ${username}`); // 打印登录请求的用户名
  158. if (req.session.captcha !== parseInt(captcha)) {
  159. console.log(`Captcha verification failed for user: ${username}`); // 打印验证码验证失败
  160. return res.status(401).json({ error: '验证码错误' });
  161. }
  162. const users = await readUsers();
  163. const user = users.users.find(u => u.username === username);
  164. if (!user) {
  165. console.log(`User ${username} not found`); // 打印用户未找到
  166. return res.status(401).json({ error: '用户名或密码错误' });
  167. }
  168. console.log(`User ${username} found, comparing passwords`); // 打印用户找到,开始比较密码
  169. if (bcrypt.compareSync(password, user.password)) {
  170. console.log(`User ${username} logged in successfully`); // 打印登录成功
  171. req.session.user = user;
  172. res.json({ success: true });
  173. } else {
  174. console.log(`Login failed for user: ${username}, password mismatch`); // 打印密码不匹配
  175. res.status(401).json({ error: '用户名或密码错误' });
  176. }
  177. });
  178. // 修改密码
  179. app.post('/api/change-password', async (req, res) => {
  180. if (!req.session.user) {
  181. return res.status(401).json({ error: 'Not logged in' });
  182. }
  183. const { currentPassword, newPassword } = req.body;
  184. const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/;
  185. if (!passwordRegex.test(newPassword)) {
  186. return res.status(400).json({ error: 'Password must be 8-16 characters long and contain at least one letter, one number, and one special character' });
  187. }
  188. const users = await readUsers();
  189. const user = users.users.find(u => u.username === req.session.user.username);
  190. if (user && bcrypt.compareSync(currentPassword, user.password)) {
  191. user.password = bcrypt.hashSync(newPassword, 10);
  192. await writeUsers(users.users);
  193. res.json({ success: true });
  194. } else {
  195. res.status(401).json({ error: 'Invalid current password' });
  196. }
  197. });
  198. // 需要登录验证的中间件
  199. function requireLogin(req, res, next) {
  200. console.log('Session:', req.session); // 添加这行
  201. if (req.session.user) {
  202. next();
  203. } else {
  204. console.log('用户未登录'); // 添加这行
  205. res.status(401).json({ error: 'Not logged in' });
  206. }
  207. }
  208. // API 端点:获取配置
  209. app.get('/api/config', async (req, res) => {
  210. try {
  211. const config = await readConfig();
  212. res.json(config);
  213. } catch (error) {
  214. res.status(500).json({ error: 'Failed to read config' });
  215. }
  216. });
  217. // API 端点:保存配置
  218. app.post('/api/config', requireLogin, async (req, res) => {
  219. try {
  220. const currentConfig = await readConfig();
  221. const newConfig = { ...currentConfig, ...req.body };
  222. await writeConfig(newConfig);
  223. res.json({ success: true });
  224. } catch (error) {
  225. res.status(500).json({ error: 'Failed to save config' });
  226. }
  227. });
  228. // API 端点:检查会话状态
  229. app.get('/api/check-session', (req, res) => {
  230. if (req.session.user) {
  231. res.json({ success: true });
  232. } else {
  233. res.status(401).json({ error: 'Not logged in' });
  234. }
  235. });
  236. // API 端点:生成验证码
  237. app.get('/api/captcha', (req, res) => {
  238. const num1 = Math.floor(Math.random() * 10);
  239. const num2 = Math.floor(Math.random() * 10);
  240. const captcha = `${num1} + ${num2} = ?`;
  241. req.session.captcha = num1 + num2;
  242. res.json({ captcha });
  243. });
  244. // API端点:获取文档列表
  245. app.get('/api/documentation-list', requireLogin, async (req, res) => {
  246. try {
  247. const files = await fs.readdir(DOCUMENTATION_DIR);
  248. const documents = await Promise.all(files.map(async file => {
  249. const content = await fs.readFile(path.join(DOCUMENTATION_DIR, file), 'utf8');
  250. const doc = JSON.parse(content);
  251. return { id: path.parse(file).name, ...doc };
  252. }));
  253. res.json(documents);
  254. } catch (error) {
  255. res.status(500).json({ error: '读取文档列表失败' });
  256. }
  257. });
  258. // API端点:保存文档
  259. app.post('/api/documentation', requireLogin, async (req, res) => {
  260. try {
  261. const { id, title, content } = req.body;
  262. const docId = id || Date.now().toString();
  263. const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`);
  264. await fs.writeFile(docPath, JSON.stringify({ title, content, published: false }));
  265. res.json({ success: true });
  266. } catch (error) {
  267. res.status(500).json({ error: '保存文档失败' });
  268. }
  269. });
  270. // API端点:删除文档
  271. app.delete('/api/documentation/:id', requireLogin, async (req, res) => {
  272. try {
  273. const docPath = path.join(DOCUMENTATION_DIR, `${req.params.id}.json`);
  274. await fs.unlink(docPath);
  275. res.json({ success: true });
  276. } catch (error) {
  277. res.status(500).json({ error: '删除文档失败' });
  278. }
  279. });
  280. // API端点:切换文档发布状态
  281. app.post('/api/documentation/:id/toggle-publish', requireLogin, async (req, res) => {
  282. try {
  283. const docPath = path.join(DOCUMENTATION_DIR, `${req.params.id}.json`);
  284. const content = await fs.readFile(docPath, 'utf8');
  285. const doc = JSON.parse(content);
  286. doc.published = !doc.published;
  287. await fs.writeFile(docPath, JSON.stringify(doc));
  288. res.json({ success: true });
  289. } catch (error) {
  290. res.status(500).json({ error: '更改发布状态失败' });
  291. }
  292. });
  293. // API端点:获取文档
  294. app.get('/api/documentation', async (req, res) => {
  295. try {
  296. const documents = await readDocumentation();
  297. res.json(documents);
  298. } catch (error) {
  299. console.error('Error in /api/documentation:', error);
  300. res.status(500).json({ error: '读取文档失败', details: error.message });
  301. }
  302. });
  303. // API端点:保存文档
  304. app.post('/api/documentation', requireLogin, async (req, res) => {
  305. try {
  306. const { content } = req.body;
  307. await writeDocumentation(content);
  308. res.json({ success: true });
  309. } catch (error) {
  310. res.status(500).json({ error: '保存文档失败' });
  311. }
  312. });
  313. // 获取文档列表函数
  314. async function getDocumentList() {
  315. try {
  316. await ensureDocumentationDir();
  317. const files = await fs.readdir(DOCUMENTATION_DIR);
  318. console.log('Files in documentation directory:', files);
  319. const documents = await Promise.all(files.map(async file => {
  320. try {
  321. const filePath = path.join(DOCUMENTATION_DIR, file);
  322. const content = await fs.readFile(filePath, 'utf8');
  323. return {
  324. id: path.parse(file).name,
  325. title: path.parse(file).name, // 使用文件名作为标题
  326. content: content,
  327. published: true // 假设所有文档都是已发布的
  328. };
  329. } catch (fileError) {
  330. console.error(`Error reading file ${file}:`, fileError);
  331. return null;
  332. }
  333. }));
  334. const validDocuments = documents.filter(doc => doc !== null);
  335. console.log('Valid documents:', validDocuments);
  336. return validDocuments;
  337. } catch (error) {
  338. console.error('Error reading document list:', error);
  339. throw error; // 重新抛出错误,让上层函数处理
  340. }
  341. }
  342. app.get('/api/documentation-list', async (req, res) => {
  343. try {
  344. const documents = await getDocumentList();
  345. res.json(documents);
  346. } catch (error) {
  347. console.error('Error in /api/documentation-list:', error);
  348. res.status(500).json({
  349. error: '读取文档列表失败',
  350. details: error.message,
  351. stack: error.stack
  352. });
  353. }
  354. });
  355. app.get('/api/documentation/:id', async (req, res) => {
  356. try {
  357. const docId = req.params.id;
  358. const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`);
  359. const content = await fs.readFile(docPath, 'utf8');
  360. const doc = JSON.parse(content);
  361. res.json(doc);
  362. } catch (error) {
  363. console.error('Error reading document:', error);
  364. res.status(500).json({ error: '读取文档失败', details: error.message });
  365. }
  366. });
  367. // API端点来获取Docker容器状态
  368. app.get('/api/docker-status', requireLogin, async (req, res) => {
  369. try {
  370. const docker = await initDocker();
  371. if (!docker) {
  372. return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
  373. }
  374. const containers = await docker.listContainers({ all: true });
  375. const containerStatus = await Promise.all(containers.map(async (container) => {
  376. const containerInfo = await docker.getContainer(container.Id).inspect();
  377. const stats = await docker.getContainer(container.Id).stats({ stream: false });
  378. // 计算 CPU 使用率
  379. const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
  380. const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
  381. const cpuUsage = (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100;
  382. // 计算内存使用率
  383. const memoryUsage = stats.memory_stats.usage / stats.memory_stats.limit * 100;
  384. return {
  385. id: container.Id.slice(0, 12),
  386. name: container.Names[0].replace(/^\//, ''),
  387. image: container.Image,
  388. state: containerInfo.State.Status,
  389. status: container.Status,
  390. cpu: cpuUsage.toFixed(2) + '%',
  391. memory: memoryUsage.toFixed(2) + '%',
  392. created: new Date(container.Created * 1000).toLocaleString()
  393. };
  394. }));
  395. res.json(containerStatus);
  396. } catch (error) {
  397. console.error('获取 Docker 状态时出错:', error);
  398. res.status(500).json({ error: '获取 Docker 状态失败', details: error.message });
  399. }
  400. });
  401. // API端点:重启容器
  402. app.post('/api/docker/restart/:id', requireLogin, async (req, res) => {
  403. try {
  404. const docker = await initDocker();
  405. if (!docker) {
  406. return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
  407. }
  408. const container = docker.getContainer(req.params.id);
  409. await container.restart();
  410. res.json({ success: true });
  411. } catch (error) {
  412. console.error('重启容器失败:', error);
  413. res.status(500).json({ error: '重启容器失败', details: error.message });
  414. }
  415. });
  416. // API端点:停止容器
  417. app.post('/api/docker/stop/:id', requireLogin, async (req, res) => {
  418. try {
  419. const docker = await initDocker();
  420. if (!docker) {
  421. return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
  422. }
  423. const container = docker.getContainer(req.params.id);
  424. await container.stop();
  425. res.json({ success: true });
  426. } catch (error) {
  427. console.error('停止容器失败:', error);
  428. res.status(500).json({ error: '停止容器失败', details: error.message });
  429. }
  430. });
  431. // API端点:获取单个容器的状态
  432. app.get('/api/docker/status/:id', requireLogin, async (req, res) => {
  433. try {
  434. const docker = await initDocker();
  435. if (!docker) {
  436. return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
  437. }
  438. const container = docker.getContainer(req.params.id);
  439. const containerInfo = await container.inspect();
  440. res.json({ state: containerInfo.State.Status });
  441. } catch (error) {
  442. console.error('获取容器状态失败:', error);
  443. res.status(500).json({ error: '获取容器状态失败', details: error.message });
  444. }
  445. });
  446. // 启动服务器
  447. const PORT = process.env.PORT || 3000;
  448. app.listen(PORT, () => {
  449. console.log(`Server is running on http://localhost:${PORT}`);
  450. });