Nicolas Meienberger 3 роки тому
батько
коміт
4e5bf34ece

+ 3 - 0
system-api/.eslintignore

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

+ 8 - 128
system-api/.eslintrc.cjs

@@ -1,136 +1,16 @@
 module.exports = {
-  root: true,
-  env: {
-    node: true,
-  },
+  env: { node: true },
+  extends: ['airbnb-base', 'eslint:recommended', 'plugin:import/typescript'],
   parser: '@typescript-eslint/parser',
   parserOptions: {
-    project: ['./tsconfig.json'],
+    ecmaVersion: 'latest',
+    sourceType: 'module',
   },
-  extends: ['plugin:prettier/recommended', 'airbnb-typescript', 'plugin:sonarjs/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:unicorn/recommended', 'hardcore'],
-  plugins: ['prettier', '@typescript-eslint', 'no-loops', 'sonarjs', 'deprecate', 'no-secrets', 'jest', 'react'],
-  overrides: [
-    {
-      files: ['**/*.test.ts', '**/*.test.tsx', 'jest.setup.ts', 'jest.config.js'],
-      rules: {
-        'import/unambiguous': 0,
-        'unicorn/consistent-function-scoping': 0,
-      },
-      env: {
-        jest: true,
-      },
-    },
-    {
-      files: ['**/*.d.ts'],
-      rules: {
-        'import/unambiguous': 0,
-      },
-    },
-  ],
+  plugins: ['@typescript-eslint', 'import'],
   rules: {
-    'max-statements': 0,
-    camelcase: 0,
-    'unicorn/prefer-node-protocol': 0,
-    'newline-per-chained-call': 0,
-    'new-cap': 0,
-    'security/detect-non-literal-regexp': 0,
-    'promise/avoid-new': 0,
-    'import/no-commonjs': 0,
-    'unicorn/prefer-module': 0,
-    '@typescript-eslint/no-var-requires': 0,
-    'security/detect-unsafe-regex': 0,
-    'unicorn/no-unsafe-regex': 0,
-    'no-param-reassign': ['error', { props: true, ignorePropertyModificationsFor: ['^draft'] }],
-    'unicorn/no-array-callback-reference': 0,
-    'import/no-namespace': 0,
-    'unicorn/no-null': 0,
-    'unicorn/no-useless-undefined': 0,
-    'import/max-dependencies': 0,
-    'no-use-before-define': 'off',
-    '@typescript-eslint/no-use-before-define': ['error'],
-    'unicorn/no-array-for-each': 0,
-    'unicorn/prevent-abbreviations': 0,
-    'import/order': 0,
-    'import/extensions': 0,
-    'ext/lines-between-object-properties': 0,
-    'putout/putout': 0,
-    'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
-    'deprecate/function': 1,
-    'deprecate/member-expression': 1,
-    'deprecate/import': 1,
-    'no-secrets/no-secrets': 'error',
     'arrow-body-style': 0,
-    semi: 0,
-    '@typescript-eslint/semi': 0,
-    '@typescript-eslint/indent': 0,
-    'implicit-arrow-linebreak': 0,
-    'function-paren-newline': 0,
-    'operator-linebreak': 0,
-    'import/no-unused-modules': [1, { unusedExports: true }],
-    'import/no-extraneous-dependencies': [
-      'error',
-      {
-        devDependencies: true,
-      },
-    ],
-    quotes: ['warn', 'single'],
-    'no-restricted-syntax': 2,
-    'no-await-in-loop': 2,
-    'object-curly-newline': 0,
-    'no-constant-condition': 2,
-    'no-mixed-operators': 1,
-    'no-console': ['error', { allow: ['warn', 'error'] }],
-    'no-underscore-dangle': 0,
-    'no-global-assign': 2,
-    'prefer-const': [
-      'error',
-      {
-        destructuring: 'any',
-        ignoreReadBeforeAssign: false,
-      },
-    ],
-    'import/prefer-default-export': 0,
-    'import/no-named-as-default': 0,
-    'max-lines': [
-      'error',
-      {
-        max: 200,
-        skipBlankLines: true,
-        skipComments: true,
-      },
-    ],
-    'max-len': [
-      2,
-      200,
-      {
-        ignoreComments: true,
-        ignoreTemplateLiterals: true,
-        ignoreStrings: true,
-      },
-    ],
-    curly: 0,
-    'arrow-parens': 0,
-    'no-return-assign': 2,
-    'comma-dangle': 0,
-    'no-multi-str': 0,
-    'newline-before-return': 2,
-    'newline-after-var': 2,
-    'newline-per-chained-call': 2,
-    'import/newline-after-import': 2,
-    'no-loops/no-loops': 2,
-    'jest/no-disabled-tests': 'warn',
-    'jest/no-focused-tests': 'error',
-    'jest/no-identical-title': 'error',
-    'jest/prefer-to-have-length': 'warn',
-    'jest/valid-expect': 'error',
-    'id-length': 0,
-    'no-magic-numbers': 0,
-    'unicorn/prefer-type-error': 0,
-    'unicorn/no-array-method-this-argument': 0,
-    'no-shadow': 'off',
-    '@typescript-eslint/no-shadow': ['error'],
-  },
-  globals: {
-    JSX: true,
+    'no-restricted-exports': 0,
+    'max-len': [{ code: 200 }],
+    'import/extensions': ['error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never' }],
   },
 };

+ 1 - 2
system-api/.prettierrc.cjs

@@ -1,7 +1,6 @@
 module.exports = {
   singleQuote: true,
   semi: true,
-  trailingComma: "all",
-  arrowParens: "avoid",
+  trailingComma: 'all',
   printWidth: 200,
 };

Різницю між файлами не показано, бо вона завелика
+ 637 - 452
system-api/package-lock.json


+ 11 - 1
system-api/package.json

@@ -5,6 +5,7 @@
   "main": "src/server.ts",
   "type": "module",
   "scripts": {
+    "lint": "eslint . --ext .ts",
     "test": "echo \"Error: no test specified\" && exit 1",
     "build": "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",
@@ -26,8 +27,17 @@
   "devDependencies": {
     "@types/compression": "^1.7.2",
     "@types/express": "^4.17.13",
+    "@types/validator": "^13.7.2",
     "concurrently": "^7.1.0",
     "esbuild": "^0.14.32",
-    "nodemon": "^2.0.15"
+    "eslint": "^8.13.0",
+    "eslint-config-airbnb-typescript": "^17.0.0",
+    "eslint-config-hardcore": "^24.5.0",
+    "eslint-config-prettier": "^8.5.0",
+    "eslint-plugin-import": "^2.26.0",
+    "eslint-plugin-prettier": "^4.0.0",
+    "eslint-plugin-unicorn": "^42.0.0",
+    "nodemon": "^2.0.15",
+    "prettier": "2.6.2"
   }
 }

+ 1 - 19
system-api/src/config/types.ts

@@ -1,21 +1,3 @@
-// "form_fields": {
-//     "username": {
-//       "type": "text",
-//       "label": "Username",
-//       "max": 50,
-//       "min": 3,
-//       "required": true,
-//       "env_variable": "NEXTCLOUD_USERNAME"
-//     },
-//     "password": {
-//       "type": "password",
-//       "label": "Password",
-//       "max": 50,
-//       "min": 3,
-//       "required": true,
-//       "env_variable": "NEXTCLOUD_PASSWORD"
-//     }
-
 interface FormField {
   type: string;
   label: string;
@@ -29,5 +11,5 @@ export interface AppConfig {
   name: string;
   description: string;
   version: string;
-  form_fields: Record<string, FormField[]>;
+  form_fields: Record<string, FormField>;
 }

+ 1 - 1
system-api/src/constants/constants.ts

@@ -2,4 +2,4 @@ import config from '../config';
 
 export const APP_DATA_FOLDER = 'app-data';
 export const APPS_FOLDER = 'apps';
-export const __prod__ = config.NODE_ENV === 'production';
+export const isProd = config.NODE_ENV === 'production';

+ 0 - 2
system-api/src/controllers/index.ts

@@ -1,2 +0,0 @@
-export { default as SystemController } from './system.controller';
-export { default as AppController } from './app.controller';

+ 0 - 15
system-api/src/controllers/network.controller.ts

@@ -1,15 +0,0 @@
-import { Request, Response } from "express";
-import publicIp from "public-ip";
-import portScanner from "node-port-scanner";
-
-const isPortOpen = async (req: Request, res: Response<boolean>) => {
-  const port = req.params.port;
-
-  const host = await publicIp.v4();
-
-  const isOpen = await portScanner(host, [port]);
-
-  console.log(port);
-
-  res.status(200).send(isOpen);
-};

+ 3 - 0
system-api/src/helpers/helpers.ts

@@ -0,0 +1,3 @@
+const objectKeys = <T>(obj: T): (keyof T)[] => Object.keys(obj) as (keyof T)[];
+
+export default { objectKeys };

+ 57 - 46
system-api/src/controllers/app.controller.ts → system-api/src/modules/apps/apps.controller.ts

@@ -1,45 +1,37 @@
 import { Request, Response } from 'express';
-import fs from 'fs';
 import process from 'child_process';
-import config from '../config';
-import { AppConfig } from '../config/types';
+import config from '../../config';
+import { AppConfig } from '../../config/types';
+import { createFolder, fileExists, readJsonFile, writeFile, copyFile, runScript, deleteFolder } from '../fs/fs.helpers';
 
-const appScript = `${config.ROOT_FOLDER}/scripts/app.sh`;
+type AppsState = { installed: string };
 
-const getAppFolder = (appName: string) => `${config.ROOT_FOLDER}/apps/${appName}`;
-const getDataFolder = (appName: string) => `${config.ROOT_FOLDER}/app-data/${appName}`;
-
-const getStateFile = () => {
-  // Add app to apps.json
-  const rawFile = fs.readFileSync(`${config.ROOT_FOLDER}/state/apps.json`).toString();
-  let apps = JSON.parse(rawFile);
-
-  return apps;
+const getStateFile = (): AppsState => {
+  return readJsonFile('/state/apps.json');
 };
 
 const generateEnvFile = (appName: string, form: Record<string, string>) => {
-  const appExists = fs.existsSync(getDataFolder(appName));
+  const appExists = fileExists(`/app-data/${appName}`);
 
   if (!appExists) {
     throw new Error(`App ${appName} not installed`);
   }
 
-  const rawFile = fs.readFileSync(`${getAppFolder(appName)}/config.json`).toString();
-  let configFile: AppConfig = JSON.parse(rawFile);
+  const configFile: AppConfig = readJsonFile(`/apps/${appName}/config.json`);
   let envFile = '';
 
-  Object.keys(configFile.form_fields).forEach(key => {
+  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[key].required) {
+    } else if (configFile.form_fields[key].required) {
       throw new Error(`Variable ${key} is required`);
     }
   });
 
-  fs.writeFileSync(`${getDataFolder(appName)}/.env`, envFile);
+  writeFile(`/app-data/${appName}/.env`, envFile);
 };
 
 const installApp = (req: Request, res: Response) => {
@@ -50,30 +42,25 @@ const installApp = (req: Request, res: Response) => {
       throw new Error('App name is required');
     }
 
-    const appDataFolder = `${config.ROOT_FOLDER}/app-data/${appName}`;
-    const appFolder = `${config.ROOT_FOLDER}/apps/${appName}`;
-
-    const appExists = fs.existsSync(appDataFolder);
+    const appExists = fileExists(`/app-data/${appName}`);
 
     if (appExists) {
       throw new Error(`App ${appName} already installed`);
     }
 
     // Create app folder
-    fs.mkdirSync(appFolder);
-
+    createFolder(`/app-data/${appName}`);
     // Copy default app files from app-data folder
-    fs.copyFileSync(`${appFolder}/data`, `${appDataFolder}/data`);
+    copyFile(`/apps/${appName}/data`, `/app-data/${appName}/data`);
 
+    // Create env file
     generateEnvFile(appName, form);
-
     const state = getStateFile();
     state.installed += ` ${appName}`;
-
-    fs.writeFileSync(`${config.ROOT_FOLDER}/state/apps.json`, JSON.stringify(state));
+    writeFile('/state/apps.json', JSON.stringify(state));
 
     // Run script
-    process.spawnSync(appScript, ['install', appName], {});
+    runScript('/scripts/app.sh', ['install', appName]);
 
     res.status(200).json({ message: 'App installed successfully' });
   } catch (e) {
@@ -88,22 +75,23 @@ const uninstallApp = (req: Request, res: Response) => {
     if (!appName) {
       throw new Error('App name is required');
     }
-    const appExists = fs.existsSync(getDataFolder(appName));
+
+    const appExists = fileExists(`/app-data/${appName}`);
 
     if (!appExists) {
       throw new Error(`App ${appName} not installed`);
     }
 
     // Delete app folder
-    fs.rmdirSync(getAppFolder(appName), { recursive: true });
+    deleteFolder(`/app-data/${appName}`);
 
     // Remove app from apps.json
     const state = getStateFile();
     state.installed = state.installed.replace(` ${appName}`, '');
-    fs.writeFileSync(`${config.ROOT_FOLDER}/state/apps.json`, JSON.stringify(state));
+    writeFile('/state/apps.json', JSON.stringify(state));
 
     // Run script
-    process.spawnSync(appScript, ['uninstall', appName], {});
+    runScript('/scripts/app.sh', ['uninstall', appName]);
 
     res.status(200).json({ message: 'App uninstalled successfully' });
   } catch (e) {
@@ -119,18 +107,18 @@ const stopApp = (req: Request, res: Response) => {
       throw new Error('App name is required');
     }
 
-    const appExists = fs.existsSync(getDataFolder(appName));
+    const appExists = fileExists(`/app-data/${appName}`);
 
     if (!appExists) {
       throw new Error(`App ${appName} not installed`);
     }
 
     // Run script
-    process.spawnSync(appScript, ['stop', appName], {});
+    runScript('/scripts/app.sh', ['stop', appName]);
 
     res.status(200).json({ message: 'App stopped successfully' });
   } catch (e) {
-    res.status(500).send(e);
+    res.status(500).end(e);
   }
 };
 
@@ -142,7 +130,7 @@ const updateAppConfig = (req: Request, res: Response) => {
       throw new Error('App name is required');
     }
 
-    const appExists = fs.existsSync(getDataFolder(appName));
+    const appExists = fileExists(`/app-data/${appName}`);
 
     if (!appExists) {
       throw new Error(`App ${appName} not installed`);
@@ -151,26 +139,49 @@ const updateAppConfig = (req: Request, res: Response) => {
     generateEnvFile(appName, form);
 
     // Run script
-    process.spawnSync(appScript, ['stop', appName], {});
-    process.spawnSync(appScript, ['start', appName], {});
+    runScript('/scripts/app.sh', ['stop', appName]);
+    runScript('/scripts/app.sh', ['start', appName]);
 
     res.status(200).json({ message: 'App updated successfully' });
   } catch (e) {
-    res.status(500).send(e);
+    res.status(500).end(e);
   }
 };
 
 const installedApps = (req: Request, res: Response) => {
   try {
-    const rawFile = fs.readFileSync(`${config.ROOT_FOLDER}/state/apps.json`).toString();
-    const apps = JSON.parse(rawFile);
-
+    const apps = readJsonFile('/state/apps.json');
     const appNames = apps.installed.split(' ');
 
     res.status(200).json(appNames);
   } catch (e) {
-    res.status(500).send(e);
+    res.status(500).end(e);
   }
 };
 
-export default { uninstallApp, installApp, stopApp, updateAppConfig, installedApps };
+const getAppInfo = (req: Request, res: Response<AppConfig>) => {
+  try {
+    const { appName } = req.body;
+
+    if (!appName) {
+      throw new Error('App name is required');
+    }
+
+    const configFile: AppConfig = readJsonFile(`/apps/${appName}/config.json`);
+
+    res.status(200).json(configFile);
+  } catch (e) {
+    res.status(500).end(e);
+  }
+};
+
+const AppController = {
+  uninstallApp,
+  installApp,
+  stopApp,
+  updateAppConfig,
+  installedApps,
+  getAppInfo,
+};
+
+export default AppController;

+ 1 - 1
system-api/src/routes/app.routes.ts → system-api/src/modules/apps/apps.routes.ts

@@ -1,5 +1,5 @@
 import { Router } from 'express';
-import { AppController } from '../controllers';
+import AppController from './apps.controller';
 
 const router = Router();
 

+ 21 - 0
system-api/src/modules/fs/fs.helpers.ts

@@ -0,0 +1,21 @@
+import fs from 'fs';
+import childProcess from 'child_process';
+import config from '../../config';
+
+export const getAbsolutePath = (path: string) => `${config.ROOT_FOLDER}${path}`;
+
+export const readJsonFile = (path: string): any => {
+  const rawFile = fs.readFileSync(getAbsolutePath(path)).toString();
+  return JSON.parse(rawFile);
+};
+
+export const fileExists = (path: string): boolean => fs.existsSync(getAbsolutePath(path));
+
+export const writeFile = (path: string, data: any) => fs.writeFileSync(getAbsolutePath(path), data);
+
+export const createFolder = (path: string) => fs.mkdirSync(getAbsolutePath(path));
+export const deleteFolder = (path: string) => fs.rmSync(getAbsolutePath(path), { recursive: true });
+
+export const copyFile = (source: string, destination: string) => fs.copyFileSync(getAbsolutePath(source), getAbsolutePath(destination));
+
+export const runScript = (path: string, args: string[]) => childProcess.spawnSync(getAbsolutePath(path), args, {});

+ 19 - 0
system-api/src/modules/network/network.controller.ts

@@ -0,0 +1,19 @@
+import { Request, Response } from 'express';
+import publicIp from 'public-ip';
+import portScanner from 'node-port-scanner';
+
+const isPortOpen = async (req: Request, res: Response<boolean>) => {
+  const { port } = req.params;
+
+  const host = await publicIp.v4();
+
+  const isOpen = await portScanner(host, [port]);
+
+  res.status(200).send(isOpen);
+};
+
+const NetworkController = {
+  isPortOpen,
+};
+
+export default NetworkController;

+ 1 - 1
system-api/src/controllers/system.controller.ts → system-api/src/modules/system/system.controller.ts

@@ -37,7 +37,7 @@ const getCpuInfo = async (req: Request, res: Response<CpuData>) => {
 const getDiskInfo = async (req: Request, res: Response<DiskData>) => {
   const disk = await si.fsSize();
 
-  const rootDisk = disk.find(item => item.mount === '/');
+  const rootDisk = disk.find((item) => item.mount === '/');
 
   if (!rootDisk) {
     throw new Error('Could not find root disk');

+ 10 - 0
system-api/src/modules/system/system.routes.ts

@@ -0,0 +1,10 @@
+import { Router } from 'express';
+import SystemController from './system.controller';
+
+const router = Router();
+
+router.route('/cpu').get(SystemController.getCpuInfo);
+router.route('/disk').get(SystemController.getDiskInfo);
+router.route('/memory').get(SystemController.getMemoryInfo);
+
+export default router;

+ 0 - 2
system-api/src/routes/index.ts

@@ -1,2 +0,0 @@
-export { default as systemRoutes } from './system.routes';
-export { default as appRoutes } from './app.routes';

+ 0 - 0
system-api/src/routes/network.routes.ts


+ 0 - 10
system-api/src/routes/system.routes.ts

@@ -1,10 +0,0 @@
-import { Router } from "express";
-import { SystemController } from "../controllers";
-
-const router = Router();
-
-router.route("/cpu").get(SystemController.getCpuInfo);
-router.route("/disk").get(SystemController.getDiskInfo);
-router.route("/memory").get(SystemController.getMemoryInfo);
-
-export default router;

+ 5 - 4
system-api/src/server.ts

@@ -1,19 +1,20 @@
 import express from 'express';
 import compression from 'compression';
 import helmet from 'helmet';
-import { __prod__ } from './constants/constants';
-import { appRoutes, systemRoutes } from './routes';
+import { isProd } from './constants/constants';
+import appsRoutes from './modules/apps/apps.routes';
+import systemRoutes from './modules/system/system.routes';
 
 const app = express();
 const port = 3001;
 
-if (__prod__) {
+if (isProd) {
   app.use(compression());
   app.use(helmet());
 }
 
 app.use('/system', systemRoutes);
-app.use('/app', appRoutes);
+app.use('/app', appsRoutes);
 
 app.listen(port, () => {
   console.log(`System API listening on port ${port}`);

+ 1 - 1
system-api/tsconfig.json

@@ -15,6 +15,6 @@
     "jsx": "preserve",
     "incremental": true
   },
-  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs"],
   "exclude": ["node_modules"]
 }

Деякі файли не було показано, через те що забагато файлів було змінено