浏览代码

Merge develop

Nicolas Meienberger 3 年之前
父节点
当前提交
96555d884b

+ 1 - 0
.gitignore

@@ -1,3 +1,4 @@
+.pnpm-debug.log
 .env
 .env
 .env*
 .env*
 node_modules/
 node_modules/

+ 3 - 0
packages/dashboard/package.json

@@ -30,12 +30,15 @@
     "zustand": "^3.7.2"
     "zustand": "^3.7.2"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@babel/core": "^7.0.0",
     "@types/js-cookie": "^3.0.2",
     "@types/js-cookie": "^3.0.2",
     "@types/node": "17.0.31",
     "@types/node": "17.0.31",
     "@types/react": "18.0.8",
     "@types/react": "18.0.8",
     "@types/react-dom": "18.0.3",
     "@types/react-dom": "18.0.3",
     "@types/validator": "^13.7.2",
     "@types/validator": "^13.7.2",
     "@typescript-eslint/eslint-plugin": "^5.18.0",
     "@typescript-eslint/eslint-plugin": "^5.18.0",
+    "@typescript-eslint/parser": "^5.0.0",
+    "eslint-plugin-import": "^2.25.3",
     "autoprefixer": "^10.4.4",
     "autoprefixer": "^10.4.4",
     "eslint": "8.12.0",
     "eslint": "8.12.0",
     "eslint-config-airbnb-typescript": "^17.0.0",
     "eslint-config-airbnb-typescript": "^17.0.0",

+ 1 - 1
packages/system-api/.eslintignore

@@ -1,3 +1,3 @@
 node_modules/
 node_modules/
 dist/
 dist/
-*.cjs
+*.cjs

+ 1 - 1
packages/system-api/.eslintrc.cjs

@@ -1,5 +1,5 @@
 module.exports = {
 module.exports = {
-  env: { node: true },
+  env: { node: true, jest: true },
   extends: ['airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript'],
   extends: ['airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript'],
   parser: '@typescript-eslint/parser',
   parser: '@typescript-eslint/parser',
   parserOptions: {
   parserOptions: {

+ 7 - 0
packages/system-api/jest.config.cjs

@@ -0,0 +1,7 @@
+/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
+module.exports = {
+  preset: 'ts-jest',
+  testEnvironment: 'node',
+  testMatch: ['**/__tests__/**/*.test.ts'],
+  setupFiles: ['dotenv/config'],
+};

+ 13 - 8
packages/system-api/package.json

@@ -2,12 +2,15 @@
   "name": "system-api",
   "name": "system-api",
   "version": "1.0.0",
   "version": "1.0.0",
   "description": "",
   "description": "",
-  "main": "src/server.ts",
+  "exports": "./dist/server.js",
   "type": "module",
   "type": "module",
+  "engines": {
+    "node": ">=14.16"
+  },
   "scripts": {
   "scripts": {
     "clean": "rimraf dist",
     "clean": "rimraf dist",
     "lint": "eslint . --ext .ts",
     "lint": "eslint . --ext .ts",
-    "test": "echo \"Error: no test specified\" && exit 1",
+    "test": "jest",
     "build-prod": "esbuild --bundle src/server.ts --outdir=dist --allow-overwrite --sourcemap --platform=node --minify --analyze=verbose --external:./node_modules/* --format=esm",
     "build-prod": "esbuild --bundle src/server.ts --outdir=dist --allow-overwrite --sourcemap --platform=node --minify --analyze=verbose --external:./node_modules/* --format=esm",
     "build:watch": "esbuild --bundle src/server.ts --outdir=dist --allow-overwrite --sourcemap --platform=node --external:./node_modules/* --format=esm --watch",
     "build:watch": "esbuild --bundle src/server.ts --outdir=dist --allow-overwrite --sourcemap --platform=node --external:./node_modules/* --format=esm --watch",
     "start:dev": "NODE_ENV=development nodemon --trace-deprecation --trace-warnings --watch dist dist/server.js",
     "start:dev": "NODE_ENV=development nodemon --trace-deprecation --trace-warnings --watch dist dist/server.js",
@@ -24,7 +27,7 @@
     "dotenv": "^16.0.0",
     "dotenv": "^16.0.0",
     "express": "^4.17.3",
     "express": "^4.17.3",
     "helmet": "^5.0.2",
     "helmet": "^5.0.2",
-    "internal-ip": "^7.0.0",
+    "internal-ip": "^6.0.0",
     "jsonwebtoken": "^8.5.1",
     "jsonwebtoken": "^8.5.1",
     "node-port-scanner": "^3.0.1",
     "node-port-scanner": "^3.0.1",
     "p-iteration": "^1.1.8",
     "p-iteration": "^1.1.8",
@@ -41,23 +44,25 @@
     "@types/cookie-parser": "^1.4.3",
     "@types/cookie-parser": "^1.4.3",
     "@types/cors": "^2.8.12",
     "@types/cors": "^2.8.12",
     "@types/express": "^4.17.13",
     "@types/express": "^4.17.13",
+    "@types/jest": "^27.5.0",
     "@types/jsonwebtoken": "^8.5.8",
     "@types/jsonwebtoken": "^8.5.8",
     "@types/passport": "^1.0.7",
     "@types/passport": "^1.0.7",
     "@types/passport-http-bearer": "^1.0.37",
     "@types/passport-http-bearer": "^1.0.37",
     "@types/tcp-port-used": "^1.0.1",
     "@types/tcp-port-used": "^1.0.1",
     "@types/validator": "^13.7.2",
     "@types/validator": "^13.7.2",
+    "@typescript-eslint/eslint-plugin": "^5.18.0",
+    "@typescript-eslint/parser": "^5.22.0",
     "concurrently": "^7.1.0",
     "concurrently": "^7.1.0",
     "esbuild": "^0.14.32",
     "esbuild": "^0.14.32",
     "eslint": "^8.13.0",
     "eslint": "^8.13.0",
     "eslint-config-airbnb-typescript": "^17.0.0",
     "eslint-config-airbnb-typescript": "^17.0.0",
-    "eslint-config-hardcore": "^24.5.0",
     "eslint-config-prettier": "^8.5.0",
     "eslint-config-prettier": "^8.5.0",
-    "eslint-config-react": "^1.1.7",
     "eslint-plugin-import": "^2.26.0",
     "eslint-plugin-import": "^2.26.0",
     "eslint-plugin-prettier": "^4.0.0",
     "eslint-plugin-prettier": "^4.0.0",
-    "eslint-plugin-react": "^7.29.4",
-    "eslint-plugin-unicorn": "^42.0.0",
+    "jest": "^28.1.0",
     "nodemon": "^2.0.15",
     "nodemon": "^2.0.15",
-    "prettier": "2.6.2"
+    "prettier": "2.6.2",
+    "ts-jest": "^28.0.2",
+    "typescript": "4.6.4"
   }
   }
 }
 }

+ 5 - 1
packages/system-api/src/config/config.ts

@@ -7,7 +7,11 @@ interface IConfig {
   CLIENT_URLS: string[];
   CLIENT_URLS: string[];
 }
 }
 
 
-dotenv.config();
+if (process.env.NODE_ENV === 'test') {
+  dotenv.config({ path: '.env.test' });
+} else {
+  dotenv.config();
+}
 
 
 const { NODE_ENV = 'development', ROOT_FOLDER = '', JWT_SECRET = '', INTERNAL_IP = '' } = process.env;
 const { NODE_ENV = 'development', ROOT_FOLDER = '', JWT_SECRET = '', INTERNAL_IP = '' } = process.env;
 
 

+ 7 - 0
packages/system-api/src/modules/apps/__tests__/apps.service.test.ts

@@ -0,0 +1,7 @@
+import AppsService from '../apps.service';
+
+describe('Install app', () => {
+  it('Should throw when app is not available', () => {
+    expect(AppsService.installApp('not-available', {})).rejects.toThrow('App not-available not available');
+  });
+});

+ 12 - 108
packages/system-api/src/modules/apps/apps.controller.ts

@@ -1,34 +1,7 @@
 import { NextFunction, Request, Response } from 'express';
 import { NextFunction, Request, Response } from 'express';
-import si from 'systeminformation';
-import { appNames } from '../../config/apps';
+import AppsService from './apps.service';
 import { AppConfig } from '../../config/types';
 import { AppConfig } from '../../config/types';
-import { createFolder, fileExists, readJsonFile, writeFile, readFile } from '../fs/fs.helpers';
-import { checkAppExists, checkAppRequirements, checkEnvFile, ensureAppState, getInitalFormValues, runAppScript } from './apps.helpers';
-
-type AppsState = { installed: string };
-
-const getStateFile = (): AppsState => {
-  return readJsonFile('/state/apps.json');
-};
-
-const generateEnvFile = (appName: string, form: Record<string, string>) => {
-  const configFile: AppConfig = readJsonFile(`/apps/${appName}/config.json`);
-  const baseEnvFile = readFile('/.env').toString();
-  let envFile = `${baseEnvFile}\nAPP_PORT=${configFile.port}\n`;
-
-  Object.keys(configFile.form_fields).forEach((key) => {
-    const value = form[key];
-
-    if (value) {
-      const envVar = configFile.form_fields[key].env_variable;
-      envFile += `${envVar}=${value}\n`;
-    } else if (configFile.form_fields[key].required) {
-      throw new Error(`Variable ${key} is required`);
-    }
-  });
-
-  writeFile(`/app-data/${appName}/app.env`, envFile);
-};
+import { getInitalFormValues } from './apps.helpers';
 
 
 const uninstallApp = async (req: Request, res: Response, next: NextFunction) => {
 const uninstallApp = async (req: Request, res: Response, next: NextFunction) => {
   try {
   try {
@@ -38,11 +11,7 @@ const uninstallApp = async (req: Request, res: Response, next: NextFunction) =>
       throw new Error('App name is required');
       throw new Error('App name is required');
     }
     }
 
 
-    checkAppExists(appName);
-    ensureAppState(appName, false);
-
-    // Run script
-    await runAppScript(['uninstall', appName]);
+    await AppsService.uninstallApp(appName);
 
 
     res.status(200).json({ message: 'App uninstalled successfully' });
     res.status(200).json({ message: 'App uninstalled successfully' });
   } catch (e) {
   } catch (e) {
@@ -58,9 +27,7 @@ const stopApp = async (req: Request, res: Response, next: NextFunction) => {
       throw new Error('App name is required');
       throw new Error('App name is required');
     }
     }
 
 
-    checkAppExists(appName);
-    // Run script
-    await runAppScript(['stop', appName]);
+    await AppsService.stopApp(appName);
 
 
     res.status(200).json({ message: 'App stopped successfully' });
     res.status(200).json({ message: 'App stopped successfully' });
   } catch (e) {
   } catch (e) {
@@ -77,8 +44,7 @@ const updateAppConfig = async (req: Request, res: Response, next: NextFunction)
       throw new Error('App name is required');
       throw new Error('App name is required');
     }
     }
 
 
-    checkAppExists(appName);
-    generateEnvFile(appName, form);
+    AppsService.updateAppConfig(appName, form);
 
 
     res.status(200).json({ message: 'App updated successfully' });
     res.status(200).json({ message: 'App updated successfully' });
   } catch (e) {
   } catch (e) {
@@ -94,15 +60,9 @@ const getAppInfo = async (req: Request, res: Response<AppConfig>, next: NextFunc
       throw new Error('App name is required');
       throw new Error('App name is required');
     }
     }
 
 
-    const dockerContainers = await si.dockerContainers();
-    const configFile: AppConfig = readJsonFile(`/apps/${id}/config.json`);
-
-    const state = getStateFile();
-    const installed: string[] = state.installed.split(' ').filter(Boolean);
-    configFile.installed = installed.includes(id);
-    configFile.status = (dockerContainers.find((container) => container.name === `${id}`)?.state as 'running') || 'stopped';
+    const appInfo = await AppsService.getAppInfo(id);
 
 
-    res.status(200).json(configFile);
+    res.status(200).json(appInfo);
   } catch (e) {
   } catch (e) {
     next(e);
     next(e);
   }
   }
@@ -110,25 +70,7 @@ const getAppInfo = async (req: Request, res: Response<AppConfig>, next: NextFunc
 
 
 const listApps = async (req: Request, res: Response, next: NextFunction) => {
 const listApps = async (req: Request, res: Response, next: NextFunction) => {
   try {
   try {
-    const apps = appNames
-      .map((app) => {
-        try {
-          return readJsonFile(`/apps/${app}/config.json`);
-        } catch {
-          return null;
-        }
-      })
-      .filter(Boolean);
-
-    const dockerContainers = await si.dockerContainers();
-
-    const state = getStateFile();
-    const installed: string[] = state.installed.split(' ').filter(Boolean);
-
-    apps.forEach((app) => {
-      app.installed = installed.includes(app.id);
-      app.status = dockerContainers.find((container) => container.name === `${app.id}`)?.state || 'stopped';
-    });
+    const apps = await AppsService.listApps();
 
 
     res.status(200).json(apps);
     res.status(200).json(apps);
   } catch (e) {
   } catch (e) {
@@ -138,23 +80,13 @@ const listApps = async (req: Request, res: Response, next: NextFunction) => {
 
 
 const startApp = async (req: Request, res: Response, next: NextFunction) => {
 const startApp = async (req: Request, res: Response, next: NextFunction) => {
   try {
   try {
-    const { id: appName } = req.params;
+    const { id } = req.params;
 
 
-    if (!appName) {
+    if (!id) {
       throw new Error('App name is required');
       throw new Error('App name is required');
     }
     }
 
 
-    checkAppExists(appName);
-    checkEnvFile(appName);
-
-    // Regenerate env file
-    const form = getInitalFormValues(appName);
-    generateEnvFile(appName, form);
-
-    // Run script
-    await runAppScript(['start', appName]);
-
-    ensureAppState(appName, true);
+    await AppsService.startApp(id);
 
 
     res.status(200).json({ message: 'App started successfully' });
     res.status(200).json({ message: 'App started successfully' });
   } catch (e) {
   } catch (e) {
@@ -171,35 +103,7 @@ const installApp = async (req: Request, res: Response, next: NextFunction) => {
       throw new Error('App name is required');
       throw new Error('App name is required');
     }
     }
 
 
-    const appIsAvailable = appNames.includes(id);
-
-    if (!appIsAvailable) {
-      throw new Error(`App ${id} not available`);
-    }
-
-    const appExists = fileExists(`/app-data/${id}`);
-
-    if (appExists) {
-      await startApp(req, res, next);
-    } else {
-      const appIsValid = await checkAppRequirements(id);
-
-      if (!appIsValid) {
-        throw new Error(`App ${id} requirements not met`);
-      }
-
-      // Create app folder
-      createFolder(`/app-data/${id}`);
-
-      // Create env file
-      generateEnvFile(id, form);
-      ensureAppState(id, true);
-
-      // Run script
-      await runAppScript(['install', id]);
-
-      res.status(200).json({ message: 'App installed successfully' });
-    }
+    await AppsService.installApp(id, form);
   } catch (e) {
   } catch (e) {
     next(e);
     next(e);
   }
   }

+ 27 - 2
packages/system-api/src/modules/apps/apps.helpers.ts

@@ -2,7 +2,9 @@ import portUsed from 'tcp-port-used';
 import p from 'p-iteration';
 import p from 'p-iteration';
 import { AppConfig } from '../../config/types';
 import { AppConfig } from '../../config/types';
 import { fileExists, readFile, readJsonFile, runScript, writeFile } from '../fs/fs.helpers';
 import { fileExists, readFile, readJsonFile, runScript, writeFile } from '../fs/fs.helpers';
-import { internalIpV4 } from 'internal-ip';
+import InternalIp from 'internal-ip';
+
+type AppsState = { installed: string };
 
 
 export const checkAppRequirements = async (appName: string) => {
 export const checkAppRequirements = async (appName: string) => {
   let valid = true;
   let valid = true;
@@ -10,7 +12,7 @@ export const checkAppRequirements = async (appName: string) => {
 
 
   if (configFile.requirements?.ports) {
   if (configFile.requirements?.ports) {
     await p.forEachSeries(configFile.requirements.ports, async (port: number) => {
     await p.forEachSeries(configFile.requirements.ports, async (port: number) => {
-      const ip = await internalIpV4();
+      const ip = await InternalIp.v4();
       const used = await portUsed.check(port, ip);
       const used = await portUsed.check(port, ip);
 
 
       if (used) valid = false;
       if (used) valid = false;
@@ -99,3 +101,26 @@ export const ensureAppState = (appName: string, installed: boolean) => {
     }
     }
   }
   }
 };
 };
+
+export const generateEnvFile = (appName: string, form: Record<string, string>) => {
+  const configFile: AppConfig = readJsonFile(`/apps/${appName}/config.json`);
+  const baseEnvFile = readFile('/.env').toString();
+  let envFile = `${baseEnvFile}\nAPP_PORT=${configFile.port}\n`;
+
+  Object.keys(configFile.form_fields).forEach((key) => {
+    const value = form[key];
+
+    if (value) {
+      const envVar = configFile.form_fields[key].env_variable;
+      envFile += `${envVar}=${value}\n`;
+    } else if (configFile.form_fields[key].required) {
+      throw new Error(`Variable ${key} is required`);
+    }
+  });
+
+  writeFile(`/app-data/${appName}/app.env`, envFile);
+};
+
+export const getStateFile = (): AppsState => {
+  return readJsonFile('/state/apps.json');
+};

+ 106 - 0
packages/system-api/src/modules/apps/apps.service.ts

@@ -0,0 +1,106 @@
+import si from 'systeminformation';
+import { appNames } from '../../config/apps';
+import { AppConfig } from '../../config/types';
+import { createFolder, fileExists, readJsonFile } from '../fs/fs.helpers';
+import { checkAppExists, checkAppRequirements, checkEnvFile, ensureAppState, generateEnvFile, getInitalFormValues, getStateFile, runAppScript } from './apps.helpers';
+
+const startApp = async (appName: string): Promise<void> => {
+  checkAppExists(appName);
+  checkEnvFile(appName);
+
+  // Regenerate env file
+  const form = getInitalFormValues(appName);
+  generateEnvFile(appName, form);
+
+  // Run script
+  await runAppScript(['start', appName]);
+
+  ensureAppState(appName, true);
+};
+
+const installApp = async (id: string, form: Record<string, string>): Promise<void> => {
+  const appIsAvailable = appNames.includes(id);
+
+  if (!appIsAvailable) {
+    throw new Error(`App ${id} not available`);
+  }
+
+  const appExists = fileExists(`/app-data/${id}`);
+
+  if (appExists) {
+    await startApp(id);
+  } else {
+    const appIsValid = await checkAppRequirements(id);
+
+    if (!appIsValid) {
+      throw new Error(`App ${id} requirements not met`);
+    }
+
+    // Create app folder
+    createFolder(`/app-data/${id}`);
+
+    // Create env file
+    generateEnvFile(id, form);
+    ensureAppState(id, true);
+
+    // Run script
+    await runAppScript(['install', id]);
+  }
+};
+
+const listApps = async (): Promise<AppConfig[]> => {
+  const apps: AppConfig[] = appNames
+    .map((app) => {
+      try {
+        return readJsonFile(`/apps/${app}/config.json`);
+      } catch {
+        return null;
+      }
+    })
+    .filter(Boolean);
+
+  const dockerContainers = await si.dockerContainers();
+
+  const state = getStateFile();
+  const installed: string[] = state.installed.split(' ').filter(Boolean);
+
+  apps.forEach((app) => {
+    app.installed = installed.includes(app.id);
+    app.status = (dockerContainers.find((container) => container.name === `${app.id}`)?.state as 'running') || 'stopped';
+  });
+
+  return apps;
+};
+
+const getAppInfo = async (id: string): Promise<AppConfig> => {
+  const dockerContainers = await si.dockerContainers();
+  const configFile: AppConfig = readJsonFile(`/apps/${id}/config.json`);
+
+  const state = getStateFile();
+  const installed: string[] = state.installed.split(' ').filter(Boolean);
+  configFile.installed = installed.includes(id);
+  configFile.status = (dockerContainers.find((container) => container.name === `${id}`)?.state as 'running') || 'stopped';
+
+  return configFile;
+};
+
+const updateAppConfig = async (id: string, form: Record<string, string>): Promise<void> => {
+  checkAppExists(id);
+  generateEnvFile(id, form);
+};
+
+const stopApp = async (id: string): Promise<void> => {
+  checkAppExists(id);
+  // Run script
+  await runAppScript(['stop', id]);
+};
+
+const uninstallApp = async (id: string): Promise<void> => {
+  checkAppExists(id);
+  ensureAppState(id, false);
+
+  // Run script
+  await runAppScript(['uninstall', id]);
+};
+
+export default { installApp, startApp, listApps, getAppInfo, updateAppConfig, stopApp, uninstallApp };

+ 2 - 2
packages/system-api/src/modules/network/network.controller.ts

@@ -1,7 +1,7 @@
 import { Request, Response } from 'express';
 import { Request, Response } from 'express';
 import publicIp from 'public-ip';
 import publicIp from 'public-ip';
 import portScanner from 'node-port-scanner';
 import portScanner from 'node-port-scanner';
-import { internalIpV4 } from 'internal-ip';
+import internalIp from 'internal-ip';
 
 
 const isPortOpen = async (req: Request, res: Response<boolean>) => {
 const isPortOpen = async (req: Request, res: Response<boolean>) => {
   const { port } = req.params;
   const { port } = req.params;
@@ -14,7 +14,7 @@ const isPortOpen = async (req: Request, res: Response<boolean>) => {
 };
 };
 
 
 const getInternalIp = async (req: Request, res: Response<string>) => {
 const getInternalIp = async (req: Request, res: Response<string>) => {
-  const ip = await internalIpV4();
+  const ip = await internalIp.v4();
 
 
   res.status(200).send(ip);
   res.status(200).send(ip);
 };
 };

+ 3 - 3
packages/system-api/tsconfig.json

@@ -8,13 +8,13 @@
     "forceConsistentCasingInFileNames": true,
     "forceConsistentCasingInFileNames": true,
     "noEmit": true,
     "noEmit": true,
     "esModuleInterop": true,
     "esModuleInterop": true,
-    "module": "esnext",
+    "module": "commonjs",
     "moduleResolution": "node",
     "moduleResolution": "node",
     "resolveJsonModule": true,
     "resolveJsonModule": true,
-    "isolatedModules": true,
+    "isolatedModules": false,
     "jsx": "preserve",
     "jsx": "preserve",
     "incremental": true
     "incremental": true
   },
   },
-  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs"],
+  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "jest.config.cjs"],
   "exclude": ["node_modules"]
   "exclude": ["node_modules"]
 }
 }

文件差异内容过多而无法显示
+ 363 - 1485
pnpm-lock.yaml


部分文件因为文件数量过多而无法显示