Compare commits
4 Commits
main
...
nuxt3_back
Author | SHA1 | Date | |
---|---|---|---|
|
fac9447910 | ||
|
6dd06b9141 | ||
|
0255ef6c0b | ||
|
6a01a9e658 |
93
.eslintrc.js
93
.eslintrc.js
@ -1,93 +0,0 @@
|
|||||||
// 规则参见:https://cn.eslint.org/docs/rules/
|
|
||||||
module.exports = {
|
|
||||||
root: true, // 当前配置文件不能往父级查找
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
es6: true
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended' // 应用Eslint全部默认规则
|
|
||||||
],
|
|
||||||
'parserOptions': {
|
|
||||||
'ecmaVersion': 'latest',
|
|
||||||
'sourceType': 'module' // 目标类型 Node项目得添加这个
|
|
||||||
},
|
|
||||||
// 自定义规则,可以覆盖 extends 的配置【安装Eslint插件可以静态检查本地文件是否符合以下规则】
|
|
||||||
'ignorePatterns': ['*.html', 'node-os-utils'],
|
|
||||||
rules: {
|
|
||||||
// 0: 关闭规则(允许) 1/2: 警告warning/错误error(不允许)
|
|
||||||
'no-console': 'off',
|
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
|
||||||
'template-curly-spacing': ['error', 'always'], // 模板字符串空格
|
|
||||||
'default-case': 0,
|
|
||||||
'object-curly-spacing': ['error', 'always'],
|
|
||||||
'no-multi-spaces': ['error'],
|
|
||||||
indent: ['error', 2, { 'SwitchCase': 1 }], // 缩进:2
|
|
||||||
quotes: ['error', 'single'], // 引号:single单引 double双引
|
|
||||||
semi: ['error', 'never'], // 结尾分号:never禁止 always必须
|
|
||||||
'comma-dangle': ['error', 'never'], // 对象拖尾逗号
|
|
||||||
'no-redeclare': ['error', { builtinGlobals: true }], // 禁止重复对象声明
|
|
||||||
'no-multi-assign': 0,
|
|
||||||
'no-restricted-globals': 0,
|
|
||||||
'space-before-function-paren': 0, // 函数定义时括号前面空格
|
|
||||||
'one-var': 0, // 允许连续声明
|
|
||||||
// 'no-undef': 0, // 允许未定义的变量【会使env配置无效】
|
|
||||||
'linebreak-style': 0, // 检测CRLF/LF检测【默认LF】
|
|
||||||
'no-extra-boolean-cast': 0, // 允许意外的Boolean值转换
|
|
||||||
'no-constant-condition': 0, // if语句中禁止常量表达式
|
|
||||||
'no-prototype-builtins': 0, // 允许使用Object.prototypes内置对象(如:xxx.hasOwnProperty)
|
|
||||||
'no-regex-spaces': 0, // 允许正则匹配多个空格
|
|
||||||
'no-unexpected-multiline': 0, // 允许多行表达式
|
|
||||||
'no-fallthrough': 0, // 允许switch穿透
|
|
||||||
'no-delete-var': 0, // 允许 delete 删除对象属性
|
|
||||||
'no-mixed-spaces-and-tabs': 0, // 允许空格tab混用
|
|
||||||
'no-class-assign': 0, // 允许修改class类型
|
|
||||||
'no-param-reassign': 0, // 允许对函数params赋值
|
|
||||||
'max-len': 0, // 允许长行
|
|
||||||
'func-names': 0, // 允许命名函数
|
|
||||||
'import/no-unresolved': 0, // 不检测模块not fund
|
|
||||||
'import/prefer-default-export': 0, // 允许单个导出
|
|
||||||
'no-const-assign': 1, // 警告:修改const命名的变量
|
|
||||||
'no-unused-vars': 1, // 警告:已声明未使用
|
|
||||||
'no-unsafe-negation': 1, // 警告:使用 in / instanceof 关系运算符时,左边表达式请勿使用 ! 否定操作符
|
|
||||||
'use-isnan': 1, // 警告:使用 isNaN() 检查 NaN
|
|
||||||
'no-var': 2, // 禁止使用var声明
|
|
||||||
'no-empty-pattern': 2, // 空解构赋值
|
|
||||||
'eqeqeq': 2, // 必须使用 全等=== 或 非全等 !==
|
|
||||||
'no-cond-assign': 2, // if语句中禁止赋值
|
|
||||||
'no-dupe-args': 2, // 禁止function重复参数
|
|
||||||
'no-dupe-keys': 2, // 禁止object重复key
|
|
||||||
'no-duplicate-case': 2,
|
|
||||||
'no-empty': 2, // 禁止空语句块
|
|
||||||
'no-func-assign': 2, // 禁止重复声明函数
|
|
||||||
'no-inner-declarations': 2, // 禁止在嵌套的语句块中出现变量或 function 声明
|
|
||||||
'no-sparse-arrays': 2, // 禁止稀缺数组
|
|
||||||
'no-unreachable': 2, // 禁止非条件return、throw、continue 和 break 语句后出现代码
|
|
||||||
'no-unsafe-finally': 2, // 禁止finally出现控制流语句,如:return、throw等,因为这会导致try...catch捕获不到
|
|
||||||
'valid-typeof': 2, // 强制 typeof 表达式与有效的字符串进行比较
|
|
||||||
// auto format options
|
|
||||||
'prefer-const': 0, // 禁用声明自动化
|
|
||||||
'no-extra-parens': 0, // 允许函数周围出现不明括号
|
|
||||||
'no-extra-semi': 2, // 禁止不必要的分号
|
|
||||||
// curly: ['error', 'multi'], // if、else、for、while 语句单行代码时不使用大括号
|
|
||||||
'dot-notation': 0, // 允许使用点号或方括号来访问对象属性
|
|
||||||
'dot-location': ['error', 'property'], // 点操作符位置,要求跟随下一行
|
|
||||||
'no-else-return': 2, // 禁止if中有return后又else
|
|
||||||
'no-implicit-coercion': [2, { allow: ['!!', '~', '+'] }], // 禁止隐式转换,allow字段内符号允许
|
|
||||||
'no-trailing-spaces': 1, //一行结束后面不要有空格
|
|
||||||
'no-multiple-empty-lines': [1, { 'max': 1 }], // 空行最多不能超过1行
|
|
||||||
'no-useless-return': 2,
|
|
||||||
'wrap-iife': 0, // 允许自调用函数
|
|
||||||
'yoda': 0, // 允许yoda语句
|
|
||||||
'strict': 0, // 允许strict
|
|
||||||
'no-undef-init': 0, // 允许将变量初始化为undefined
|
|
||||||
'prefer-promise-reject-errors': 0, // 允许使用非 Error 对象作为 Promise 拒绝的原因
|
|
||||||
'consistent-return': 0, // 允许函数不使用return
|
|
||||||
'no-new': 0, // 允许单独new
|
|
||||||
'no-restricted-syntax': 0, // 允许特定的语法
|
|
||||||
'no-plusplus': 0,
|
|
||||||
'import/extensions': 0, // 忽略扩展名
|
|
||||||
'global-require': 0,
|
|
||||||
'no-return-assign': 0
|
|
||||||
}
|
|
||||||
}
|
|
40
.gitattributes
vendored
Normal file
40
.gitattributes
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# These settings are for any web project
|
||||||
|
|
||||||
|
# Handle line endings automatically for files detected as text
|
||||||
|
# and leave all files detected as binary untouched.
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Force the following filetypes to have unix eols, so Windows does not break them
|
||||||
|
*.* text eol=crlf
|
||||||
|
|
||||||
|
# Windows forced line-endings
|
||||||
|
/.idea/* text eol=crlf
|
||||||
|
|
||||||
|
#
|
||||||
|
## These files are binary and should be left untouched
|
||||||
|
#
|
||||||
|
|
||||||
|
# (binary is a macro for -text -diff)
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.mov binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.flv binary
|
||||||
|
*.fla binary
|
||||||
|
*.swf binary
|
||||||
|
*.gz binary
|
||||||
|
*.zip binary
|
||||||
|
*.7z binary
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
*.woff binary
|
||||||
|
*.pyc binary
|
||||||
|
*.pdf binary
|
||||||
|
*.ez binary
|
||||||
|
*.bz2 binary
|
||||||
|
*.swp binary
|
||||||
|
|
29
.gitignore
vendored
29
.gitignore
vendored
@ -1,7 +1,26 @@
|
|||||||
node_modules
|
|
||||||
!.gitkeep
|
!.gitkeep
|
||||||
|
|
||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
dist
|
dist
|
||||||
easynode-server.zip
|
|
||||||
server/app/static/upload/*
|
# Node dependencies
|
||||||
server/app/socket/temp/*
|
node_modules
|
||||||
server/app/logs/*
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
120
CHANGELOG.md
120
CHANGELOG.md
@ -1,58 +1,62 @@
|
|||||||
## [1.2.1](https://github.com/chaos-zhu/easynode/releases) (2022-12-12)
|
## [2.0.0-beta](https://github.com/chaos-zhu/easynode/releases) (2024-07-13)
|
||||||
|
|
||||||
### Features
|
底层代码重构
|
||||||
|
|
||||||
* 新增支持终端长命令输入模式 ✔
|
## [1.2.1](https://github.com/chaos-zhu/easynode/releases) (2022-12-12)
|
||||||
* 新增前端静态文件缓存 ✔
|
|
||||||
* 【重要】v1.2.1开始移除创建https服务 ✔
|
### Features
|
||||||
|
|
||||||
### Bug Fixes
|
* 新增支持终端长命令输入模式 ✔
|
||||||
|
* 新增前端静态文件缓存 ✔
|
||||||
* v1.2的若干bug...
|
* 【重要】v1.2.1开始移除创建https服务 ✔
|
||||||
|
|
||||||
## [1.2.0](https://github.com/chaos-zhu/easynode/releases) (2022-09-12)
|
### Bug Fixes
|
||||||
|
|
||||||
### Features
|
* v1.2的若干bug...
|
||||||
|
|
||||||
* 新增邮件通知: 包括登录面板、密码修改、服务器到期、服务器离线等 ✔
|
## [1.2.0](https://github.com/chaos-zhu/easynode/releases) (2022-09-12)
|
||||||
* 支持服务器分组(为新版UI作准备的) ✔
|
|
||||||
* 面板功能调整,支持http延迟显示、支持服务器控制台直达与到期时间字段 ✔
|
### Features
|
||||||
* 优化终端输入、支持状态面板收缩 ✔
|
|
||||||
* **全新SFTP功能支持,上传下载进度条展示** ✔
|
* 新增邮件通知: 包括登录面板、密码修改、服务器到期、服务器离线等 ✔
|
||||||
* **支持在线文件编辑与保存** ✔
|
* 支持服务器分组(为新版UI作准备的) ✔
|
||||||
|
* 面板功能调整,支持http延迟显示、支持服务器控制台直达与到期时间字段 ✔
|
||||||
### Bug Fixes
|
* 优化终端输入、支持状态面板收缩 ✔
|
||||||
|
* **全新SFTP功能支持,上传下载进度条展示** ✔
|
||||||
* v1.1的若干bug...
|
* **支持在线文件编辑与保存** ✔
|
||||||
|
|
||||||
---
|
### Bug Fixes
|
||||||
|
|
||||||
## [1.1.0](https://github.com/chaos-zhu/easynode/releases) (2022-06-27)
|
* v1.1的若干bug...
|
||||||
|
|
||||||
### Features
|
---
|
||||||
|
|
||||||
* ssh密钥/密码(采用对称AES+RSA非对称双加密传输与存储)、jwtToken(服务端对称加密传输) ✔
|
## [1.1.0](https://github.com/chaos-zhu/easynode/releases) (2022-06-27)
|
||||||
* 加密储存登录密码 ✔
|
|
||||||
* 登录IP检测机制&历史登录查询✔
|
### Features
|
||||||
* 终端多tab支持✔
|
|
||||||
* 终端页左侧栏信息✔
|
* ssh密钥/密码(采用对称AES+RSA非对称双加密传输与存储)、jwtToken(服务端对称加密传输) ✔
|
||||||
* 客户端支持ARM实例✔
|
* 加密储存登录密码 ✔
|
||||||
|
* 登录IP检测机制&历史登录查询✔
|
||||||
### Bug Fixes
|
* 终端多tab支持✔
|
||||||
|
* 终端页左侧栏信息✔
|
||||||
* 修复终端展示异常的Bug✔
|
* 客户端支持ARM实例✔
|
||||||
* 修复保存私钥时第二次选择无效的bug✔
|
|
||||||
* 修复面板客户端探针断开更新不及时的bug✔
|
### Bug Fixes
|
||||||
* 修复移除主机未移除ssh密钥信息的bug✔
|
|
||||||
* 修复服务器排序bug✔
|
* 修复终端展示异常的Bug✔
|
||||||
* 解决https下无法socket连接到客户端bug✔
|
* 修复保存私钥时第二次选择无效的bug✔
|
||||||
|
* 修复面板客户端探针断开更新不及时的bug✔
|
||||||
---
|
* 修复移除主机未移除ssh密钥信息的bug✔
|
||||||
|
* 修复服务器排序bug✔
|
||||||
## [1.0.0](https://github.com/chaos-zhu/easynode/releases) (2022-06-08)
|
* 解决https下无法socket连接到客户端bug✔
|
||||||
|
|
||||||
|
---
|
||||||
### Features
|
|
||||||
|
## [1.0.0](https://github.com/chaos-zhu/easynode/releases) (2022-06-08)
|
||||||
* 通过`websocker实时更新`服务器基本信息: 系统、公网IP、CPU、内存、硬盘、网卡等
|
|
||||||
* 解决`SSH跨端同步`问题——Web SSH
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 通过`websocker实时更新`服务器基本信息: 系统、公网IP、CPU、内存、硬盘、网卡等
|
||||||
|
* 解决`SSH跨端同步`问题——Web SSH
|
||||||
|
40
Q&A.md
40
Q&A.md
@ -1,20 +1,20 @@
|
|||||||
# Q&A
|
# Q&A
|
||||||
|
|
||||||
## CentOS7/8启动服务失败
|
## CentOS7/8启动服务失败
|
||||||
|
|
||||||
> 先关闭SELinux
|
> 先关闭SELinux
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
vi /etc/selinux/config
|
vi /etc/selinux/config
|
||||||
SELINUX=enforcing
|
SELINUX=enforcing
|
||||||
// 修改为禁用
|
// 修改为禁用
|
||||||
SELINUX=disabled
|
SELINUX=disabled
|
||||||
```
|
```
|
||||||
|
|
||||||
> 重启:`reboot`,再使用一键脚本安装
|
> 重启:`reboot`,再使用一键脚本安装
|
||||||
|
|
||||||
> 查看SELinux状态:sestatus
|
> 查看SELinux状态:sestatus
|
||||||
|
|
||||||
## 客户端服务启动成功,无法连接?
|
## 客户端服务启动成功,无法连接?
|
||||||
|
|
||||||
> 端口未开放:`iptables -I INPUT -s 0.0.0.0/0 -p tcp --dport 22022 -j ACCEPT` 或者 `rm -rf /etc/iptables && reboot`
|
> 端口未开放:`iptables -I INPUT -s 0.0.0.0/0 -p tcp --dport 22022 -j ACCEPT` 或者 `rm -rf /etc/iptables && reboot`
|
||||||
|
179
README.md
179
README.md
@ -1,173 +1,6 @@
|
|||||||
# EasyNode v1.2
|
# 重构中, 老版本切换v1.2分支部署
|
||||||
|
|
||||||
> 一个简易的个人Linux服务器管理面板(基于Node.js).
|
## EasyNode
|
||||||
|
|
||||||
> 前端仓库地址:https://github.com/chaos-zhu/easynode_web
|
> 一个简易的个人Linux服务器管理面板.
|
||||||
|
|
||||||
<!-- - [EasyNode](#easynode) -->
|
|
||||||
- [功能简介](#功能简介)
|
|
||||||
- [安装指南](#安装指南)
|
|
||||||
- [服务端安装](#服务端安装)
|
|
||||||
- [Docker镜像](#docker镜像)
|
|
||||||
- [一键脚本](#一键脚本)
|
|
||||||
- [手动部署](#手动部署)
|
|
||||||
- [客户端安装](#客户端安装)
|
|
||||||
- [X86架构](#x86架构)
|
|
||||||
- [ARM架构](#arm架构)
|
|
||||||
- [升级指南](#升级指南)
|
|
||||||
- [服务端](#服务端)
|
|
||||||
- [客户端](#客户端)
|
|
||||||
- [版本日志](#版本日志)
|
|
||||||
- [安全与说明](#安全与说明)
|
|
||||||
- [Q&A](#qa)
|
|
||||||
- [感谢Star](#感谢star)
|
|
||||||
- [License](#license)
|
|
||||||
|
|
||||||
## 功能简介
|
|
||||||
|
|
||||||
> 多服务器管理; 通过`websocket实时更新`服务器基本信息: **系统、公网IP、CPU、内存、硬盘、网卡**等
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
> 基于浏览器解决`SSH&SFTP跨端`烦恼——**Web SSH**&**Web SFTP**
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
> 在线编辑文件
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 安装指南
|
|
||||||
|
|
||||||
### 服务端安装
|
|
||||||
|
|
||||||
- 依赖Node.js环境
|
|
||||||
|
|
||||||
- 占用端口:8082(http端口)、22022(客户端端口)
|
|
||||||
|
|
||||||
- 建议使用**境外服务器**(最好延迟低)安装服务端,客户端信息监控与webssh功能都将以`该服务器作为跳板机`
|
|
||||||
|
|
||||||
#### Docker镜像
|
|
||||||
|
|
||||||
> 注意:网速统计功能可能受限,docker网络将使用host模式(与宿主机共享端口,占用: 8082、22022)
|
|
||||||
|
|
||||||
- 如果你是第一次运行,先创建一个volume用于保存数据
|
|
||||||
```shell
|
|
||||||
docker volume create --name easynode-server
|
|
||||||
```
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker run -d --net=host --name=easynode-server -v easynode-server:/easynode-server/app/storage/ chaoszhu/easynode:v1.2.1
|
|
||||||
```
|
|
||||||
|
|
||||||
- 如果你想清除容器与数据
|
|
||||||
```shell
|
|
||||||
docker rm -f easynode-server && docker volume remove easynode-server
|
|
||||||
```
|
|
||||||
|
|
||||||
访问:http://yourip:8082
|
|
||||||
|
|
||||||
#### 一键脚本
|
|
||||||
|
|
||||||
- **依赖Linux基础命令工具:curl wget git zip tar;如未安装请先安装:**
|
|
||||||
|
|
||||||
> ubuntu/debian: `apt install curl wget git zip tar -y`
|
|
||||||
>
|
|
||||||
> centos: `yum install curl wget git zip tar -y`
|
|
||||||
|
|
||||||
- 运行环境:[Node.js](https://nodejs.org/en/download/) **v14+**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
wget -qO- --no-check-certificate https://ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/v1.2/easynode-server-install.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
访问:http://yourip:8082
|
|
||||||
|
|
||||||
- 查看日志:`pm2 log easynode-server`
|
|
||||||
- 启动服务:`pm2 start easynode-server`
|
|
||||||
- 停止服务:`pm2 stop easynode-server`
|
|
||||||
- 删除服务:`pm2 delete easynode-server`
|
|
||||||
|
|
||||||
#### 手动部署
|
|
||||||
|
|
||||||
1. 安装Node.js
|
|
||||||
2. 安装pm2、安装yarn
|
|
||||||
3. 拉取代码:git clone https://github.com/chaos-zhu/easynode.git
|
|
||||||
4. 安装依赖:yarn
|
|
||||||
5. 启动服务:pm2 start server/app/main.js --name easynode-server
|
|
||||||
6. 访问:http://yourip:8082
|
|
||||||
|
|
||||||
- 默认登录密码:admin(首次部署完成后请及时修改).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 客户端安装
|
|
||||||
|
|
||||||
- 占用端口:22022
|
|
||||||
|
|
||||||
#### X86架构
|
|
||||||
|
|
||||||
```shell
|
|
||||||
wget -qO- --no-check-certificate https://ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/v1.2/easynode-client-install-x86.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ARM架构
|
|
||||||
|
|
||||||
```shell
|
|
||||||
wget -qO- --no-check-certificate https://ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/v1.1/easynode-client-install-arm.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
> 卸载
|
|
||||||
|
|
||||||
```shell
|
|
||||||
wget -qO- --no-check-certificate https://ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/v1.2/easynode-client-uninstall.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
> 查看客户端状态:`systemctl status easynode-client`
|
|
||||||
>
|
|
||||||
> 查看客户端日志: `journalctl --follow -u easynode-client`
|
|
||||||
>
|
|
||||||
> 查看详细日志:journalctl -xe
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 升级指南
|
|
||||||
|
|
||||||
- **v1.1 to v1.2**
|
|
||||||
|
|
||||||
### 服务端
|
|
||||||
|
|
||||||
> v1.1对所有的敏感信息全部加密,所有的v1.0为加密的信息全部失效. 主要影响已存储的ssh密钥.
|
|
||||||
>
|
|
||||||
> **还原客户端列表:** 先备份`app\config\storage\host-list.json`, 使用一键脚本或者手动部署的同志安装好使用备份文件覆盖`\app\storage`下的同名文件即可。
|
|
||||||
>
|
|
||||||
> 由于加密文件调整,使用docker镜像的v1.1一键脚本自己从镜像里把备份抠出来再重新构建镜像.
|
|
||||||
|
|
||||||
### 客户端
|
|
||||||
|
|
||||||
> v1.2未对客户端包进行改动,客户端无需重复安装. 不会备份的在面板重新添加客户端机器即可.
|
|
||||||
|
|
||||||
### 版本日志
|
|
||||||
|
|
||||||
- [CHANGELOG](./CHANGELOG.md)
|
|
||||||
|
|
||||||
## 安全与说明
|
|
||||||
|
|
||||||
> 本人非专业后端,此服务全凭兴趣开发. 由于知识受限,并不能保证没有漏洞的存在,重要生产服务器最好不要使用此服务!!!
|
|
||||||
|
|
||||||
> 所有服务器信息相关接口已做`jwt鉴权`, 安全信息均使用加密传输与储存!
|
|
||||||
|
|
||||||
> webssh功能需要的密钥信息全部保存在服务端服务器的`app\storage\ssh-record.json`中. 在保存ssh密钥信息到服务器储存与传输过程皆已加密,`不过最好还是套https使用`
|
|
||||||
|
|
||||||
## Q&A
|
|
||||||
|
|
||||||
- [Q&A](./Q%26A.md)
|
|
||||||
|
|
||||||
## 感谢Star
|
|
||||||
|
|
||||||
- 你的Star是我更新的动力,感谢~
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](LICENSE). Copyright (c).
|
|
||||||
|
9
app.vue
Normal file
9
app.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtPage />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
@ -1,67 +1,67 @@
|
|||||||
const { Server } = require('socket.io')
|
const { Server } = require('socket.io')
|
||||||
const schedule = require('node-schedule')
|
const schedule = require('node-schedule')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
let getOsData = require('../utils/os-data')
|
let getOsData = require('../utils/os-data')
|
||||||
|
|
||||||
let serverSockets = {}, ipInfo = {}, osData = {}
|
let serverSockets = {}, ipInfo = {}, osData = {}
|
||||||
|
|
||||||
async function getIpInfo() {
|
async function getIpInfo() {
|
||||||
try {
|
try {
|
||||||
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
|
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
|
||||||
console.log('getIpInfo Success: ', new Date())
|
console.log('getIpInfo Success: ', new Date())
|
||||||
ipInfo = data
|
ipInfo = data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('getIpInfo Error: ', new Date(), error)
|
console.log('getIpInfo Error: ', new Date(), error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ipSchedule() {
|
function ipSchedule() {
|
||||||
let rule1 = new schedule.RecurrenceRule()
|
let rule1 = new schedule.RecurrenceRule()
|
||||||
rule1.second = [0, 10, 20, 30, 40, 50]
|
rule1.second = [0, 10, 20, 30, 40, 50]
|
||||||
schedule.scheduleJob(rule1, () => {
|
schedule.scheduleJob(rule1, () => {
|
||||||
let { query, country, city } = ipInfo || {}
|
let { query, country, city } = ipInfo || {}
|
||||||
if(query && country && city) return
|
if(query && country && city) return
|
||||||
console.log('Task: start getIpInfo', new Date())
|
console.log('Task: start getIpInfo', new Date())
|
||||||
getIpInfo()
|
getIpInfo()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
|
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
|
||||||
let rule2 = new schedule.RecurrenceRule()
|
let rule2 = new schedule.RecurrenceRule()
|
||||||
rule2.hour = 2
|
rule2.hour = 2
|
||||||
rule2.minute = 0
|
rule2.minute = 0
|
||||||
rule2.second = 0
|
rule2.second = 0
|
||||||
schedule.scheduleJob(rule2, () => {
|
schedule.scheduleJob(rule2, () => {
|
||||||
console.log('Task: refresh ip info', new Date())
|
console.log('Task: refresh ip info', new Date())
|
||||||
getIpInfo()
|
getIpInfo()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ipSchedule()
|
ipSchedule()
|
||||||
|
|
||||||
module.exports = (httpServer) => {
|
module.exports = (httpServer) => {
|
||||||
const serverIo = new Server(httpServer, {
|
const serverIo = new Server(httpServer, {
|
||||||
path: '/client/os-info',
|
path: '/client/os-info',
|
||||||
cors: {
|
cors: {
|
||||||
origin: '*'
|
origin: '*'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
serverIo.on('connection', (socket) => {
|
serverIo.on('connection', (socket) => {
|
||||||
serverSockets[socket.id] = setInterval(async () => {
|
serverSockets[socket.id] = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
osData = await getOsData()
|
osData = await getOsData()
|
||||||
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
|
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('客户端错误:', error)
|
console.error('客户端错误:', error)
|
||||||
socket && socket.emit('client_error', { error })
|
socket && socket.emit('client_error', { error })
|
||||||
}
|
}
|
||||||
}, 1500)
|
}, 1500)
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
|
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
|
||||||
delete serverSockets[socket.id]
|
delete serverSockets[socket.id]
|
||||||
socket.close && socket.close()
|
socket.close && socket.close()
|
||||||
socket = null
|
socket = null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,35 @@
|
|||||||
{
|
{
|
||||||
"name": "easynode-client",
|
"name": "easynode-client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "easynode-client",
|
"description": "easynode-client",
|
||||||
"bin": "./bin/www",
|
"bin": "./bin/www",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
"outputPath": "dist"
|
"outputPath": "dist"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"client": "nodemon ./app/main.js",
|
"client": "nodemon ./app/main.js",
|
||||||
"pkgwin": "pkg . -t node16-win-x64",
|
"pkgwin": "pkg . -t node16-win-x64",
|
||||||
"pkglinux:x86": "pkg . -t node16-linux-x64",
|
"pkglinux:x86": "pkg . -t node16-linux-x64",
|
||||||
"pkglinux:arm": "pkg . -t node16-linux-arm64"
|
"pkglinux:arm": "pkg . -t node16-linux-arm64"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"*.json"
|
"*.json"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.4",
|
"axios": "^0.21.4",
|
||||||
"koa": "^2.13.1",
|
"koa": "^2.13.1",
|
||||||
"node-os-utils": "^1.3.6",
|
"node-os-utils": "^1.3.6",
|
||||||
"node-schedule": "^2.1.0",
|
"node-schedule": "^2.1.0",
|
||||||
"socket.io": "^4.4.1"
|
"socket.io": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"nodemon": "^2.0.15",
|
"nodemon": "^2.0.15",
|
||||||
"pkg": "5.6"
|
"pkg": "5.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
components.d.ts
vendored
Normal file
15
components.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
|
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||||
|
}
|
||||||
|
}
|
11
composables/use-sync-props.js
Normal file
11
composables/use-sync-props.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
export const useSyncProps = (props, key, emit) => {
|
||||||
|
return computed({
|
||||||
|
get() {
|
||||||
|
return props[key]
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit(`update:${key}`, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -1,83 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if [ "$(id -u)" != "0" ] ; then
|
|
||||||
echo "***********************请切换到root再尝试执行***********************"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SERVER_NAME=easynode-client
|
|
||||||
FILE_PATH=/root/local/easynode-client
|
|
||||||
SERVICE_PATH=/etc/systemd/system
|
|
||||||
SERVER_VERSION=v1.0
|
|
||||||
|
|
||||||
echo "***********************开始安装 easynode-client_${SERVER_VERSION}***********************"
|
|
||||||
|
|
||||||
systemctl status ${SERVER_NAME} > /dev/null 2>&1
|
|
||||||
if [ $? != 4 ]
|
|
||||||
then
|
|
||||||
echo "***********************停用旧服务***********************"
|
|
||||||
systemctl stop ${SERVER_NAME}
|
|
||||||
systemctl disable ${SERVER_NAME}
|
|
||||||
systemctl daemon-reload
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "${SERVICE_PATH}/${SERVER_NAME}.service" ]
|
|
||||||
then
|
|
||||||
echo "***********************移除旧服务***********************"
|
|
||||||
chmod 777 ${SERVICE_PATH}/${SERVER_NAME}.service
|
|
||||||
rm -Rf ${SERVICE_PATH}/${SERVER_NAME}.service
|
|
||||||
systemctl daemon-reload
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -d ${FILE_PATH} ]
|
|
||||||
then
|
|
||||||
echo "***********************移除旧文件***********************"
|
|
||||||
chmod 777 ${FILE_PATH}
|
|
||||||
rm -Rf ${FILE_PATH}
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 开始安装
|
|
||||||
|
|
||||||
echo "***********************创建文件PATH***********************"
|
|
||||||
mkdir -p ${FILE_PATH}
|
|
||||||
|
|
||||||
echo "***********************下载开始***********************"
|
|
||||||
DOWNLOAD_FILE_URL="https://ghproxy.com/https://github.com/chaos-zhu/easynode/releases/download/v1.1/easynode-client-arm"
|
|
||||||
DOWNLOAD_SERVICE_URL="https://ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/v1.1/client/easynode-client.service"
|
|
||||||
|
|
||||||
# -O 指定路径和文件名(这里是二进制文件, 不需要扩展名)
|
|
||||||
wget -O ${FILE_PATH}/${SERVER_NAME} --no-check-certificate --no-cache ${DOWNLOAD_FILE_URL}
|
|
||||||
if [ $? != 0 ]
|
|
||||||
then
|
|
||||||
echo "***********************下载${SERVER_NAME}失败***********************"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
wget -O ${FILE_PATH}/${SERVER_NAME}.service --no-check-certificate --no-cache ${DOWNLOAD_SERVICE_URL}
|
|
||||||
|
|
||||||
if [ $? != 0 ]
|
|
||||||
then
|
|
||||||
echo "***********************下载${SERVER_NAME}.service失败***********************"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "***********************下载成功***********************"
|
|
||||||
|
|
||||||
# echo "***********************设置权限***********************"
|
|
||||||
chmod +x ${FILE_PATH}/${SERVER_NAME}
|
|
||||||
chmod 777 ${FILE_PATH}/${SERVER_NAME}.service
|
|
||||||
|
|
||||||
# echo "***********************移动service&reload***********************"
|
|
||||||
mv ${FILE_PATH}/${SERVER_NAME}.service ${SERVICE_PATH}
|
|
||||||
|
|
||||||
# echo "***********************daemon-reload***********************"
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
echo "***********************启动服务***********************"
|
|
||||||
systemctl start ${SERVER_NAME}
|
|
||||||
|
|
||||||
|
|
||||||
# echo "***********************设置开机启动***********************"
|
|
||||||
systemctl enable ${SERVER_NAME}
|
|
||||||
|
|
||||||
echo "***********************安装成功***********************"
|
|
@ -1,90 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if [ "$(id -u)" != "0" ] ; then
|
|
||||||
echo "***********************请切换到root再尝试执行***********************"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SERVER_NAME=easynode-client
|
|
||||||
FILE_PATH=/root/local/easynode-client
|
|
||||||
SERVICE_PATH=/etc/systemd/system
|
|
||||||
SERVER_VERSION=v1.0
|
|
||||||
|
|
||||||
echo "***********************开始安装 easynode-client_${SERVER_VERSION}***********************"
|
|
||||||
|
|
||||||
systemctl status ${SERVER_NAME} > /dev/null 2>&1
|
|
||||||
if [ $? != 4 ]
|
|
||||||
then
|
|
||||||
echo "***********************停用旧服务***********************"
|
|
||||||
systemctl stop ${SERVER_NAME}
|
|
||||||
systemctl disable ${SERVER_NAME}
|
|
||||||
systemctl daemon-reload
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "${SERVICE_PATH}/${SERVER_NAME}.service" ]
|
|
||||||
then
|
|
||||||
echo "***********************移除旧服务***********************"
|
|
||||||
chmod 777 ${SERVICE_PATH}/${SERVER_NAME}.service
|
|
||||||
rm -Rf ${SERVICE_PATH}/${SERVER_NAME}.service
|
|
||||||
systemctl daemon-reload
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -d ${FILE_PATH} ]
|
|
||||||
then
|
|
||||||
echo "***********************移除旧文件***********************"
|
|
||||||
chmod 777 ${FILE_PATH}
|
|
||||||
rm -Rf ${FILE_PATH}
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 开始安装
|
|
||||||
|
|
||||||
echo "***********************创建文件PATH***********************"
|
|
||||||
mkdir -p ${FILE_PATH}
|
|
||||||
|
|
||||||
echo "***********************下载开始***********************"
|
|
||||||
DOWNLOAD_FILE_URL="https://ghproxy.com/https://github.com/chaos-zhu/easynode/releases/download/v1.1/easynode-client-x86"
|
|
||||||
DOWNLOAD_SERVICE_URL="https://ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/v1.1/client/easynode-client.service"
|
|
||||||
|
|
||||||
# -O 指定路径和文件名(这里是二进制文件, 不需要扩展名)
|
|
||||||
wget -O ${FILE_PATH}/${SERVER_NAME} --no-check-certificate --no-cache ${DOWNLOAD_FILE_URL}
|
|
||||||
if [ $? != 0 ]
|
|
||||||
then
|
|
||||||
echo "***********************下载${SERVER_NAME}失败***********************"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
wget -O ${FILE_PATH}/${SERVER_NAME}.service --no-check-certificate --no-cache ${DOWNLOAD_SERVICE_URL}
|
|
||||||
|
|
||||||
if [ $? != 0 ]
|
|
||||||
then
|
|
||||||
echo "***********************下载${SERVER_NAME}.service失败***********************"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "***********************下载成功***********************"
|
|
||||||
|
|
||||||
# echo "***********************设置权限***********************"
|
|
||||||
chmod +x ${FILE_PATH}/${SERVER_NAME}
|
|
||||||
chmod 777 ${FILE_PATH}/${SERVER_NAME}.service
|
|
||||||
|
|
||||||
# echo "***********************移动service&reload***********************"
|
|
||||||
mv ${FILE_PATH}/${SERVER_NAME}.service ${SERVICE_PATH}
|
|
||||||
|
|
||||||
# echo "***********************daemon-reload***********************"
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
echo "***********************准备启动服务***********************"
|
|
||||||
systemctl start ${SERVER_NAME}
|
|
||||||
|
|
||||||
if [ $? != 0 ]
|
|
||||||
then
|
|
||||||
echo "***********************${SERVER_NAME}.service启动失败***********************"
|
|
||||||
echo "***********************可能是服务器开启了SELinux, 参见Q&A***********************"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "***********************服务启动成功***********************"
|
|
||||||
|
|
||||||
# echo "***********************设置开机启动***********************"
|
|
||||||
systemctl enable ${SERVER_NAME}
|
|
||||||
|
|
||||||
echo "***********************安装成功***********************"
|
|
@ -1,34 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if [ "$(id -u)" != "0" ] ; then
|
|
||||||
echo "***********************请切换到root再尝试执行***********************"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SERVER_NAME=easynode-client
|
|
||||||
FILE_PATH=/root/local/easynode-client
|
|
||||||
SERVICE_PATH=/etc/systemd/system
|
|
||||||
|
|
||||||
echo "*********************** 开始卸载 ***************************"
|
|
||||||
|
|
||||||
service ${SERVER_NAME} stop
|
|
||||||
systemctl disable ${SERVER_NAME}
|
|
||||||
|
|
||||||
echo "*********************** 移除文件 ***************************"
|
|
||||||
|
|
||||||
if [ -d "${FILE_PATH}" ]
|
|
||||||
then
|
|
||||||
rm -Rf ${FILE_PATH}
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "*********************** 移除服务 ***************************"
|
|
||||||
if [ -f "${SERVICE_PATH}/${SERVER_NAME}.service" ]
|
|
||||||
then
|
|
||||||
rm -Rf ${SERVICE_PATH}/${SERVER_NAME}.service
|
|
||||||
systemctl daemon-reload
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "*********************** 卸载完成 ***************************"
|
|
||||||
|
|
||||||
# echo "***********************删除脚本***********************"
|
|
||||||
# rm "$0"
|
|
@ -1,74 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if [ "$(id -u)" != "0" ] ; then
|
|
||||||
echo "***********************请切换到root再尝试执行***********************"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "***********************检测node环境***********************"
|
|
||||||
node -v
|
|
||||||
if [ $? != 0 ]
|
|
||||||
then
|
|
||||||
echo "未安装node运行环境"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "已安装"
|
|
||||||
|
|
||||||
|
|
||||||
echo "***********************检测pm2守护进程***********************"
|
|
||||||
pm2 list
|
|
||||||
if [ $? != 0 ]
|
|
||||||
then
|
|
||||||
echo "未安装pm2,正在安装..."
|
|
||||||
npm i -g pm2
|
|
||||||
fi
|
|
||||||
echo "已安装"
|
|
||||||
|
|
||||||
echo "***********************开始下载EasyNode***********************"
|
|
||||||
|
|
||||||
DOWNLOAD_FILE_URL="https://ghproxy.com/https://github.com/chaos-zhu/easynode/releases/download/v1.2.1/easynode-server.zip"
|
|
||||||
SERVER_NAME=easynode-server
|
|
||||||
SERVER_ZIP=easynode-server.zip
|
|
||||||
FILE_PATH=/root
|
|
||||||
wget -O ${FILE_PATH}/${SERVER_ZIP} --no-check-certificate --no-cache ${DOWNLOAD_FILE_URL}
|
|
||||||
|
|
||||||
if [ $? != 0 ]
|
|
||||||
then
|
|
||||||
echo "下载EasyNode.zip失败,请检查网络环境或稍后再试"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "下载成功"
|
|
||||||
|
|
||||||
echo '***********************开始解压***********************'
|
|
||||||
|
|
||||||
unzip -o -d ${FILE_PATH}/${SERVER_NAME} ${SERVER_ZIP}
|
|
||||||
if [ $? != 0 ]
|
|
||||||
then
|
|
||||||
echo "解压失败, 请确保已安装zip、tar基础工具"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "解压成功"
|
|
||||||
|
|
||||||
cd ${FILE_PATH}/${SERVER_NAME} || exit
|
|
||||||
|
|
||||||
echo '***********************开始安装依赖***********************'
|
|
||||||
yarn -v
|
|
||||||
if [ $? != 0 ]
|
|
||||||
then
|
|
||||||
echo "未安装yarn管理工具,正在安装..."
|
|
||||||
npm i -g yarn
|
|
||||||
fi
|
|
||||||
yarn
|
|
||||||
|
|
||||||
if [ $? != 0 ]
|
|
||||||
then
|
|
||||||
echo "yarn安装失败,请检测网络环境. 使用大陆vps请执行以下命令设置镜像源,再重新运行该脚本:npm config set registry https://registry.npm.taobao.org
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
echo "依赖安装成功"
|
|
||||||
|
|
||||||
echo '启动服务'
|
|
||||||
|
|
||||||
pm2 start ${FILE_PATH}/${SERVER_NAME}/app/main.js --name easynode-server
|
|
||||||
|
|
||||||
echo '查看日志请输入: pm2 log easynode-server'
|
|
85
eslint.config.mjs
Normal file
85
eslint.config.mjs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
|
||||||
|
export default
|
||||||
|
{
|
||||||
|
...pluginVue.configs['flat/recommended'],
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
// 0: 关闭规则(允许) 1/2: 警告warning/错误error(不允许)
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
|
'template-curly-spacing': ['error', 'always'], // 模板字符串空格
|
||||||
|
'default-case': 0,
|
||||||
|
'object-curly-spacing': ['error', 'always'],
|
||||||
|
'no-multi-spaces': ['error'],
|
||||||
|
'indent': ['error', 2, { 'SwitchCase': 1 }], // 缩进:2
|
||||||
|
'quotes': ['error', 'single'], // 引号:single单引 double双引
|
||||||
|
'semi': ['error', 'never'], // 结尾分号:never禁止 always必须
|
||||||
|
'comma-dangle': ['error', 'never'], // 对象拖尾逗号
|
||||||
|
'no-redeclare': ['error', { builtinGlobals: true }], // 禁止重复对象声明
|
||||||
|
'no-multi-assign': 0,
|
||||||
|
'no-restricted-globals': 0,
|
||||||
|
'space-before-function-paren': 0, // 函数定义时括号前面空格
|
||||||
|
'one-var': 0, // 允许连续声明
|
||||||
|
// 'no-undef': 0, // 允许未定义的变量【会使env配置无效】
|
||||||
|
'linebreak-style': 0, // 检测CRLF/LF检测【默认LF】
|
||||||
|
'no-extra-boolean-cast': 0, // 允许意外的Boolean值转换
|
||||||
|
'no-constant-condition': 0, // if语句中禁止常量表达式
|
||||||
|
'no-prototype-builtins': 0, // 允许使用Object.prototypes内置对象(如:xxx.hasOwnProperty)
|
||||||
|
'no-regex-spaces': 0, // 允许正则匹配多个空格
|
||||||
|
'no-unexpected-multiline': 0, // 允许多行表达式
|
||||||
|
'no-fallthrough': 0, // 允许switch穿透
|
||||||
|
'no-delete-var': 0, // 允许 delete 删除对象属性
|
||||||
|
'no-mixed-spaces-and-tabs': 0, // 允许空格tab混用
|
||||||
|
'no-class-assign': 0, // 允许修改class类型
|
||||||
|
'no-param-reassign': 0, // 允许对函数params赋值
|
||||||
|
'max-len': 0, // 允许长行
|
||||||
|
'func-names': 0, // 允许命名函数
|
||||||
|
'import/no-unresolved': 0, // 不检测模块not fund
|
||||||
|
'import/prefer-default-export': 0, // 允许单个导出
|
||||||
|
'no-const-assign': 1, // 警告:修改const命名的变量
|
||||||
|
'no-unused-vars': 1, // 警告:已声明未使用
|
||||||
|
'no-unsafe-negation': 1, // 警告:使用 in / instanceof 关系运算符时,左边表达式请勿使用 ! 否定操作符
|
||||||
|
'use-isnan': 1, // 警告:使用 isNaN() 检查 NaN
|
||||||
|
'no-var': 2, // 禁止使用var声明
|
||||||
|
'no-empty-pattern': 2, // 空解构赋值
|
||||||
|
'eqeqeq': 2, // 必须使用 全等=== 或 非全等 !==
|
||||||
|
'no-cond-assign': 2, // if语句中禁止赋值
|
||||||
|
'no-dupe-args': 2, // 禁止function重复参数
|
||||||
|
'no-dupe-keys': 2, // 禁止object重复key
|
||||||
|
'no-duplicate-case': 2,
|
||||||
|
'no-empty': 2, // 禁止空语句块
|
||||||
|
'no-func-assign': 2, // 禁止重复声明函数
|
||||||
|
'no-inner-declarations': 2, // 禁止在嵌套的语句块中出现变量或 function 声明
|
||||||
|
'no-sparse-arrays': 2, // 禁止稀缺数组
|
||||||
|
'no-unreachable': 2, // 禁止非条件return、throw、continue 和 break 语句后出现代码
|
||||||
|
'no-unsafe-finally': 2, // 禁止finally出现控制流语句,如:return、throw等,因为这会导致try...catch捕获不到
|
||||||
|
'valid-typeof': 2, // 强制 typeof 表达式与有效的字符串进行比较
|
||||||
|
// auto format options
|
||||||
|
'prefer-const': 0, // 禁用声明自动化
|
||||||
|
'no-extra-parens': 0, // 允许函数周围出现不明括号
|
||||||
|
'no-extra-semi': 2, // 禁止不必要的分号
|
||||||
|
// curly: ['error', 'multi'], // if、else、for、while 语句单行代码时不使用大括号
|
||||||
|
'dot-notation': 0, // 允许使用点号或方括号来访问对象属性
|
||||||
|
'dot-location': ['error', 'property'], // 点操作符位置,要求跟随下一行
|
||||||
|
'no-else-return': 2, // 禁止if中有return后又else
|
||||||
|
'no-implicit-coercion': [2, { allow: ['!!', '~', '+'] }], // 禁止隐式转换,allow字段内符号允许
|
||||||
|
'no-trailing-spaces': 1, //一行结束后面不要有空格
|
||||||
|
'no-multiple-empty-lines': [1, { 'max': 1 }], // 空行最多不能超过1行
|
||||||
|
'no-useless-return': 2,
|
||||||
|
'wrap-iife': 0, // 允许自调用函数
|
||||||
|
'yoda': 0, // 允许yoda语句
|
||||||
|
'strict': 0, // 允许strict
|
||||||
|
'no-undef-init': 0, // 允许将变量初始化为undefined
|
||||||
|
'prefer-promise-reject-errors': 0, // 允许使用非 Error 对象作为 Promise 拒绝的原因
|
||||||
|
'consistent-return': 0, // 允许函数不使用return
|
||||||
|
'no-new': 0, // 允许单独new
|
||||||
|
'no-restricted-syntax': 0, // 允许特定的语法
|
||||||
|
'no-plusplus': 0,
|
||||||
|
'import/extensions': 0, // 忽略扩展名
|
||||||
|
'global-require': 0,
|
||||||
|
'no-return-assign': 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
24
modules/auth-db.js
Normal file
24
modules/auth-db.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { existsSync, statSync } from 'fs'
|
||||||
|
|
||||||
|
import {
|
||||||
|
useNuxt,
|
||||||
|
defineNuxtModule,
|
||||||
|
createResolver,
|
||||||
|
addImportsDir,
|
||||||
|
} from '@nuxt/kit'
|
||||||
|
|
||||||
|
// 尝试借助这个模块创建一个空的db文件
|
||||||
|
export default defineNuxtModule({
|
||||||
|
meta: {
|
||||||
|
name: 'auto db',
|
||||||
|
},
|
||||||
|
async setup(_options, nuxt) {
|
||||||
|
// const resolver = createResolver(import.meta.url)
|
||||||
|
// if (existsSync(storesPath) && statSync(storesPath).isFile()) {
|
||||||
|
// // get config
|
||||||
|
// const configPath = resolver.resolve(storesPath)
|
||||||
|
// const config = await import(configPath)
|
||||||
|
// tsConfigs = Object.assign(tsConfigs, config.default)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
})
|
14
nuxt.config.js
Normal file
14
nuxt.config.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
devtools: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
compatibilityDate: "2024-07-03",
|
||||||
|
modules: [
|
||||||
|
"@nuxt/eslint",
|
||||||
|
"@pinia/nuxt",
|
||||||
|
"@nuxtjs/tailwindcss",
|
||||||
|
"@element-plus/nuxt"
|
||||||
|
],
|
||||||
|
elementPlus: {}
|
||||||
|
})
|
39
package.json
39
package.json
@ -1,24 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "easynode",
|
"name": "easynode",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "easy to manage the server",
|
"description": "connect to your server via the easynode",
|
||||||
"private": true,
|
"private": false,
|
||||||
"workspaces": ["server", "client"],
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/chaos-zhu/easynode.git"
|
"url": "git+https://github.com/chaos-zhu/easynode.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"type": "module",
|
||||||
"vps",
|
"scripts": {
|
||||||
"node",
|
"build": "nuxt build",
|
||||||
"easynode",
|
"dev": "nuxt dev",
|
||||||
"chaos",
|
"generate": "nuxt generate",
|
||||||
"chaoszhu"
|
"preview": "nuxt preview",
|
||||||
],
|
"postinstall": "nuxt prepare"
|
||||||
"author": "chaoszhu",
|
|
||||||
"license": "ISC",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/chaos-zhu/easynode/issues"
|
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/chaos-zhu/easynode#readme"
|
"dependencies": {
|
||||||
|
"@nuxt/eslint": "^0.3.13",
|
||||||
|
"@nuxtjs/eslint-module": "^4.1.0",
|
||||||
|
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||||
|
"@pinia/nuxt": "^0.5.1",
|
||||||
|
"nuxt": "^3.12.3",
|
||||||
|
"vue": "latest"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a",
|
||||||
|
"devDependencies": {
|
||||||
|
"@element-plus/nuxt": "^1.0.9",
|
||||||
|
"@nuxt/devtools": "^1.3.9",
|
||||||
|
"element-plus": "^2.7.6",
|
||||||
|
"eslint-plugin-vue": "^9.27.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
58
pages/index.vue
Normal file
58
pages/index.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex miel-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
||||||
|
<div class="sm:mx-auto sm:w-full sm:max-w-sm mt-32">
|
||||||
|
<img class="mx-auto h-10 w-auto" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600">
|
||||||
|
<!-- <h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">Sign in to your account</h2> -->
|
||||||
|
<h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">EasyNode</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
|
<form class="space-y-6" action="#" method="POST">
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium leading-6 text-gray-900">用户名</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<el-input v-model="username" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label for="password" class="block text-sm font-medium leading-6 text-gray-900">密码</label>
|
||||||
|
<!-- <div class="text-sm">
|
||||||
|
<a href="#" class="font-semibold text-indigo-600 hover:text-indigo-500">忘记密码?</a>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<el-input type="password" v-model="password" show-password-on="mousedown" :maxlength="18" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" class="w-full">
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- <p class="mt-10 text-center text-sm text-gray-500">
|
||||||
|
Not a member?
|
||||||
|
<a href="#" class="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">Start a 14 day free trial</a>
|
||||||
|
</p> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const username = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
|
||||||
|
const onLogin = () => {
|
||||||
|
// 实现登录逻辑
|
||||||
|
console.log('Username:', username.value);
|
||||||
|
console.log('Password:', password.value);
|
||||||
|
// 这里可以添加向后端发送请求的代码
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 可以在这里添加额外的CSS样式 */
|
||||||
|
</style>
|
3
plugins/awesome.js
Normal file
3
plugins/awesome.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default defineNuxtPlugin((nuxt) => {
|
||||||
|
// console.log('plugin')
|
||||||
|
})
|
8575
pnpm-lock.yaml
generated
Normal file
8575
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
@ -1,96 +0,0 @@
|
|||||||
// 规则参见:https://cn.eslint.org/docs/rules/
|
|
||||||
module.exports = {
|
|
||||||
root: true, // 当前配置文件不能往父级查找
|
|
||||||
'globals': {
|
|
||||||
'consola': true
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
es6: true
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended' // 应用Eslint全部默认规则
|
|
||||||
],
|
|
||||||
'parserOptions': {
|
|
||||||
'ecmaVersion': 'latest',
|
|
||||||
'sourceType': 'module' // 目标类型 Node项目得添加这个
|
|
||||||
},
|
|
||||||
// 自定义规则,可以覆盖 extends 的配置【安装Eslint插件可以静态检查本地文件是否符合以下规则】
|
|
||||||
'ignorePatterns': ['*.html', 'node-os-utils'],
|
|
||||||
rules: {
|
|
||||||
// 0: 关闭规则(允许) 1/2: 警告warning/错误error(不允许)
|
|
||||||
'no-console': 'off',
|
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
|
||||||
'template-curly-spacing': ['error', 'always'], // 模板字符串空格
|
|
||||||
'default-case': 0,
|
|
||||||
'no-empty': 0,
|
|
||||||
'object-curly-spacing': ['error', 'always'],
|
|
||||||
'no-multi-spaces': ['error'],
|
|
||||||
indent: ['error', 2, { 'SwitchCase': 1 }], // 缩进:2
|
|
||||||
quotes: ['error', 'single'], // 引号:single单引 double双引
|
|
||||||
semi: ['error', 'never'], // 结尾分号:never禁止 always必须
|
|
||||||
'comma-dangle': ['error', 'never'], // 对象拖尾逗号
|
|
||||||
'no-redeclare': ['error', { builtinGlobals: true }], // 禁止重复对象声明
|
|
||||||
'no-multi-assign': 0,
|
|
||||||
'no-restricted-globals': 0,
|
|
||||||
'space-before-function-paren': 0, // 函数定义时括号前面空格
|
|
||||||
'one-var': 0, // 允许连续声明
|
|
||||||
// 'no-undef': 0, // 允许未定义的变量【会使env配置无效】
|
|
||||||
'linebreak-style': 0, // 检测CRLF/LF检测【默认LF】
|
|
||||||
'no-extra-boolean-cast': 0, // 允许意外的Boolean值转换
|
|
||||||
'no-constant-condition': 0, // if语句中禁止常量表达式
|
|
||||||
'no-prototype-builtins': 0, // 允许使用Object.prototypes内置对象(如:xxx.hasOwnProperty)
|
|
||||||
'no-regex-spaces': 0, // 允许正则匹配多个空格
|
|
||||||
'no-unexpected-multiline': 0, // 允许多行表达式
|
|
||||||
'no-fallthrough': 0, // 允许switch穿透
|
|
||||||
'no-delete-var': 0, // 允许 delete 删除对象属性
|
|
||||||
'no-mixed-spaces-and-tabs': 0, // 允许空格tab混用
|
|
||||||
'no-class-assign': 0, // 允许修改class类型
|
|
||||||
'no-param-reassign': 0, // 允许对函数params赋值
|
|
||||||
'max-len': 0, // 允许长行
|
|
||||||
'func-names': 0, // 允许命名函数
|
|
||||||
'import/no-unresolved': 0, // 不检测模块not fund
|
|
||||||
'import/prefer-default-export': 0, // 允许单个导出
|
|
||||||
'no-const-assign': 1, // 警告:修改const命名的变量
|
|
||||||
'no-unused-vars': 1, // 警告:已声明未使用
|
|
||||||
'no-unsafe-negation': 1, // 警告:使用 in / instanceof 关系运算符时,左边表达式请勿使用 ! 否定操作符
|
|
||||||
'use-isnan': 1, // 警告:使用 isNaN() 检查 NaN
|
|
||||||
'no-var': 2, // 禁止使用var声明
|
|
||||||
'no-empty-pattern': 2, // 空解构赋值
|
|
||||||
'eqeqeq': 2, // 必须使用 全等=== 或 非全等 !==
|
|
||||||
'no-cond-assign': 2, // if语句中禁止赋值
|
|
||||||
'no-dupe-args': 2, // 禁止function重复参数
|
|
||||||
'no-dupe-keys': 2, // 禁止object重复key
|
|
||||||
'no-duplicate-case': 2,
|
|
||||||
'no-func-assign': 2, // 禁止重复声明函数
|
|
||||||
'no-inner-declarations': 2, // 禁止在嵌套的语句块中出现变量或 function 声明
|
|
||||||
'no-sparse-arrays': 2, // 禁止稀缺数组
|
|
||||||
'no-unreachable': 2, // 禁止非条件return、throw、continue 和 break 语句后出现代码
|
|
||||||
'no-unsafe-finally': 2, // 禁止finally出现控制流语句,如:return、throw等,因为这会导致try...catch捕获不到
|
|
||||||
'valid-typeof': 2, // 强制 typeof 表达式与有效的字符串进行比较
|
|
||||||
// auto format options
|
|
||||||
'prefer-const': 0, // 禁用声明自动化
|
|
||||||
'no-extra-parens': 0, // 允许函数周围出现不明括号
|
|
||||||
'no-extra-semi': 2, // 禁止不必要的分号
|
|
||||||
// curly: ['error', 'multi'], // if、else、for、while 语句单行代码时不使用大括号
|
|
||||||
'dot-notation': 0, // 允许使用点号或方括号来访问对象属性
|
|
||||||
'dot-location': ['error', 'property'], // 点操作符位置,要求跟随下一行
|
|
||||||
'no-else-return': 2, // 禁止if中有return后又else
|
|
||||||
'no-implicit-coercion': [2, { allow: ['!!', '~', '+'] }], // 禁止隐式转换,allow字段内符号允许
|
|
||||||
'no-trailing-spaces': 1, //一行结束后面不要有空格
|
|
||||||
'no-multiple-empty-lines': [1, { 'max': 1 }], // 空行最多不能超过1行
|
|
||||||
'no-useless-return': 2,
|
|
||||||
'wrap-iife': 0, // 允许自调用函数
|
|
||||||
'yoda': 0, // 允许yoda语句
|
|
||||||
'strict': 0, // 允许strict
|
|
||||||
'no-undef-init': 0, // 允许将变量初始化为undefined
|
|
||||||
'prefer-promise-reject-errors': 0, // 允许使用非 Error 对象作为 Promise 拒绝的原因
|
|
||||||
'consistent-return': 0, // 允许函数不使用return
|
|
||||||
'no-new': 0, // 允许单独new
|
|
||||||
'no-restricted-syntax': 0, // 允许特定的语法
|
|
||||||
'no-plusplus': 0,
|
|
||||||
'import/extensions': 0, // 忽略扩展名
|
|
||||||
'global-require': 0,
|
|
||||||
'no-return-assign': 0
|
|
||||||
}
|
|
||||||
}
|
|
7
server/.gitignore
vendored
7
server/.gitignore
vendored
@ -1,7 +0,0 @@
|
|||||||
node_modules
|
|
||||||
app/static/upload/*
|
|
||||||
app/socket/temp/*
|
|
||||||
app/socket/.sftp-cache/*
|
|
||||||
app/logs/*
|
|
||||||
!.gitkeep
|
|
||||||
dist
|
|
@ -1,13 +0,0 @@
|
|||||||
FROM node:16.15.0-alpine3.14
|
|
||||||
ARG TARGET_DIR=/easynode-server
|
|
||||||
WORKDIR ${TARGET_DIR}
|
|
||||||
RUN yarn config set registry https://registry.npm.taobao.org
|
|
||||||
COPY package.json ${TARGET_DIR}
|
|
||||||
COPY yarn.lock ${TARGET_DIR}
|
|
||||||
RUN yarn
|
|
||||||
COPY . ${TARGET_DIR}
|
|
||||||
ENV HOST 0.0.0.0
|
|
||||||
EXPOSE 8082
|
|
||||||
EXPOSE 8083
|
|
||||||
EXPOSE 22022
|
|
||||||
CMD ["npm", "run", "server"]
|
|
@ -1,40 +0,0 @@
|
|||||||
# 面板服务端
|
|
||||||
|
|
||||||
- 基于Koa
|
|
||||||
|
|
||||||
## docker
|
|
||||||
|
|
||||||
<!-- 修改版本号 -->
|
|
||||||
- 构建镜像:docker build -t chaoszhu/easynode:v1.1 .
|
|
||||||
- 推送镜像:docker push chaoszhu/easynode:v1.1
|
|
||||||
|
|
||||||
> `docker run -d --net=host easynode-server`
|
|
||||||
<!-- > `docker run -d -p 8888:8082 -p 22022:22022 easynode-server` -->
|
|
||||||
|
|
||||||
## 遇到的问题
|
|
||||||
|
|
||||||
> MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 input listeners added to [Socket]. Use emitter.setMaxListeners() to increase limit
|
|
||||||
- ssh连接数过多(默认最多11个)
|
|
||||||
- 每次连接新建一个vps实例,断开则销毁
|
|
||||||
|
|
||||||
> Error signing data with key: error:0D07209B:asn1 encoding routines:ASN1_get_object:too long
|
|
||||||
- 经比对,ssh的rsa密钥在前端往后端的存储过程中丢失了部分字符
|
|
||||||
|
|
||||||
> 获取客户端信息:跨域请求客户端系统信息,建立ws socket实时更新网络
|
|
||||||
- 问题:服务端前端套上https后,前端无法请求客户端(http)的信息, 也无法建立ws socket连接(原因是https下无法建立http/ws协议请求)
|
|
||||||
- 方案1: 所有客户端与服务端通信,再全部由服务端与前端通信(考虑:服务端/客户端性能问题). Node实现http+https||nginx转发实现https
|
|
||||||
- 方案2: 给所有客户端加上https(客户端只有ip,没法给个人ip签订证书)
|
|
||||||
|
|
||||||
## 构建运行包
|
|
||||||
|
|
||||||
### 坑
|
|
||||||
|
|
||||||
> log4js: 该module使用到了fs.mkdir()等读写api,pkg打包后的环境不支持,设置保存日志的目录需使用process.cwd()】
|
|
||||||
|
|
||||||
> win闪退: 在linux机器上构建可查看输出日志
|
|
||||||
|
|
||||||
## 客户端
|
|
||||||
|
|
||||||
> **构建客户端服务, 后台运行** `nohup ./easynode-server &`
|
|
||||||
|
|
||||||
> 功能:服务器基本信息【ssh信息保存在主服务器】
|
|
3
server/api/get-user-info.js
Normal file
3
server/api/get-user-info.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
return 'get-user-info'
|
||||||
|
})
|
@ -1,20 +0,0 @@
|
|||||||
const path = require('path')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
httpPort: 8082,
|
|
||||||
clientPort: 22022, // 勿更改
|
|
||||||
uploadDir: path.join(process.cwd(),'app/static/upload'),
|
|
||||||
staticDir: path.join(process.cwd(),'app/static'),
|
|
||||||
sftpCacheDir: path.join(process.cwd(),'app/socket/sftp-cache'),
|
|
||||||
sshRecordPath: path.join(process.cwd(),'app/storage/ssh-record.json'),
|
|
||||||
keyPath: path.join(process.cwd(),'app/storage/key.json'),
|
|
||||||
hostListPath: path.join(process.cwd(),'app/storage/host-list.json'),
|
|
||||||
emailPath: path.join(process.cwd(),'app/storage/email.json'),
|
|
||||||
notifyPath: path.join(process.cwd(),'app/storage/notify.json'),
|
|
||||||
groupPath: path.join(process.cwd(),'app/storage/group.json'),
|
|
||||||
apiPrefix: '/api/v1',
|
|
||||||
logConfig: {
|
|
||||||
outDir: path.join(process.cwd(),'./app/logs'),
|
|
||||||
recordLog: false // 是否记录日志
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
const { readGroupList, writeGroupList, readHostList, writeHostList,randomStr } = require('../utils')
|
|
||||||
|
|
||||||
function getGroupList({ res }) {
|
|
||||||
const data = readGroupList()
|
|
||||||
res.success({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
const addGroupList = async ({ res, request }) => {
|
|
||||||
let { body: { name, index } } = request
|
|
||||||
if(!name) return res.fail({ data: false, msg: '参数错误' })
|
|
||||||
let groupList = readGroupList()
|
|
||||||
let group = { id: randomStr(), name, index }
|
|
||||||
groupList.push(group)
|
|
||||||
groupList.sort((a, b) => a.index - b.index)
|
|
||||||
writeGroupList(groupList)
|
|
||||||
res.success({ data: '新增成功' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateGroupList = async ({ res, request }) => {
|
|
||||||
let { params: { id } } = request
|
|
||||||
let { body: { name, index } } = request
|
|
||||||
if(!id || !name) return res.fail({ data: false, msg: '参数错误' })
|
|
||||||
let groupList = readGroupList()
|
|
||||||
let idx = groupList.findIndex(item => item.id === id)
|
|
||||||
let group = { id, name, index }
|
|
||||||
if(idx === -1) return res.fail({ data: false, msg: `分组ID${ id }不存在` })
|
|
||||||
groupList.splice(idx, 1, group)
|
|
||||||
groupList.sort((a, b) => a.index - b.index)
|
|
||||||
writeGroupList(groupList)
|
|
||||||
res.success({ data: '修改成功' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeGroup = async ({ res, request }) => {
|
|
||||||
let { params: { id } } = request
|
|
||||||
if(id ==='default') return res.fail({ data: false, msg: '保留分组, 禁止删除' })
|
|
||||||
let groupList = readGroupList()
|
|
||||||
let idx = groupList.findIndex(item => item.id === id)
|
|
||||||
if(idx === -1) return res.fail({ msg: '分组不存在' })
|
|
||||||
|
|
||||||
// 移除分组将所有该分组下host分配到default中去
|
|
||||||
let hostList = readHostList()
|
|
||||||
hostList = hostList.map((item) => {
|
|
||||||
if(item.group === groupList[idx].id) item.group = 'default'
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
writeHostList(hostList)
|
|
||||||
|
|
||||||
groupList.splice(idx, 1)
|
|
||||||
writeGroupList(groupList)
|
|
||||||
|
|
||||||
res.success({ data: '移除成功' })
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
addGroupList,
|
|
||||||
getGroupList,
|
|
||||||
updateGroupList,
|
|
||||||
removeGroup
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
const { readHostList, writeHostList, readSSHRecord, writeSSHRecord } = require('../utils')
|
|
||||||
|
|
||||||
function getHostList({ res }) {
|
|
||||||
const data = readHostList()
|
|
||||||
res.success({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveHost({ res, request }) {
|
|
||||||
let { body: { host: newHost, name, expired, expiredNotify, group, consoleUrl, remark } } = request
|
|
||||||
if(!newHost || !name) return res.fail({ msg: 'missing params: name or host' })
|
|
||||||
let hostList = readHostList()
|
|
||||||
if(hostList.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
|
|
||||||
hostList.push({ host: newHost, name, expired, expiredNotify, group, consoleUrl, remark })
|
|
||||||
writeHostList(hostList)
|
|
||||||
res.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHost({ res, request }) {
|
|
||||||
let { body: { host: newHost, name: newName, oldHost, expired, expiredNotify, group, consoleUrl, remark } } = request
|
|
||||||
if(!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
|
||||||
let hostList = readHostList()
|
|
||||||
if(!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${ newHost }不存在` })
|
|
||||||
let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
|
|
||||||
hostList.splice(targetIdx, 1, { name: newName, host: newHost, expired, expiredNotify, group, consoleUrl, remark })
|
|
||||||
writeHostList(hostList)
|
|
||||||
res.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeHost({ res, request }) {
|
|
||||||
let { body: { host } } = request
|
|
||||||
let hostList = readHostList()
|
|
||||||
let hostIdx = hostList.findIndex(item => item.host === host)
|
|
||||||
if(hostIdx === -1) return res.fail({ msg: `${ host }不存在` })
|
|
||||||
hostList.splice(hostIdx, 1)
|
|
||||||
writeHostList(hostList)
|
|
||||||
// 查询是否存在ssh记录
|
|
||||||
let sshRecord = readSSHRecord()
|
|
||||||
let sshIdx = sshRecord.findIndex(item => item.host === host)
|
|
||||||
let flag = sshIdx !== -1
|
|
||||||
if(flag) sshRecord.splice(sshIdx, 1)
|
|
||||||
writeSSHRecord(sshRecord)
|
|
||||||
|
|
||||||
res.success({ data: `${ host }已移除, ${ flag ? '并移除ssh记录' : '' }` })
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHostSort({ res, request }) {
|
|
||||||
let { body: { list } } = request
|
|
||||||
if(!list) return res.fail({ msg: '参数错误' })
|
|
||||||
let hostList = readHostList()
|
|
||||||
if(hostList.length !== list.length) return res.fail({ msg: '失败: host数量不匹配' })
|
|
||||||
let sortResult = []
|
|
||||||
for (let i = 0; i < list.length; i++) {
|
|
||||||
const curHost = list[i]
|
|
||||||
let temp = hostList.find(({ host }) => curHost.host === host)
|
|
||||||
if(!temp) return res.fail({ msg: `查找失败: ${ curHost.name }` })
|
|
||||||
sortResult.push(temp)
|
|
||||||
}
|
|
||||||
writeHostList(sortResult)
|
|
||||||
res.success({ msg: 'success' })
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getHostList,
|
|
||||||
saveHost,
|
|
||||||
updateHost,
|
|
||||||
removeHost,
|
|
||||||
updateHostSort
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
const {
|
|
||||||
readSupportEmailList,
|
|
||||||
readUserEmailList,
|
|
||||||
writeUserEmailList,
|
|
||||||
emailTransporter,
|
|
||||||
readNotifyList,
|
|
||||||
writeNotifyList } = require('../utils')
|
|
||||||
const commonTemp = require('../template/commonTemp')
|
|
||||||
|
|
||||||
function getSupportEmailList({ res }) {
|
|
||||||
const data = readSupportEmailList()
|
|
||||||
res.success({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserEmailList({ res }) {
|
|
||||||
const userEmailList = readUserEmailList().map(({ target, auth: { user } }) => ({ target, user }))
|
|
||||||
const supportEmailList = readSupportEmailList()
|
|
||||||
const data = userEmailList.map(({ target: userTarget, user: email }) => {
|
|
||||||
let name = supportEmailList.find(({ target: supportTarget }) => supportTarget === userTarget).name
|
|
||||||
return { name, email }
|
|
||||||
})
|
|
||||||
res.success({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pushEmail({ res, request }) {
|
|
||||||
let { body: { toEmail, isTest } } = request
|
|
||||||
if(!isTest) return res.fail({ msg: '此接口暂时只做测试邮件使用, 需传递参数isTest: true' })
|
|
||||||
consola.info('发送测试邮件:', toEmail)
|
|
||||||
let { code, msg } = await emailTransporter({ toEmail, title: '测试邮件', html: commonTemp('邮件通知测试邮件') })
|
|
||||||
msg = msg && msg.message || msg
|
|
||||||
if(code === 0) return res.success({ msg })
|
|
||||||
return res.fail({ msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUserEmailList({ res, request }) {
|
|
||||||
let { body: { target, auth } } = request
|
|
||||||
const supportList = readSupportEmailList()
|
|
||||||
let flag = supportList.some((item) => item.target === target)
|
|
||||||
if(!flag) return res.fail({ msg: `不支持的邮箱类型:${ target }` })
|
|
||||||
if(!auth.user || !auth.pass) return res.fail({ msg: 'missing params: auth.' })
|
|
||||||
|
|
||||||
let newUserEmail = { target, auth }
|
|
||||||
let userEmailList = readUserEmailList()
|
|
||||||
let idx = userEmailList.findIndex(({ auth: { user } }) => auth.user === user)
|
|
||||||
if(idx !== -1) userEmailList.splice(idx, 1, newUserEmail)
|
|
||||||
else userEmailList.unshift(newUserEmail)
|
|
||||||
|
|
||||||
const { code, msg } = writeUserEmailList(userEmailList)
|
|
||||||
if(code === 0) return res.success()
|
|
||||||
return res.fail({ msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeUserEmail({ res, request }) {
|
|
||||||
let { params: { email } } = request
|
|
||||||
const userEmailList = readUserEmailList()
|
|
||||||
let idx = userEmailList.findIndex(({ auth: { user } }) => user === email)
|
|
||||||
if(idx === -1) return res.fail({ msg: `删除失败, 不存在该邮箱:${ email }` })
|
|
||||||
userEmailList.splice(idx, 1)
|
|
||||||
const { code, msg } = writeUserEmailList(userEmailList)
|
|
||||||
if(code === 0) return res.success()
|
|
||||||
return res.fail({ msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNotifyList({ res }) {
|
|
||||||
const data = readNotifyList()
|
|
||||||
res.success({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateNotifyList({ res, request }) {
|
|
||||||
let { body: { type, sw } } = request
|
|
||||||
if(!([true, false].includes(sw))) return res.fail({ msg: `Error type for sw:${ sw }, must be Boolean` })
|
|
||||||
const notifyList = readNotifyList()
|
|
||||||
let target = notifyList.find((item) => item.type === type)
|
|
||||||
if(!target) return res.fail({ msg: `更新失败, 不存在该通知类型:${ type }` })
|
|
||||||
target.sw = sw
|
|
||||||
console.log(notifyList)
|
|
||||||
writeNotifyList(notifyList)
|
|
||||||
res.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
pushEmail,
|
|
||||||
getSupportEmailList,
|
|
||||||
getUserEmailList,
|
|
||||||
updateUserEmailList,
|
|
||||||
removeUserEmail,
|
|
||||||
getNotifyList,
|
|
||||||
updateNotifyList
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
const { readSSHRecord, writeSSHRecord, AESEncrypt } = require('../utils')
|
|
||||||
|
|
||||||
const updateSSH = async ({ res, request }) => {
|
|
||||||
let { body: { host, port, username, type, password, privateKey, randomKey, command } } = request
|
|
||||||
let record = { host, port, username, type, password, privateKey, randomKey, command }
|
|
||||||
if(!host || !port || !username || !type || !randomKey) return res.fail({ data: false, msg: '参数错误' })
|
|
||||||
// 再做一次对称加密(方便ssh连接时解密)
|
|
||||||
record.randomKey = AESEncrypt(randomKey)
|
|
||||||
let sshRecord = readSSHRecord()
|
|
||||||
let idx = sshRecord.findIndex(item => item.host === host)
|
|
||||||
if(idx === -1)
|
|
||||||
sshRecord.push(record)
|
|
||||||
else
|
|
||||||
sshRecord.splice(idx, 1, record)
|
|
||||||
writeSSHRecord(sshRecord)
|
|
||||||
consola.info('新增凭证:', host)
|
|
||||||
res.success({ data: '保存成功' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeSSH = async ({ res, request }) => {
|
|
||||||
let { body: { host } } = request
|
|
||||||
let sshRecord = readSSHRecord()
|
|
||||||
let idx = sshRecord.findIndex(item => item.host === host)
|
|
||||||
if(idx === -1) return res.fail({ msg: '凭证不存在' })
|
|
||||||
sshRecord.splice(idx, 1)
|
|
||||||
consola.info('移除凭证:', host)
|
|
||||||
writeSSHRecord(sshRecord)
|
|
||||||
res.success({ data: '移除成功' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const existSSH = async ({ res, request }) => {
|
|
||||||
let { body: { host } } = request
|
|
||||||
let sshRecord = readSSHRecord()
|
|
||||||
let idx = sshRecord.findIndex(item => item.host === host)
|
|
||||||
consola.info('查询凭证:', host)
|
|
||||||
if(idx === -1) return res.success({ data: false }) // host不存在
|
|
||||||
res.success({ data: true }) // 存在
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCommand = async ({ res, request }) => {
|
|
||||||
let { host } = request.query
|
|
||||||
if(!host) return res.fail({ data: false, msg: '参数错误' })
|
|
||||||
let sshRecord = readSSHRecord()
|
|
||||||
let record = sshRecord.find(item => item.host === host)
|
|
||||||
consola.info('查询登录后执行的指令:', host)
|
|
||||||
if(!record) return res.fail({ data: false, msg: 'host not found' }) // host不存在
|
|
||||||
const { command } = record
|
|
||||||
if(!command) return res.success({ data: false }) // command不存在
|
|
||||||
res.success({ data: command }) // 存在
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
updateSSH,
|
|
||||||
removeSSH,
|
|
||||||
existSSH,
|
|
||||||
getCommand
|
|
||||||
}
|
|
@ -1,118 +0,0 @@
|
|||||||
const jwt = require('jsonwebtoken')
|
|
||||||
const { getNetIPInfo, readKey, writeKey, RSADecrypt, AESEncrypt, SHA1Encrypt, sendEmailToConfList, getNotifySwByType } = require('../utils')
|
|
||||||
|
|
||||||
const getpublicKey = ({ res }) => {
|
|
||||||
let { publicKey: data } = readKey()
|
|
||||||
if(!data) return res.fail({ msg: 'publicKey not found, Try to restart the server', status: 500 })
|
|
||||||
res.success({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
let timer = null
|
|
||||||
const allowErrCount = 5 // 允许错误的次数
|
|
||||||
const forbidTimer = 60 * 5 // 禁止登录时间
|
|
||||||
let loginErrCount = 0 // 每一轮的登录错误次数
|
|
||||||
let loginErrTotal = 0 // 总的登录错误次数
|
|
||||||
let loginCountDown = forbidTimer
|
|
||||||
let forbidLogin = false
|
|
||||||
|
|
||||||
const login = async ({ res, request }) => {
|
|
||||||
let { body: { ciphertext, jwtExpires }, ip: clientIp } = request
|
|
||||||
if(!ciphertext) return res.fail({ msg: '参数错误' })
|
|
||||||
|
|
||||||
if(forbidLogin) return res.fail({ msg: `禁止登录! 倒计时[${ loginCountDown }s]后尝试登录或重启面板服务` })
|
|
||||||
|
|
||||||
loginErrCount++
|
|
||||||
loginErrTotal++
|
|
||||||
if(loginErrCount >= allowErrCount) {
|
|
||||||
const { ip, country, city } = await getNetIPInfo(clientIp)
|
|
||||||
// 发送通知&禁止登录
|
|
||||||
let sw = getNotifySwByType('err_login')
|
|
||||||
if(sw) sendEmailToConfList('登录错误提醒', `重新登录次数: ${ loginErrTotal }<br/>地点:${ country + city }<br/>IP: ${ ip }`)
|
|
||||||
forbidLogin = true
|
|
||||||
loginErrCount = 0
|
|
||||||
|
|
||||||
// forbidTimer秒后解禁
|
|
||||||
setTimeout(() => {
|
|
||||||
forbidLogin = false
|
|
||||||
}, loginCountDown * 1000)
|
|
||||||
|
|
||||||
// 计算登录倒计时
|
|
||||||
timer = setInterval(() => {
|
|
||||||
if(loginCountDown <= 0){
|
|
||||||
clearInterval(timer)
|
|
||||||
loginCountDown = forbidTimer
|
|
||||||
return
|
|
||||||
}
|
|
||||||
loginCountDown--
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登录流程
|
|
||||||
try {
|
|
||||||
// console.log('ciphertext', ciphertext)
|
|
||||||
let password = RSADecrypt(ciphertext)
|
|
||||||
// console.log('Decrypt解密password:', password)
|
|
||||||
let { pwd } = readKey()
|
|
||||||
if(password === 'admin' && pwd === 'admin') {
|
|
||||||
const token = await beforeLoginHandler(clientIp, jwtExpires)
|
|
||||||
return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认密码' })
|
|
||||||
}
|
|
||||||
password = SHA1Encrypt(password)
|
|
||||||
if(password !== pwd) return res.fail({ msg: '密码错误' })
|
|
||||||
const token = await beforeLoginHandler(clientIp, jwtExpires)
|
|
||||||
return res.success({ data: { token, jwtExpires }, msg: '登录成功' })
|
|
||||||
} catch (error) {
|
|
||||||
console.log('解密失败:', error)
|
|
||||||
res.fail({ msg: '解密失败, 请查看服务端日志' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const beforeLoginHandler = async (clientIp, jwtExpires) => {
|
|
||||||
loginErrCount = loginErrTotal = 0 // 登录成功, 清空错误次数
|
|
||||||
|
|
||||||
// consola.success('登录成功, 准备生成token', new Date())
|
|
||||||
// 生产token
|
|
||||||
let { commonKey } = readKey()
|
|
||||||
let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token
|
|
||||||
token = AESEncrypt(token) // 对称加密token后再传输给前端
|
|
||||||
|
|
||||||
// 记录客户端登录IP(用于判断是否异地且只保留最近10条)
|
|
||||||
const clientIPInfo = await getNetIPInfo(clientIp)
|
|
||||||
const { ip, country, city } = clientIPInfo || {}
|
|
||||||
|
|
||||||
// 邮件登录通知
|
|
||||||
let sw = getNotifySwByType('login')
|
|
||||||
if(sw) sendEmailToConfList('登录提醒', `地点:${ country + city }<br/>IP: ${ ip }`)
|
|
||||||
|
|
||||||
global.loginRecord.unshift(clientIPInfo)
|
|
||||||
if(global.loginRecord.length > 10) global.loginRecord = global.loginRecord.slice(0, 10)
|
|
||||||
return token
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePwd = async ({ res, request }) => {
|
|
||||||
let { body: { oldPwd, newPwd } } = request
|
|
||||||
let rsaOldPwd = RSADecrypt(oldPwd)
|
|
||||||
oldPwd = rsaOldPwd === 'admin' ? 'admin' : SHA1Encrypt(rsaOldPwd)
|
|
||||||
let keyObj = readKey()
|
|
||||||
if(oldPwd !== keyObj.pwd) return res.fail({ data: false, msg: '旧密码校验失败' })
|
|
||||||
// 旧密钥校验通过,加密保存新密码
|
|
||||||
newPwd = RSADecrypt(newPwd) === 'admin' ? 'admin' : SHA1Encrypt(RSADecrypt(newPwd))
|
|
||||||
keyObj.pwd = newPwd
|
|
||||||
writeKey(keyObj)
|
|
||||||
|
|
||||||
let sw = getNotifySwByType('updatePwd')
|
|
||||||
if(sw) sendEmailToConfList('密码修改提醒', '面板登录密码已更改')
|
|
||||||
|
|
||||||
res.success({ data: true, msg: 'success' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLoginRecord = async ({ res }) => {
|
|
||||||
res.success({ data: global.loginRecord, msg: 'success' })
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
login,
|
|
||||||
getpublicKey,
|
|
||||||
updatePwd,
|
|
||||||
getLoginRecord
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
const NodeRSA = require('node-rsa')
|
|
||||||
const { getNetIPInfo, readHostList, writeHostList, readKey, writeKey, randomStr, isProd, AESEncrypt } = require('./utils')
|
|
||||||
|
|
||||||
const isDev = !isProd()
|
|
||||||
|
|
||||||
// 存储本机IP, 供host列表接口调用
|
|
||||||
async function initLocalIp() {
|
|
||||||
if(isDev) return consola.info('非生产环境不初始化保存本地IP')
|
|
||||||
const localNetIPInfo = await getNetIPInfo()
|
|
||||||
let vpsList = readHostList()
|
|
||||||
let { ip: localNetIP } = localNetIPInfo
|
|
||||||
if(vpsList.some(({ host }) => host === localNetIP)) return consola.info('本机IP已储存: ', localNetIP)
|
|
||||||
vpsList.unshift({ name: 'server-side-host', host: localNetIP, group: 'default' })
|
|
||||||
writeHostList(vpsList)
|
|
||||||
consola.info('Task: 生产环境首次启动储存本机IP: ', localNetIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化公私钥, 供登录、保存ssh密钥/密码等加解密
|
|
||||||
async function initRsa() {
|
|
||||||
let keyObj = readKey()
|
|
||||||
if(keyObj.privateKey && keyObj.publicKey) return consola.info('公私钥已存在[重新生成会导致已保存的ssh密钥信息失效]')
|
|
||||||
|
|
||||||
let key = new NodeRSA({ b: 1024 })
|
|
||||||
key.setOptions({ encryptionScheme: 'pkcs1' })
|
|
||||||
let privateKey = key.exportKey('pkcs1-private-pem')
|
|
||||||
let publicKey = key.exportKey('pkcs8-public-pem')
|
|
||||||
keyObj.privateKey = AESEncrypt(privateKey) // 加密私钥
|
|
||||||
keyObj.publicKey = publicKey // 公开公钥
|
|
||||||
writeKey(keyObj)
|
|
||||||
consola.info('Task: 已生成新的非对称加密公私钥')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机的commonKey secret
|
|
||||||
function randomJWTSecret() {
|
|
||||||
let keyObj = readKey()
|
|
||||||
if(keyObj.commonKey) return consola.info('commonKey密钥已存在')
|
|
||||||
|
|
||||||
keyObj.commonKey = randomStr(16)
|
|
||||||
writeKey(keyObj)
|
|
||||||
consola.info('Task: 已生成新的随机commonKey密钥')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
randomJWTSecret() // 先生成全局唯一密钥
|
|
||||||
initLocalIp()
|
|
||||||
initRsa()
|
|
||||||
// 用于记录客户端登录IP的列表
|
|
||||||
global.loginRecord = []
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
const consola = require('consola')
|
|
||||||
global.consola = consola
|
|
||||||
const { httpServer, clientHttpServer } = require('./server')
|
|
||||||
const initLocal = require('./init')
|
|
||||||
const scheduleJob = require('./schedule')
|
|
||||||
|
|
||||||
scheduleJob()
|
|
||||||
|
|
||||||
initLocal()
|
|
||||||
|
|
||||||
httpServer()
|
|
||||||
|
|
||||||
clientHttpServer()
|
|
@ -1,27 +0,0 @@
|
|||||||
const { verifyAuth } = require('../utils')
|
|
||||||
const { apiPrefix } = require('../config')
|
|
||||||
|
|
||||||
let whitePath = [
|
|
||||||
'/login',
|
|
||||||
'/get-pub-pem'
|
|
||||||
].map(item => (apiPrefix + item))
|
|
||||||
consola.info('路由白名单:', whitePath)
|
|
||||||
|
|
||||||
const useAuth = async ({ request, res }, next) => {
|
|
||||||
const { path, headers: { token } } = request
|
|
||||||
consola.info('verify path: ', path)
|
|
||||||
if(whitePath.includes(path)) return next()
|
|
||||||
if(!token) return res.fail({ msg: '未登录', status: 403 })
|
|
||||||
// 验证token
|
|
||||||
const { code, msg } = verifyAuth(token, request.ip)
|
|
||||||
switch(code) {
|
|
||||||
case 1:
|
|
||||||
return await next()
|
|
||||||
case -1:
|
|
||||||
return res.fail({ msg, status: 401 })
|
|
||||||
case -2:
|
|
||||||
return res.fail({ msg: '登录态错误, 请重新登录', status: 401, data: msg })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = useAuth
|
|
@ -1,12 +0,0 @@
|
|||||||
const koaBody = require('koa-body')
|
|
||||||
const { uploadDir } = require('../config')
|
|
||||||
|
|
||||||
module.exports = koaBody({
|
|
||||||
multipart: true, // 支持 multipart-formdate 的表单
|
|
||||||
formidable: {
|
|
||||||
uploadDir, // 上传目录
|
|
||||||
keepExtensions: true, // 保持文件的后缀
|
|
||||||
multipart: true, // 多文件上传
|
|
||||||
maxFieldsSize: 2 * 1024 * 1024 // 文件上传大小 单位:B
|
|
||||||
}
|
|
||||||
})
|
|
@ -1,5 +0,0 @@
|
|||||||
const compress = require('koa-compress')
|
|
||||||
|
|
||||||
const options = { threshold: 2048 }
|
|
||||||
|
|
||||||
module.exports = compress(options)
|
|
@ -1,12 +0,0 @@
|
|||||||
const cors = require('@koa/cors')
|
|
||||||
|
|
||||||
// 跨域处理
|
|
||||||
const useCors = cors({
|
|
||||||
origin: ({ req }) => {
|
|
||||||
return req.headers.origin
|
|
||||||
},
|
|
||||||
credentials: true,
|
|
||||||
allowMethods: [ 'GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH' ]
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = useCors
|
|
@ -1,3 +0,0 @@
|
|||||||
const { historyApiFallback } = require('koa2-connect-history-api-fallback')
|
|
||||||
|
|
||||||
module.exports = historyApiFallback({ whiteList: ['/api'] })
|
|
@ -1,23 +0,0 @@
|
|||||||
const responseHandler = require('./response') // 统一返回格式, 错误捕获
|
|
||||||
const useAuth = require('./auth') // 鉴权
|
|
||||||
// const useCors = require('./cors') // 处理跨域[暂时禁止]
|
|
||||||
const useLog = require('./log4') // 记录日志,需要等待路由处理完成,所以得放路由前
|
|
||||||
const useKoaBody = require('./body') // 处理body参数 【请求需先走该中间件】
|
|
||||||
const { useRoutes, useAllowedMethods } = require('./router') // 路由管理
|
|
||||||
const useStatic = require('./static') // 静态目录
|
|
||||||
const compress = require('./compress') // br/gzip压缩
|
|
||||||
const history = require('./history') // vue-router的history模式
|
|
||||||
|
|
||||||
// 注意注册顺序
|
|
||||||
module.exports = [
|
|
||||||
compress,
|
|
||||||
history,
|
|
||||||
useStatic, // staic先注册,不然会被jwt拦截
|
|
||||||
// useCors,
|
|
||||||
responseHandler,
|
|
||||||
useKoaBody, // 先处理body,log和router都要用到
|
|
||||||
useLog, // 日志记录开始【该module使用到了fs.mkdir()等读写api, 设置保存日志的目录需使用process.cwd()】
|
|
||||||
useAuth,
|
|
||||||
useAllowedMethods,
|
|
||||||
useRoutes
|
|
||||||
]
|
|
@ -1,58 +0,0 @@
|
|||||||
const log4js = require('log4js')
|
|
||||||
const { outDir, recordLog } = require('../config').logConfig
|
|
||||||
|
|
||||||
log4js.configure({
|
|
||||||
appenders: {
|
|
||||||
// 控制台输出
|
|
||||||
out: {
|
|
||||||
type: 'stdout',
|
|
||||||
layout: {
|
|
||||||
type: 'colored'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 保存日志文件
|
|
||||||
cheese: {
|
|
||||||
type: 'file',
|
|
||||||
maxLogSize: 512*1024, // unit: bytes 1KB = 1024bytes
|
|
||||||
filename: `${ outDir }/receive.log`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
categories: {
|
|
||||||
default: {
|
|
||||||
appenders: [ 'out', 'cheese' ], // 配置
|
|
||||||
level: 'info' // 只输出info以上级别的日志
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// pm2: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const logger = log4js.getLogger()
|
|
||||||
|
|
||||||
const useLog = () => {
|
|
||||||
return async (ctx, next) => {
|
|
||||||
const { method, path, origin, query, body, headers, ip } = ctx.request
|
|
||||||
const data = {
|
|
||||||
method,
|
|
||||||
path,
|
|
||||||
origin,
|
|
||||||
query,
|
|
||||||
body,
|
|
||||||
ip,
|
|
||||||
headers
|
|
||||||
}
|
|
||||||
await next() // 等待路由处理完成,再开始记录日志
|
|
||||||
// 是否记录日志
|
|
||||||
if (recordLog) {
|
|
||||||
const { status, params } = ctx
|
|
||||||
data.status = status
|
|
||||||
data.params = params
|
|
||||||
data.result = ctx.body || 'no content'
|
|
||||||
if (String(status).startsWith(4) || String(status).startsWith(5))
|
|
||||||
logger.error(JSON.stringify(data))
|
|
||||||
else
|
|
||||||
logger.info(JSON.stringify(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = useLog()
|
|
@ -1,33 +0,0 @@
|
|||||||
const responseHandler = async (ctx, next) => {
|
|
||||||
// 统一成功响应
|
|
||||||
ctx.res.success = ({ status, data, msg = 'success' } = {}) => {
|
|
||||||
ctx.status = status || 200 // 没传默认200
|
|
||||||
ctx.body = {
|
|
||||||
status: ctx.status, // 响应成功默认 200
|
|
||||||
data,
|
|
||||||
msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 统一错误响应
|
|
||||||
ctx.res.fail = ({ status, msg = 'fail', data = {} } = {}) => {
|
|
||||||
ctx.status = status || 400 // 响应失败默认 400
|
|
||||||
ctx.body = {
|
|
||||||
status, // 失败默认 400
|
|
||||||
data,
|
|
||||||
msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 错误响应捕获
|
|
||||||
try {
|
|
||||||
await next() // 每个中间件都需等待next完成调用,不然会返回404给前端!!!
|
|
||||||
} catch (err) {
|
|
||||||
consola.error('中间件错误:', err)
|
|
||||||
if (err.status)
|
|
||||||
ctx.res.fail({ status: err.status, msg: err.message }) // 自己主动抛出的错误 throwError
|
|
||||||
else
|
|
||||||
ctx.app.emit('error', err, ctx) // 程序运行时的错误 main.js中监听
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = responseHandler
|
|
@ -1,13 +0,0 @@
|
|||||||
const router = require('../router')
|
|
||||||
|
|
||||||
// 路由中间件
|
|
||||||
const useRoutes = router.routes()
|
|
||||||
// 优化错误提示中间件
|
|
||||||
// 原先如果请求方法错误响应404
|
|
||||||
// 使用该中间件后请求方法错误会提示405 Method Not Allowed【get list ✔200 post /list ❌405】
|
|
||||||
const useAllowedMethods = router.allowedMethods()
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
useRoutes,
|
|
||||||
useAllowedMethods
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
const koaStatic = require('koa-static')
|
|
||||||
const { staticDir } = require('../config')
|
|
||||||
|
|
||||||
const useStatic = koaStatic(staticDir, {
|
|
||||||
maxage: 1000 * 60 * 60 * 24 * 30,
|
|
||||||
gzip: true,
|
|
||||||
setHeaders: (res, path) => {
|
|
||||||
if(path && path.endsWith('.html')) {
|
|
||||||
res.setHeader('Cache-Control', 'max-age=0')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = useStatic
|
|
@ -1,13 +0,0 @@
|
|||||||
const { apiPrefix } = require('../config')
|
|
||||||
const koaRouter = require('koa-router')
|
|
||||||
const router = new koaRouter({ prefix: apiPrefix })
|
|
||||||
|
|
||||||
const routeList = require('./routes')
|
|
||||||
|
|
||||||
// 统一注册路由
|
|
||||||
routeList.forEach(item => {
|
|
||||||
const { method, path, controller } = item
|
|
||||||
router[method](path, controller)
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = router
|
|
@ -1,139 +0,0 @@
|
|||||||
const { updateSSH, removeSSH, existSSH, getCommand } = require('../controller/ssh')
|
|
||||||
const { getHostList, saveHost, updateHost, removeHost, updateHostSort } = require('../controller/host')
|
|
||||||
const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controller/user')
|
|
||||||
const { getSupportEmailList, getUserEmailList, updateUserEmailList, removeUserEmail, pushEmail, getNotifyList, updateNotifyList } = require('../controller/notify')
|
|
||||||
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
|
||||||
|
|
||||||
const ssh = [
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/update-ssh',
|
|
||||||
controller: updateSSH
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/remove-ssh',
|
|
||||||
controller: removeSSH
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/exist-ssh',
|
|
||||||
controller: existSSH
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/command',
|
|
||||||
controller: getCommand
|
|
||||||
}
|
|
||||||
]
|
|
||||||
const host = [
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/host-list',
|
|
||||||
controller: getHostList
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/host-save',
|
|
||||||
controller: saveHost
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'put',
|
|
||||||
path: '/host-save',
|
|
||||||
controller: updateHost
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/host-remove',
|
|
||||||
controller: removeHost
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'put',
|
|
||||||
path: '/host-sort',
|
|
||||||
controller: updateHostSort
|
|
||||||
}
|
|
||||||
]
|
|
||||||
const user = [
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/get-pub-pem',
|
|
||||||
controller: getpublicKey
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/login',
|
|
||||||
controller: login
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'put',
|
|
||||||
path: '/pwd',
|
|
||||||
controller: updatePwd
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/get-login-record',
|
|
||||||
controller: getLoginRecord
|
|
||||||
}
|
|
||||||
]
|
|
||||||
const notify = [
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/support-email',
|
|
||||||
controller: getSupportEmailList
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/user-email',
|
|
||||||
controller: getUserEmailList
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/push-email',
|
|
||||||
controller: pushEmail
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/user-email',
|
|
||||||
controller: updateUserEmailList
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'delete',
|
|
||||||
path: '/user-email/:email',
|
|
||||||
controller: removeUserEmail
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/notify',
|
|
||||||
controller: getNotifyList
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'put',
|
|
||||||
path: '/notify',
|
|
||||||
controller: updateNotifyList
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const group = [
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/group',
|
|
||||||
controller: getGroupList
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/group',
|
|
||||||
controller: addGroupList
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'delete',
|
|
||||||
path: '/group/:id',
|
|
||||||
controller: removeGroup
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'put',
|
|
||||||
path: '/group/:id',
|
|
||||||
controller: updateGroupList
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
module.exports = [].concat(ssh, host, user, notify, group)
|
|
@ -1,29 +0,0 @@
|
|||||||
const schedule = require('node-schedule')
|
|
||||||
const { readHostList, sendEmailToConfList, formatTimestamp } = require('../utils')
|
|
||||||
|
|
||||||
const expiredNotifyJob = () => {
|
|
||||||
consola.info('=====开始检测服务器到期时间=====', new Date())
|
|
||||||
const hostList = readHostList()
|
|
||||||
for (const item of hostList) {
|
|
||||||
if(!item.expiredNotify) continue
|
|
||||||
const { host, name, expired, consoleUrl } = item
|
|
||||||
const restDay = Number(((expired - Date.now()) / (1000 * 60 * 60 * 24)).toFixed(1))
|
|
||||||
console.log(Date.now(), restDay)
|
|
||||||
let title = '服务器到期提醒'
|
|
||||||
let content = `别名: ${ name }<br/>IP: ${ host }<br/>到期时间:${ formatTimestamp(expired, 'week') }<br/>控制台: ${ consoleUrl || '未填写' }`
|
|
||||||
if(0 <= restDay && restDay <= 1) {
|
|
||||||
let temp = '有服务器将在一天后到期,请关注<br/>'
|
|
||||||
sendEmailToConfList(title, temp + content)
|
|
||||||
}else if(3 <= restDay && restDay < 4) {
|
|
||||||
let temp = '有服务器将在三天后到期,请关注<br/>'
|
|
||||||
sendEmailToConfList(title, temp + content)
|
|
||||||
}else if(7 <= restDay && restDay < 8) {
|
|
||||||
let temp = '有服务器将在七天后到期,请关注<br/>'
|
|
||||||
sendEmailToConfList(title, temp + content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
schedule.scheduleJob('0 0 12 1/1 * ?', expiredNotifyJob)
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
const offlineInspect = require('./offline-inspect')
|
|
||||||
const expiredNotify = require('./expired-notify')
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
offlineInspect()
|
|
||||||
expiredNotify()
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
const schedule = require('node-schedule')
|
|
||||||
const { clientPort } = require('../config')
|
|
||||||
const { readHostList, sendEmailToConfList, getNotifySwByType, formatTimestamp, isProd } = require('../utils')
|
|
||||||
const testConnectAsync = require('../utils/test-connect')
|
|
||||||
|
|
||||||
let sendNotifyRecord = new Map()
|
|
||||||
const offlineJob = () => {
|
|
||||||
let sw = getNotifySwByType('host_offline')
|
|
||||||
if(!sw) return
|
|
||||||
consola.info('=====开始检测服务器状态=====', new Date())
|
|
||||||
for (const item of readHostList()) {
|
|
||||||
const { host, name } = item
|
|
||||||
// consola.info('start inpect:', host, name )
|
|
||||||
testConnectAsync({
|
|
||||||
port: clientPort ,
|
|
||||||
host: `http://${ host }`,
|
|
||||||
timeout: 3000,
|
|
||||||
retryTimes: 20 // 尝试重连次数
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
// consola.success('测试连接成功:', host, name)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
consola.error('测试连接失败: ', host, name)
|
|
||||||
// 当前小时是否发送过通知
|
|
||||||
let curHourIsSend = sendNotifyRecord.has(host) && (sendNotifyRecord.get(host).sendTime === formatTimestamp(Date.now(), 'hour'))
|
|
||||||
if(curHourIsSend) return consola.info('当前小时已发送过通知: ', sendNotifyRecord.get(host).sendTime)
|
|
||||||
sendEmailToConfList('服务器离线提醒', `别名: ${ name }<br/>IP: ${ host }<br/>错误信息:${ error.message }`)
|
|
||||||
.then(() => {
|
|
||||||
sendNotifyRecord.set(host, { 'sendTime': formatTimestamp(Date.now(), 'hour') })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
if(!isProd()) return consola.info('本地开发不检测服务器离线状态')
|
|
||||||
schedule.scheduleJob('0 0/5 12 1/1 * ?', offlineJob)
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
const Koa = require('koa')
|
|
||||||
const compose = require('koa-compose') // 组合中间件,简化写法
|
|
||||||
const http = require('http')
|
|
||||||
const { clientPort } = require('./config')
|
|
||||||
const { httpPort } = require('./config')
|
|
||||||
const middlewares = require('./middlewares')
|
|
||||||
const wsMonitorOsInfo = require('./socket/monitor')
|
|
||||||
const wsTerminal = require('./socket/terminal')
|
|
||||||
const wsSftp = require('./socket/sftp')
|
|
||||||
const wsHostStatus = require('./socket/host-status')
|
|
||||||
const wsClientInfo = require('./socket/clients')
|
|
||||||
const { throwError } = require('./utils')
|
|
||||||
|
|
||||||
const httpServer = () => {
|
|
||||||
const app = new Koa()
|
|
||||||
const server = http.createServer(app.callback())
|
|
||||||
serverHandler(app, server)
|
|
||||||
// ws一直报跨域的错误:参照官方文档使用createServer API创建服务
|
|
||||||
server.listen(httpPort, () => {
|
|
||||||
consola.success(`Server(http) is running on: http://localhost:${ httpPort }`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientHttpServer = () => {
|
|
||||||
const app = new Koa()
|
|
||||||
const server = http.createServer(app.callback())
|
|
||||||
wsMonitorOsInfo(server) // 监控本机信息
|
|
||||||
server.listen(clientPort, () => {
|
|
||||||
consola.success(`Client(http) is running on: http://localhost:${ clientPort }`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 服务
|
|
||||||
function serverHandler(app, server) {
|
|
||||||
app.proxy = true // 用于nginx反代时获取真实客户端ip
|
|
||||||
wsTerminal(server) // 终端
|
|
||||||
wsSftp(server) // sftp
|
|
||||||
wsHostStatus(server) // 终端侧边栏host信息
|
|
||||||
wsClientInfo(server) // 客户端信息
|
|
||||||
app.context.throwError = throwError // 常用方法挂载全局ctx上
|
|
||||||
app.use(compose(middlewares))
|
|
||||||
// 捕获error.js模块抛出的服务错误
|
|
||||||
app.on('error', (err, ctx) => {
|
|
||||||
ctx.status = 500
|
|
||||||
ctx.body = {
|
|
||||||
status: ctx.status,
|
|
||||||
message: `Program Error:${ err.message }`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
httpServer,
|
|
||||||
clientHttpServer
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
const { Server: ServerIO } = require('socket.io')
|
|
||||||
const { io: ClientIO } = require('socket.io-client')
|
|
||||||
const { readHostList } = require('../utils')
|
|
||||||
const { clientPort } = require('../config')
|
|
||||||
const { verifyAuth } = require('../utils')
|
|
||||||
|
|
||||||
let clientSockets = {}, clientsData = {}
|
|
||||||
|
|
||||||
function getClientsInfo(socketId) {
|
|
||||||
let hostList = readHostList()
|
|
||||||
hostList
|
|
||||||
.map(({ host, name }) => {
|
|
||||||
let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, {
|
|
||||||
path: '/client/os-info',
|
|
||||||
forceNew: true,
|
|
||||||
timeout: 5000,
|
|
||||||
reconnectionDelay: 3000,
|
|
||||||
reconnectionAttempts: 100
|
|
||||||
})
|
|
||||||
// 将与客户端连接的socket实例保存起来,web端断开时关闭这些连接
|
|
||||||
clientSockets[socketId].push(clientSocket)
|
|
||||||
return {
|
|
||||||
host,
|
|
||||||
name,
|
|
||||||
clientSocket
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(({ host, name, clientSocket }) => {
|
|
||||||
clientSocket
|
|
||||||
.on('connect', () => {
|
|
||||||
consola.success('client connect success:', host, name)
|
|
||||||
clientSocket.on('client_data', (osData) => {
|
|
||||||
clientsData[host] = osData
|
|
||||||
})
|
|
||||||
clientSocket.on('client_error', (error) => {
|
|
||||||
clientsData[host] = error
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.on('connect_error', (error) => {
|
|
||||||
consola.error('client connect fail:', host, name, error.message)
|
|
||||||
clientsData[host] = null
|
|
||||||
})
|
|
||||||
.on('disconnect', () => {
|
|
||||||
consola.info('client connect disconnect:', host, name)
|
|
||||||
clientsData[host] = null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = (httpServer) => {
|
|
||||||
const serverIo = new ServerIO(httpServer, {
|
|
||||||
path: '/clients',
|
|
||||||
cors: {
|
|
||||||
origin: '*' // 需配置跨域
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
serverIo.on('connection', (socket) => {
|
|
||||||
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
|
||||||
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
|
||||||
socket.on('init_clients_data', ({ token }) => {
|
|
||||||
// 校验登录态
|
|
||||||
const { code, msg } = verifyAuth(token, clientIp)
|
|
||||||
if(code !== 1) {
|
|
||||||
socket.emit('token_verify_fail', msg || '鉴权失败')
|
|
||||||
socket.disconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 收集web端连接的id
|
|
||||||
clientSockets[socket.id] = []
|
|
||||||
consola.info('client连接socketId: ', socket.id, 'clients-socket已连接数: ', Object.keys(clientSockets).length)
|
|
||||||
|
|
||||||
// 获取客户端数据
|
|
||||||
getClientsInfo(socket.id)
|
|
||||||
|
|
||||||
// 立即推送一次
|
|
||||||
socket.emit('clients_data', clientsData)
|
|
||||||
|
|
||||||
// 向web端推送数据
|
|
||||||
let timer = null
|
|
||||||
timer = setInterval(() => {
|
|
||||||
socket.emit('clients_data', clientsData)
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
// 关闭连接
|
|
||||||
socket.on('disconnect', () => {
|
|
||||||
// 防止内存泄漏
|
|
||||||
if(timer) clearInterval(timer)
|
|
||||||
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
|
|
||||||
clientSockets[socket.id].forEach(socket => socket.close && socket.close())
|
|
||||||
delete clientSockets[socket.id]
|
|
||||||
consola.info('断开socketId: ', socket.id, 'clients-socket剩余连接数: ', Object.keys(clientSockets).length)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
const { Server: ServerIO } = require('socket.io')
|
|
||||||
const { io: ClientIO } = require('socket.io-client')
|
|
||||||
const { clientPort } = require('../config')
|
|
||||||
const { verifyAuth } = require('../utils')
|
|
||||||
|
|
||||||
let hostSockets = {}
|
|
||||||
|
|
||||||
function getHostInfo(serverSocket, host) {
|
|
||||||
let hostSocket = ClientIO(`http://${ host }:${ clientPort }`, {
|
|
||||||
path: '/client/os-info',
|
|
||||||
forceNew: false,
|
|
||||||
timeout: 5000,
|
|
||||||
reconnectionDelay: 3000,
|
|
||||||
reconnectionAttempts: 100
|
|
||||||
})
|
|
||||||
// 将与客户端连接的socket实例保存起来,web端断开时关闭与客户端的连接
|
|
||||||
hostSockets[serverSocket.id] = hostSocket
|
|
||||||
|
|
||||||
hostSocket
|
|
||||||
.on('connect', () => {
|
|
||||||
consola.success('host-status-socket连接成功:', host)
|
|
||||||
hostSocket.on('client_data', (data) => {
|
|
||||||
serverSocket.emit('host_data', data)
|
|
||||||
})
|
|
||||||
hostSocket.on('client_error', () => {
|
|
||||||
serverSocket.emit('host_data', null)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.on('connect_error', (error) => {
|
|
||||||
consola.error('host-status-socket连接[失败]:', host, error.message)
|
|
||||||
serverSocket.emit('host_data', null)
|
|
||||||
})
|
|
||||||
.on('disconnect', () => {
|
|
||||||
consola.info('host-status-socket连接[断开]:', host)
|
|
||||||
serverSocket.emit('host_data', null)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = (httpServer) => {
|
|
||||||
const serverIo = new ServerIO(httpServer, {
|
|
||||||
path: '/host-status',
|
|
||||||
cors: {
|
|
||||||
origin: '*' // 需配置跨域
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
serverIo.on('connection', (serverSocket) => {
|
|
||||||
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
|
||||||
let clientIp = serverSocket.handshake.headers['x-forwarded-for'] || serverSocket.handshake.address
|
|
||||||
serverSocket.on('init_host_data', ({ token, host }) => {
|
|
||||||
// 校验登录态
|
|
||||||
const { code, msg } = verifyAuth(token, clientIp)
|
|
||||||
if(code !== 1) {
|
|
||||||
serverSocket.emit('token_verify_fail', msg || '鉴权失败')
|
|
||||||
serverSocket.disconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取客户端数据
|
|
||||||
getHostInfo(serverSocket, host)
|
|
||||||
|
|
||||||
consola.info('host-status-socket连接socketId: ', serverSocket.id, 'host-status-socket已连接数: ', Object.keys(hostSockets).length)
|
|
||||||
|
|
||||||
// 关闭连接
|
|
||||||
serverSocket.on('disconnect', () => {
|
|
||||||
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
|
|
||||||
let socket = hostSockets[serverSocket.id]
|
|
||||||
socket.close && socket.close()
|
|
||||||
delete hostSockets[serverSocket.id]
|
|
||||||
consola.info('host-status-socket剩余连接数: ', Object.keys(hostSockets).length)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
const { Server } = require('socket.io')
|
|
||||||
const schedule = require('node-schedule')
|
|
||||||
const axios = require('axios')
|
|
||||||
let getOsData = require('../utils/os-data')
|
|
||||||
const consola = require('consola')
|
|
||||||
|
|
||||||
let serverSockets = {}, ipInfo = {}, osData = {}
|
|
||||||
|
|
||||||
async function getIpInfo() {
|
|
||||||
try {
|
|
||||||
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
|
|
||||||
consola.success('getIpInfo Success: ', new Date())
|
|
||||||
ipInfo = data
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('getIpInfo Error: ', new Date(), error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ipSchedule() {
|
|
||||||
let rule1 = new schedule.RecurrenceRule()
|
|
||||||
rule1.second = [0, 30]
|
|
||||||
schedule.scheduleJob(rule1, () => {
|
|
||||||
let { query, country, city } = ipInfo || {}
|
|
||||||
if(query && country && city) return
|
|
||||||
consola.success('Task: start getIpInfo', new Date())
|
|
||||||
getIpInfo()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
|
|
||||||
let rule2 = new schedule.RecurrenceRule()
|
|
||||||
rule2.hour = 2
|
|
||||||
rule2.minute = 0
|
|
||||||
rule2.second = 0
|
|
||||||
schedule.scheduleJob(rule2, () => {
|
|
||||||
consola.info('Task: refresh ip info', new Date())
|
|
||||||
getIpInfo()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ipSchedule()
|
|
||||||
|
|
||||||
module.exports = (httpServer) => {
|
|
||||||
const serverIo = new Server(httpServer, {
|
|
||||||
path: '/client/os-info',
|
|
||||||
cors: {
|
|
||||||
origin: '*'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
serverIo.on('connection', (socket) => {
|
|
||||||
// 存储对应websocket连接的定时器
|
|
||||||
serverSockets[socket.id] = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
osData = await getOsData()
|
|
||||||
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('客户端错误:', error)
|
|
||||||
socket && socket.emit('client_error', { error })
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
|
||||||
// 断开时清楚对应的websocket连接
|
|
||||||
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
|
|
||||||
delete serverSockets[socket.id]
|
|
||||||
socket.close && socket.close()
|
|
||||||
socket = null
|
|
||||||
// console.log('断开socketId: ', socket.id, '剩余链接数: ', Object.keys(serverSockets).length)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,256 +0,0 @@
|
|||||||
const { Server } = require('socket.io')
|
|
||||||
const SFTPClient = require('ssh2-sftp-client')
|
|
||||||
const rawPath = require('path')
|
|
||||||
const fs = require('fs')
|
|
||||||
const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = require('../utils')
|
|
||||||
const { sftpCacheDir } = require('../config')
|
|
||||||
const CryptoJS = require('crypto-js')
|
|
||||||
|
|
||||||
function clearDir(path, rmSelf = false) {
|
|
||||||
let files = []
|
|
||||||
if(!fs.existsSync(path)) return consola.info('clearDir: 目标文件夹不存在')
|
|
||||||
files = fs.readdirSync(path)
|
|
||||||
files.forEach((file) => {
|
|
||||||
let curPath = path + '/' + file
|
|
||||||
if(fs.statSync(curPath).isDirectory()){
|
|
||||||
clearDir(curPath) //递归删除文件夹
|
|
||||||
fs.rmdirSync(curPath) // 删除文件夹
|
|
||||||
} else {
|
|
||||||
fs.unlinkSync(curPath) //删除文件
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if(rmSelf) fs.rmdirSync(path)
|
|
||||||
consola.success('clearDir: 已清空缓存文件')
|
|
||||||
}
|
|
||||||
const pipeStream = (path, writeStream) => {
|
|
||||||
// console.log('path', path)
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const readStream = fs.createReadStream(path)
|
|
||||||
readStream.on('end', () => {
|
|
||||||
fs.unlinkSync(path) // 删除已写入切片
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
readStream.pipe(writeStream)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function listenInput(sftpClient, socket) {
|
|
||||||
socket.on('open_dir', async (path) => {
|
|
||||||
const exists = await sftpClient.exists(path)
|
|
||||||
if(!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问')
|
|
||||||
try {
|
|
||||||
let dirLs = await sftpClient.list(path)
|
|
||||||
socket.emit('dir_ls', dirLs)
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('open_dir Error', error.message)
|
|
||||||
socket.emit('sftp_error', error.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
socket.on('rm_dir', async (path) => {
|
|
||||||
const exists = await sftpClient.exists(path)
|
|
||||||
if(!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问')
|
|
||||||
try {
|
|
||||||
let res = await sftpClient.rmdir(path, true) // 递归删除
|
|
||||||
socket.emit('rm_success', res)
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('rm_dir Error', error.message)
|
|
||||||
socket.emit('sftp_error', error.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
socket.on('rm_file', async (path) => {
|
|
||||||
const exists = await sftpClient.exists(path)
|
|
||||||
if(!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问')
|
|
||||||
try {
|
|
||||||
let res = await sftpClient.delete(path)
|
|
||||||
socket.emit('rm_success', res)
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('rm_file Error', error.message)
|
|
||||||
socket.emit('sftp_error', error.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// socket.on('down_dir', async (path) => {
|
|
||||||
// const exists = await sftpClient.exists(path)
|
|
||||||
// if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问')
|
|
||||||
// let res = await sftpClient.downloadDir(path, sftpCacheDir)
|
|
||||||
// socket.emit('down_dir_success', res)
|
|
||||||
// })
|
|
||||||
socket.on('down_file', async ({ path, name, size, target = 'down' }) => {
|
|
||||||
// target: down or preview
|
|
||||||
const exists = await sftpClient.exists(path)
|
|
||||||
if(!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问')
|
|
||||||
try {
|
|
||||||
const localPath = rawPath.join(sftpCacheDir, name)
|
|
||||||
let timer = null
|
|
||||||
let res = await sftpClient.fastGet(path, localPath, {
|
|
||||||
step: step => {
|
|
||||||
if(timer) return
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
const percent = Math.ceil((step / size) * 100) // 下载进度为服务器下载到服务端的进度,前端无需*2
|
|
||||||
console.log(`从服务器下载进度:${ percent }%`)
|
|
||||||
socket.emit('down_file_progress', percent)
|
|
||||||
timer = null
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
consola.success('sftp下载成功: ', res)
|
|
||||||
let buffer = fs.readFileSync(localPath)
|
|
||||||
let data = { buffer, name }
|
|
||||||
switch(target) {
|
|
||||||
case 'down':
|
|
||||||
socket.emit('down_file_success', data)
|
|
||||||
break
|
|
||||||
case 'preview':
|
|
||||||
socket.emit('preview_file_success', data)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fs.unlinkSync(localPath) //删除文件
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('down_file Error', error.message)
|
|
||||||
socket.emit('sftp_error', error.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
socket.on('up_file', async ({ targetPath, fullPath, name, file }) => {
|
|
||||||
console.log({ targetPath, fullPath, name, file })
|
|
||||||
const exists = await sftpClient.exists(targetPath)
|
|
||||||
if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问')
|
|
||||||
try {
|
|
||||||
const localPath = rawPath.join(sftpCacheDir, name)
|
|
||||||
fs.writeFileSync(localPath, file)
|
|
||||||
let res = await sftpClient.fastPut(localPath, fullPath)
|
|
||||||
consola.success('sftp上传成功: ', res)
|
|
||||||
socket.emit('up_file_success', res)
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('up_file Error', error.message)
|
|
||||||
socket.emit('sftp_error', error.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 分片上传 */
|
|
||||||
// 1. 创建本地缓存文件夹
|
|
||||||
let md5List = []
|
|
||||||
socket.on('create_cache_dir', async ({ targetPath, name }) => {
|
|
||||||
// console.log({ targetPath, name })
|
|
||||||
const exists = await sftpClient.exists(targetPath)
|
|
||||||
if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问')
|
|
||||||
md5List = []
|
|
||||||
const localPath = rawPath.join(sftpCacheDir, name)
|
|
||||||
if(fs.existsSync(localPath)) clearDir(localPath) // 清空目录
|
|
||||||
fs.mkdirSync(localPath, { recursive: true })
|
|
||||||
console.log('================create_cache_success================')
|
|
||||||
socket.emit('create_cache_success')
|
|
||||||
})
|
|
||||||
socket.on('up_file_slice', async ({ name, sliceFile, fileIndex }) => {
|
|
||||||
// console.log('up_file_slice:', fileIndex, name)
|
|
||||||
try {
|
|
||||||
let md5 = `${ fileIndex }.${ CryptoJS.MD5(fileIndex+name).toString() }`
|
|
||||||
const localPath = rawPath.join(sftpCacheDir, name, md5)
|
|
||||||
md5List.push(localPath)
|
|
||||||
fs.writeFileSync(localPath, sliceFile)
|
|
||||||
socket.emit('up_file_slice_success', md5)
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('up_file_slice Error', error.message)
|
|
||||||
socket.emit('up_file_slice_fail', error.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
socket.on('up_file_slice_over', async ({ name, fullPath, range, size }) => {
|
|
||||||
const resultDirPath = rawPath.join(sftpCacheDir, name)
|
|
||||||
const resultFilePath = rawPath.join(sftpCacheDir, name, name)
|
|
||||||
try {
|
|
||||||
console.log('md5List: ', md5List)
|
|
||||||
const arr = md5List.map((chunkFilePath, index) => {
|
|
||||||
return pipeStream(
|
|
||||||
chunkFilePath,
|
|
||||||
// 指定位置创建可写流
|
|
||||||
fs.createWriteStream(resultFilePath, {
|
|
||||||
start: index * range,
|
|
||||||
end: (index + 1) * range
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
md5List = []
|
|
||||||
await Promise.all(arr)
|
|
||||||
let timer = null
|
|
||||||
let res = await sftpClient.fastPut(resultFilePath, fullPath, {
|
|
||||||
step: step => {
|
|
||||||
if(timer) return
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
const percent = Math.ceil((step / size) * 100)
|
|
||||||
console.log(`上传服务器进度:${ percent }%`)
|
|
||||||
socket.emit('up_file_progress', percent)
|
|
||||||
timer = null
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
consola.success('sftp上传成功: ', res)
|
|
||||||
socket.emit('up_file_success', res)
|
|
||||||
clearDir(resultDirPath, true) // 传服务器后移除文件夹及其文件
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('sftp上传失败: ', error.message)
|
|
||||||
socket.emit('up_file_fail', error.message)
|
|
||||||
clearDir(resultDirPath, true) // 传服务器后移除文件夹及其文件
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = (httpServer) => {
|
|
||||||
const serverIo = new Server(httpServer, {
|
|
||||||
path: '/sftp',
|
|
||||||
cors: {
|
|
||||||
origin: '*'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
serverIo.on('connection', (socket) => {
|
|
||||||
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
|
||||||
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
|
||||||
let sftpClient = new SFTPClient()
|
|
||||||
consola.success('terminal websocket 已连接')
|
|
||||||
|
|
||||||
socket.on('create', ({ host: ip, token }) => {
|
|
||||||
const { code } = verifyAuth(token, clientIp)
|
|
||||||
if(code !== 1) {
|
|
||||||
socket.emit('token_verify_fail')
|
|
||||||
socket.disconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const sshRecord = readSSHRecord()
|
|
||||||
let loginInfo = sshRecord.find(item => item.host === ip)
|
|
||||||
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
|
|
||||||
let { type, host, port, username, randomKey } = loginInfo
|
|
||||||
// 解密放到try里面,防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
|
|
||||||
randomKey = AESDecrypt(randomKey) // 先对称解密key
|
|
||||||
randomKey = RSADecrypt(randomKey) // 再非对称解密key
|
|
||||||
loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥
|
|
||||||
consola.info('准备连接Sftp:', host)
|
|
||||||
const authInfo = { host, port, username, [type]: loginInfo[type] }
|
|
||||||
sftpClient.connect(authInfo)
|
|
||||||
.then(() => {
|
|
||||||
consola.success('连接Sftp成功:', host)
|
|
||||||
return sftpClient.list('/')
|
|
||||||
})
|
|
||||||
.then((rootLs) => {
|
|
||||||
// 普通文件-、目录文件d、链接文件l
|
|
||||||
socket.emit('root_ls', rootLs) // 先返回根目录
|
|
||||||
listenInput(sftpClient, socket) // 监听前端请求
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
consola.error('创建Sftp失败:', err.message)
|
|
||||||
socket.emit('create_fail', err.message)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on('disconnect', async () => {
|
|
||||||
sftpClient.end()
|
|
||||||
.then(() => {
|
|
||||||
consola.info('sftp连接断开')
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
consola.info('sftp断开连接失败:', error.message)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
sftpClient = null
|
|
||||||
const cacheDir = rawPath.join(sftpCacheDir)
|
|
||||||
clearDir(cacheDir)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
const { Server } = require('socket.io')
|
|
||||||
const { Client: SSHClient } = require('ssh2')
|
|
||||||
const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = require('../utils')
|
|
||||||
|
|
||||||
function createTerminal(socket, sshClient) {
|
|
||||||
sshClient.shell({ term: 'xterm-color' }, (err, stream) => {
|
|
||||||
if (err) return socket.emit('output', err.toString())
|
|
||||||
// 终端输出
|
|
||||||
stream
|
|
||||||
.on('data', (data) => {
|
|
||||||
socket.emit('output', data.toString())
|
|
||||||
})
|
|
||||||
.on('close', () => {
|
|
||||||
consola.info('关闭终端')
|
|
||||||
sshClient.end()
|
|
||||||
})
|
|
||||||
// web端输入
|
|
||||||
socket.on('input', key => {
|
|
||||||
if(sshClient._sock.writable === false) return consola.info('终端连接已关闭')
|
|
||||||
stream.write(key)
|
|
||||||
})
|
|
||||||
socket.emit('connect_terminal') // 已连接终端,web端可以执行指令了
|
|
||||||
|
|
||||||
// 监听按键重置终端大小
|
|
||||||
socket.on('resize', ({ rows, cols }) => {
|
|
||||||
consola.info('更改tty终端行&列: ', { rows, cols })
|
|
||||||
stream.setWindow(rows, cols)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = (httpServer) => {
|
|
||||||
const serverIo = new Server(httpServer, {
|
|
||||||
path: '/terminal',
|
|
||||||
cors: {
|
|
||||||
origin: '*' // 'http://localhost:8080'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
serverIo.on('connection', (socket) => {
|
|
||||||
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
|
||||||
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
|
||||||
let sshClient = new SSHClient()
|
|
||||||
consola.success('terminal websocket 已连接')
|
|
||||||
|
|
||||||
socket.on('create', ({ host: ip, token }) => {
|
|
||||||
const { code } = verifyAuth(token, clientIp)
|
|
||||||
if(code !== 1) {
|
|
||||||
socket.emit('token_verify_fail')
|
|
||||||
socket.disconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const sshRecord = readSSHRecord()
|
|
||||||
let loginInfo = sshRecord.find(item => item.host === ip)
|
|
||||||
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
|
|
||||||
let { type, host, port, username, randomKey } = loginInfo
|
|
||||||
try {
|
|
||||||
// 解密放到try里面,防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
|
|
||||||
randomKey = AESDecrypt(randomKey) // 先对称解密key
|
|
||||||
randomKey = RSADecrypt(randomKey) // 再非对称解密key
|
|
||||||
loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥
|
|
||||||
consola.info('准备连接终端:', host)
|
|
||||||
const authInfo = { host, port, username, [type]: loginInfo[type] }
|
|
||||||
sshClient
|
|
||||||
.on('ready', () => {
|
|
||||||
consola.success('已连接到终端:', host)
|
|
||||||
socket.emit('connect_success', `已连接到终端:${ host }`)
|
|
||||||
createTerminal(socket, sshClient)
|
|
||||||
})
|
|
||||||
.on('error', (err) => {
|
|
||||||
consola.error('连接终端失败:', err.level)
|
|
||||||
socket.emit('connect_fail', err.message)
|
|
||||||
})
|
|
||||||
.connect(authInfo)
|
|
||||||
} catch (err) {
|
|
||||||
consola.error('创建终端失败:', err.message)
|
|
||||||
socket.emit('create_fail', err.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on('disconnect', (reason) => {
|
|
||||||
consola.info('终端连接断开:', reason)
|
|
||||||
sshClient.end()
|
|
||||||
sshClient.destroy()
|
|
||||||
sshClient = null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 135 KiB |
Binary file not shown.
Before Width: | Height: | Size: 27 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
@ -1 +0,0 @@
|
|||||||
<html lang="zh"> <head> <meta charset="UTF-8" /> <link rel="icon" href="/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=0,user-scalable=yes"> <title>EasyNode</title> <script src="//at.alicdn.com/t/c/font_3309550_flid0kihu36.js"></script> <script type="module" crossorigin src="/assets/index.be6b9da9.js"></script> <link rel="stylesheet" href="/assets/index.de24ebdf.css"> </head> <body> <div id="app"></div> <script> var _hmt = _hmt || []; (function() { var hm = document.createElement("script"); hm.src = "https://hm.baidu.com/hm.js?9cd0d4e4da3a7f1d4f6e4aaaa0ce8f25"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); })(); </script> </body> </html>
|
|
@ -1,38 +0,0 @@
|
|||||||
# host-list.json
|
|
||||||
|
|
||||||
> 存储服务器基本信息
|
|
||||||
|
|
||||||
# key.json
|
|
||||||
|
|
||||||
> 用于加密的密钥相关
|
|
||||||
|
|
||||||
# ssh-record.json
|
|
||||||
|
|
||||||
> ssh密钥记录(加密存储)
|
|
||||||
|
|
||||||
# email.json
|
|
||||||
|
|
||||||
> 邮件配置
|
|
||||||
|
|
||||||
- port: 587 --> secure: false
|
|
||||||
```json
|
|
||||||
// Gmail调试不通过, 暂缓
|
|
||||||
{
|
|
||||||
"name": "Google邮箱",
|
|
||||||
"target": "google",
|
|
||||||
"host": "smtp.gmail.com",
|
|
||||||
"port": 465,
|
|
||||||
"secure": true,
|
|
||||||
"tls": {
|
|
||||||
"rejectUnauthorized": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# notify.json
|
|
||||||
|
|
||||||
> 通知配置
|
|
||||||
|
|
||||||
# group.json
|
|
||||||
|
|
||||||
> 服务器分组配置
|
|
@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"support": [
|
|
||||||
{
|
|
||||||
"name": "QQ邮箱",
|
|
||||||
"target": "qq",
|
|
||||||
"host": "smtp.qq.com",
|
|
||||||
"port": 465,
|
|
||||||
"secure": true,
|
|
||||||
"tls": {
|
|
||||||
"rejectUnauthorized": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "网易126",
|
|
||||||
"target": "wangyi126",
|
|
||||||
"host": "smtp.126.com",
|
|
||||||
"port": 465,
|
|
||||||
"secure": true,
|
|
||||||
"tls": {
|
|
||||||
"rejectUnauthorized": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "网易163",
|
|
||||||
"target": "wangyi163",
|
|
||||||
"host": "smtp.163.com",
|
|
||||||
"port": 465,
|
|
||||||
"secure": true,
|
|
||||||
"tls": {
|
|
||||||
"rejectUnauthorized": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"user": [
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "default",
|
|
||||||
"name": "默认分组",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
@ -1 +0,0 @@
|
|||||||
[]
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"pwd": "admin",
|
|
||||||
"commonKey": "",
|
|
||||||
"publicKey": "",
|
|
||||||
"privateKey": ""
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"type": "login",
|
|
||||||
"desc": "登录面板提醒",
|
|
||||||
"sw": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "err_login",
|
|
||||||
"desc": "登录错误提醒(连续5次)",
|
|
||||||
"sw": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "updatePwd",
|
|
||||||
"desc": "修改密码提醒",
|
|
||||||
"sw": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "host_offline",
|
|
||||||
"desc": "客户端离线提醒(每小时最多发送一次提醒)",
|
|
||||||
"sw": true
|
|
||||||
}
|
|
||||||
]
|
|
@ -1 +0,0 @@
|
|||||||
[]
|
|
@ -1,23 +0,0 @@
|
|||||||
<!DOCTYPE html
|
|
||||||
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
||||||
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body style="margin: 0; padding: 0;text-align: center;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<h3 style="font-size: 20px;color: #5992D3;padding:0 0 0 40px;">
|
|
||||||
${ content }
|
|
||||||
</h3>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,26 +0,0 @@
|
|||||||
module.exports = (content) => {
|
|
||||||
return `<!DOCTYPE html
|
|
||||||
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
||||||
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body style="margin: 0; padding: 0;text-align: center;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<h3 style="font-size: 18px;color: #5992D3;padding:0 0 0 10px;">
|
|
||||||
${ content }
|
|
||||||
</h3>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
const nodemailer = require('nodemailer')
|
|
||||||
const { readSupportEmailList, readUserEmailList } = require('./storage')
|
|
||||||
const commonTemp = require('../template/commonTemp')
|
|
||||||
|
|
||||||
const emailCode = {
|
|
||||||
SUCCESS: 0,
|
|
||||||
FAIL: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
const emailTransporter = async (params = {}) => {
|
|
||||||
let { toEmail, title, html } = params
|
|
||||||
try {
|
|
||||||
if(!toEmail) throw Error('missing params: toEmail')
|
|
||||||
let userEmail = readUserEmailList().find(({ auth }) => auth.user === toEmail)
|
|
||||||
if(!userEmail) throw Error(`${ toEmail } 不存在已保存的配置文件中, 请移除后重新添加`)
|
|
||||||
let { target } = userEmail
|
|
||||||
let emailServerConf = readSupportEmailList().find((item) => item.target === target)
|
|
||||||
if(!emailServerConf) throw Error(`邮箱类型不支持:${ target }`)
|
|
||||||
const timeout = 1000*6
|
|
||||||
let options = Object.assign({}, userEmail, emailServerConf, { greetingTimeout: timeout, connectionTimeout: timeout })
|
|
||||||
let transporter = nodemailer.createTransport(options)
|
|
||||||
let info = await transporter.sendMail({
|
|
||||||
from: userEmail.auth.user, // sender address
|
|
||||||
to: userEmail.auth.user, // list of receivers
|
|
||||||
subject: `EasyNode: ${ title }`,
|
|
||||||
html
|
|
||||||
})
|
|
||||||
// consola.success('email发送成功:', info.accepted)
|
|
||||||
return { code: emailCode.SUCCESS, msg: `send successful:${ info.accepted }` }
|
|
||||||
} catch(error) {
|
|
||||||
// consola.error(`email发送失败(${ toEmail }):`, error.message || error)
|
|
||||||
return { code: emailCode.FAIL, msg: error }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendEmailToConfList = (title, content) => {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
return new Promise(async (res, rej) => {
|
|
||||||
let emailList = readUserEmailList()
|
|
||||||
if(Array.isArray(emailList) && emailList.length >= 1) {
|
|
||||||
for (const item of emailList) {
|
|
||||||
const toEmail = item.auth.user
|
|
||||||
await emailTransporter({ toEmail, title, html: commonTemp(content) })
|
|
||||||
.then(({ code }) => {
|
|
||||||
if(code === 0) {
|
|
||||||
consola.success('已发送邮件通知: ', toEmail, title)
|
|
||||||
return res({ code: emailCode.SUCCESS })
|
|
||||||
}
|
|
||||||
consola.error('邮件通知发送失败: ', toEmail, title)
|
|
||||||
return rej({ code: emailCode.FAIL })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
emailTransporter,
|
|
||||||
sendEmailToConfList
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
const CryptoJS = require('crypto-js')
|
|
||||||
const rawCrypto = require('crypto')
|
|
||||||
const NodeRSA = require('node-rsa')
|
|
||||||
const { readKey } = require('./storage.js')
|
|
||||||
|
|
||||||
// rsa非对称 私钥解密
|
|
||||||
const RSADecrypt = (ciphertext) => {
|
|
||||||
if(!ciphertext) return
|
|
||||||
let { privateKey } = readKey()
|
|
||||||
privateKey = AESDecrypt(privateKey) // 先解密私钥
|
|
||||||
const rsakey = new NodeRSA(privateKey)
|
|
||||||
rsakey.setOptions({ encryptionScheme: 'pkcs1' }) // Must Set It When Frontend Use jsencrypt
|
|
||||||
const plaintext = rsakey.decrypt(ciphertext, 'utf8')
|
|
||||||
return plaintext
|
|
||||||
}
|
|
||||||
|
|
||||||
// aes对称 加密(default commonKey)
|
|
||||||
const AESEncrypt = (text, key) => {
|
|
||||||
if(!text) return
|
|
||||||
let { commonKey } = readKey()
|
|
||||||
let ciphertext = CryptoJS.AES.encrypt(text, key || commonKey).toString()
|
|
||||||
return ciphertext
|
|
||||||
}
|
|
||||||
|
|
||||||
// aes对称 解密(default commonKey)
|
|
||||||
const AESDecrypt = (ciphertext, key) => {
|
|
||||||
if(!ciphertext) return
|
|
||||||
let { commonKey } = readKey()
|
|
||||||
let bytes = CryptoJS.AES.decrypt(ciphertext, key || commonKey)
|
|
||||||
let originalText = bytes.toString(CryptoJS.enc.Utf8)
|
|
||||||
return originalText
|
|
||||||
}
|
|
||||||
|
|
||||||
// sha1 加密(不可逆)
|
|
||||||
const SHA1Encrypt = (clearText) => {
|
|
||||||
return rawCrypto.createHash('sha1').update(clearText).digest('hex')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
RSADecrypt,
|
|
||||||
AESEncrypt,
|
|
||||||
AESDecrypt,
|
|
||||||
SHA1Encrypt
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
const {
|
|
||||||
readSSHRecord,
|
|
||||||
writeSSHRecord,
|
|
||||||
readHostList,
|
|
||||||
writeHostList,
|
|
||||||
readKey,
|
|
||||||
writeKey,
|
|
||||||
readSupportEmailList,
|
|
||||||
readUserEmailList,
|
|
||||||
writeUserEmailList,
|
|
||||||
readNotifyList,
|
|
||||||
getNotifySwByType,
|
|
||||||
writeNotifyList,
|
|
||||||
readGroupList,
|
|
||||||
writeGroupList } = require('./storage')
|
|
||||||
const { RSADecrypt, AESEncrypt, AESDecrypt, SHA1Encrypt } = require('./encrypt')
|
|
||||||
const { verifyAuth, isProd } = require('./verify-auth')
|
|
||||||
const { getNetIPInfo, throwError, isIP, randomStr, getUTCDate, formatTimestamp } = require('./tools')
|
|
||||||
const { emailTransporter, sendEmailToConfList } = require('./email')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getNetIPInfo,
|
|
||||||
throwError,
|
|
||||||
isIP,
|
|
||||||
randomStr,
|
|
||||||
getUTCDate,
|
|
||||||
formatTimestamp,
|
|
||||||
verifyAuth,
|
|
||||||
isProd,
|
|
||||||
RSADecrypt,
|
|
||||||
AESEncrypt,
|
|
||||||
AESDecrypt,
|
|
||||||
SHA1Encrypt,
|
|
||||||
readSSHRecord,
|
|
||||||
writeSSHRecord,
|
|
||||||
readHostList,
|
|
||||||
writeHostList,
|
|
||||||
readKey,
|
|
||||||
writeKey,
|
|
||||||
readSupportEmailList,
|
|
||||||
readUserEmailList,
|
|
||||||
writeUserEmailList,
|
|
||||||
emailTransporter,
|
|
||||||
sendEmailToConfList,
|
|
||||||
readNotifyList,
|
|
||||||
getNotifySwByType,
|
|
||||||
writeNotifyList,
|
|
||||||
readGroupList,
|
|
||||||
writeGroupList
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
const osu = require('node-os-utils')
|
|
||||||
const os = require('os')
|
|
||||||
|
|
||||||
let cpu = osu.cpu
|
|
||||||
let mem = osu.mem
|
|
||||||
let drive = osu.drive
|
|
||||||
let netstat = osu.netstat
|
|
||||||
let osuOs = osu.os
|
|
||||||
let users = osu.users
|
|
||||||
|
|
||||||
async function cpuInfo() {
|
|
||||||
let cpuUsage = await cpu.usage(200)
|
|
||||||
let cpuCount = cpu.count()
|
|
||||||
let cpuModel = cpu.model()
|
|
||||||
return {
|
|
||||||
cpuUsage,
|
|
||||||
cpuCount,
|
|
||||||
cpuModel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function memInfo() {
|
|
||||||
let memInfo = await mem.info()
|
|
||||||
return {
|
|
||||||
...memInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function driveInfo() {
|
|
||||||
let driveInfo = {}
|
|
||||||
try {
|
|
||||||
driveInfo = await drive.info()
|
|
||||||
} catch {
|
|
||||||
// console.log(driveInfo)
|
|
||||||
}
|
|
||||||
return driveInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
async function netstatInfo() {
|
|
||||||
let netstatInfo = await netstat.inOut()
|
|
||||||
return netstatInfo === 'not supported' ? {} : netstatInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
async function osInfo() {
|
|
||||||
let type = os.type()
|
|
||||||
let platform = os.platform()
|
|
||||||
let release = os.release()
|
|
||||||
let uptime = osuOs.uptime()
|
|
||||||
let ip = osuOs.ip()
|
|
||||||
let hostname = osuOs.hostname()
|
|
||||||
let arch = osuOs.arch()
|
|
||||||
return {
|
|
||||||
type,
|
|
||||||
platform,
|
|
||||||
release,
|
|
||||||
ip,
|
|
||||||
hostname,
|
|
||||||
arch,
|
|
||||||
uptime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openedCount() {
|
|
||||||
let openedCount = await users.openedCount()
|
|
||||||
return openedCount === 'not supported' ? 0 : openedCount
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async () => {
|
|
||||||
let data = {}
|
|
||||||
try {
|
|
||||||
data = {
|
|
||||||
cpuInfo: await cpuInfo(),
|
|
||||||
memInfo: await memInfo(),
|
|
||||||
driveInfo: await driveInfo(),
|
|
||||||
netstatInfo: await netstatInfo(),
|
|
||||||
osInfo: await osInfo(),
|
|
||||||
openedCount: await openedCount()
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
} catch(err){
|
|
||||||
console.error('获取系统信息出错:', err)
|
|
||||||
return err.toString()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,139 +0,0 @@
|
|||||||
const fs = require('fs')
|
|
||||||
const { sshRecordPath, hostListPath, keyPath, emailPath, notifyPath, groupPath } = require('../config')
|
|
||||||
|
|
||||||
const readSSHRecord = () => {
|
|
||||||
let list
|
|
||||||
try {
|
|
||||||
list = JSON.parse(fs.readFileSync(sshRecordPath, 'utf8'))
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('读取ssh-record错误, 即将重置ssh列表: ', error)
|
|
||||||
writeSSHRecord([])
|
|
||||||
}
|
|
||||||
return list || []
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeSSHRecord = (record = []) => {
|
|
||||||
fs.writeFileSync(sshRecordPath, JSON.stringify(record, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
const readHostList = () => {
|
|
||||||
let list
|
|
||||||
try {
|
|
||||||
list = JSON.parse(fs.readFileSync(hostListPath, 'utf8'))
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('读取host-list错误, 即将重置host列表: ', error)
|
|
||||||
writeHostList([])
|
|
||||||
}
|
|
||||||
return list || []
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeHostList = (record = []) => {
|
|
||||||
fs.writeFileSync(hostListPath, JSON.stringify(record, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
const readKey = () => {
|
|
||||||
let keyObj = JSON.parse(fs.readFileSync(keyPath, 'utf8'))
|
|
||||||
return keyObj
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeKey = (keyObj = {}) => {
|
|
||||||
fs.writeFileSync(keyPath, JSON.stringify(keyObj, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
const readEmailJson = () => {
|
|
||||||
let emailJson = {}
|
|
||||||
try {
|
|
||||||
emailJson = JSON.parse(fs.readFileSync(emailPath, 'utf8'))
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('读取email.json错误: ', error)
|
|
||||||
}
|
|
||||||
return emailJson
|
|
||||||
}
|
|
||||||
|
|
||||||
const readSupportEmailList = () => {
|
|
||||||
let supportEmailList = []
|
|
||||||
try {
|
|
||||||
supportEmailList = readEmailJson().support
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('读取email support错误: ', error)
|
|
||||||
}
|
|
||||||
return supportEmailList
|
|
||||||
}
|
|
||||||
|
|
||||||
const readUserEmailList = () => {
|
|
||||||
let configEmailList = []
|
|
||||||
try {
|
|
||||||
configEmailList = readEmailJson().user
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('读取email config错误: ', error)
|
|
||||||
}
|
|
||||||
return configEmailList
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeUserEmailList = (user) => {
|
|
||||||
let support = readSupportEmailList()
|
|
||||||
const emailJson = { support, user }
|
|
||||||
try {
|
|
||||||
fs.writeFileSync(emailPath, JSON.stringify(emailJson, null, 2))
|
|
||||||
return { code: 0 }
|
|
||||||
} catch (error) {
|
|
||||||
return { code: -1, msg: error.message || error }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const readNotifyList = () => {
|
|
||||||
let notifyList = []
|
|
||||||
try {
|
|
||||||
notifyList = JSON.parse(fs.readFileSync(notifyPath, 'utf8'))
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('读取notify list错误: ', error)
|
|
||||||
}
|
|
||||||
return notifyList
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNotifySwByType = (type) => {
|
|
||||||
if(!type) throw Error('missing params: type')
|
|
||||||
try {
|
|
||||||
let { sw } = readNotifyList().find((item) => item.type === type)
|
|
||||||
return sw
|
|
||||||
} catch (error) {
|
|
||||||
consola.error(`通知类型[${ type }]不存在`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeNotifyList = (notifyList) => {
|
|
||||||
fs.writeFileSync(notifyPath, JSON.stringify(notifyList, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
const readGroupList = () => {
|
|
||||||
let list
|
|
||||||
try {
|
|
||||||
list = JSON.parse(fs.readFileSync(groupPath, 'utf8'))
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('读取group-list错误, 即将重置group列表: ', error)
|
|
||||||
writeSSHRecord([])
|
|
||||||
}
|
|
||||||
return list || []
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeGroupList = (list = []) => {
|
|
||||||
fs.writeFileSync(groupPath, JSON.stringify(list, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
readSSHRecord,
|
|
||||||
writeSSHRecord,
|
|
||||||
readHostList,
|
|
||||||
writeHostList,
|
|
||||||
readKey,
|
|
||||||
writeKey,
|
|
||||||
readSupportEmailList,
|
|
||||||
readUserEmailList,
|
|
||||||
writeUserEmailList,
|
|
||||||
readNotifyList,
|
|
||||||
getNotifySwByType,
|
|
||||||
writeNotifyList,
|
|
||||||
readGroupList,
|
|
||||||
writeGroupList
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
// based off of https://github.com/apaszke/tcp-ping
|
|
||||||
// rewritten with modern es6 syntax & promises
|
|
||||||
const { io: ClientIO } = require('socket.io-client')
|
|
||||||
|
|
||||||
const testConnectAsync = (options) => {
|
|
||||||
let connectTimes = 0
|
|
||||||
options = Object.assign({ retryTimes: 3, timeout: 5000, host: 'http://localhost', port: '80' }, options)
|
|
||||||
const { retryTimes, host, port, timeout } = options
|
|
||||||
// eslint-disable-next-line
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
while (connectTimes < retryTimes) {
|
|
||||||
try {
|
|
||||||
connectTimes++
|
|
||||||
await connect({ host, port, timeout })
|
|
||||||
break
|
|
||||||
} catch (error) {
|
|
||||||
// 重连次数达到限制仍未连接成功
|
|
||||||
if(connectTimes === retryTimes) {
|
|
||||||
reject({ message: error.message, host, port, connectTimes })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve({ status: 'connect_success', host, port, connectTimes })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const connect = (options) => {
|
|
||||||
const { host, port, timeout } = options
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let io = ClientIO(`${ host }:${ port }`, {
|
|
||||||
path: '/client/os-info',
|
|
||||||
forceNew: false,
|
|
||||||
timeout,
|
|
||||||
reconnection: false
|
|
||||||
})
|
|
||||||
.on('connect', () => {
|
|
||||||
resolve()
|
|
||||||
io.disconnect()
|
|
||||||
})
|
|
||||||
.on('connect_error', (error) => {
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = testConnectAsync
|
|
@ -1,144 +0,0 @@
|
|||||||
const axios = require('axios')
|
|
||||||
const request = axios.create({ timeout: 3000 })
|
|
||||||
|
|
||||||
// 为空时请求本地IP
|
|
||||||
const getNetIPInfo = async (searchIp = '') => {
|
|
||||||
searchIp = searchIp.replace(/::ffff:/g, '') || '' // fix: nginx反代
|
|
||||||
if(['::ffff:', '::1'].includes(searchIp)) searchIp = '127.0.0.1'
|
|
||||||
try {
|
|
||||||
let date = Date.now()
|
|
||||||
let ipUrls = [
|
|
||||||
// 45次/分钟&支持中文(无限制)
|
|
||||||
`http://ip-api.com/json/${ searchIp }?lang=zh-CN`,
|
|
||||||
// 10000次/月&支持中文(依赖IP计算调用次数)
|
|
||||||
`http://ipwho.is/${ searchIp }?lang=zh-CN`,
|
|
||||||
// 1500次/天(依赖密钥, 超出自行注册)
|
|
||||||
`https://api.ipdata.co/${ searchIp }?api-key=c6d4d04d5f11f2cd0839ee03c47c58621d74e361c945b5c1b4f668f3`,
|
|
||||||
// 50000/月(依赖密钥, 超出自行注册)
|
|
||||||
`https://ipinfo.io/${ searchIp }/json?token=41c48b54f6d78f`,
|
|
||||||
// 1000次/天(依赖密钥, 超出自行注册)
|
|
||||||
`https://api.ipgeolocation.io/ipgeo?apiKey=105fc2c7e8864ec08b98e1ad4e8cbc6d&ip=${ searchIp }`,
|
|
||||||
// 1000次/天(依赖IP计算调用次数)
|
|
||||||
`https://ipapi.co${ searchIp ? `/${ searchIp }` : '' }/json`,
|
|
||||||
// 国内IP138提供(无限制)
|
|
||||||
`https://sp1.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query=${ searchIp }&resource_id=5809`
|
|
||||||
]
|
|
||||||
let result = await Promise.allSettled(ipUrls.map(url => request.get(url)))
|
|
||||||
|
|
||||||
let [ipApi, ipwho, ipdata, ipinfo, ipgeolocation, ipApi01, ip138] = result
|
|
||||||
|
|
||||||
let searchResult = []
|
|
||||||
if(ipApi.status === 'fulfilled') {
|
|
||||||
let { query: ip, country, regionName, city } = ipApi.value?.data || {}
|
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ipwho.status === 'fulfilled') {
|
|
||||||
let { ip, country, region: regionName, city } = ipwho.value?.data || {}
|
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ipdata.status === 'fulfilled') {
|
|
||||||
let { ip, country_name: country, region: regionName, city } = ipdata.value?.data || {}
|
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ipinfo.status === 'fulfilled') {
|
|
||||||
let { ip, country, region: regionName, city } = ipinfo.value?.data || {}
|
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ipgeolocation.status === 'fulfilled') {
|
|
||||||
let { ip, country_name: country, state_prov: regionName, city } = ipgeolocation.value?.data || {}
|
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ipApi01.status === 'fulfilled') {
|
|
||||||
let { ip, country_name: country, region: regionName, city } = ipApi01.value?.data || {}
|
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ip138.status === 'fulfilled') {
|
|
||||||
let [res] = ip138.value?.data?.data || []
|
|
||||||
let { origip: ip, location: country, city = '', regionName = '' } = res || {}
|
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
|
||||||
}
|
|
||||||
// console.log(searchResult)
|
|
||||||
let validInfo = searchResult.find(item => Boolean(item.country))
|
|
||||||
consola.info('查询IP信息:', validInfo)
|
|
||||||
return validInfo || { ip: '获取IP信息API出错,请排查或更新API', country: '未知', city: '未知', date }
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('getIpInfo Error: ', error)
|
|
||||||
return {
|
|
||||||
ip: '未知',
|
|
||||||
country: '未知',
|
|
||||||
city: '未知',
|
|
||||||
error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const throwError = ({ status = 500, msg = 'defalut error' } = {}) => {
|
|
||||||
const err = new Error(msg)
|
|
||||||
err.status = status // 主动抛错
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
|
|
||||||
const isIP = (ip = '') => {
|
|
||||||
const isIPv4 = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/
|
|
||||||
const isIPv6 = /^([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}|:((:[\da−fA−F]1,4)1,6|:)|:((:[\da−fA−F]1,4)1,6|:)|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?|([\da−fA−F]1,4:)6:|([\da−fA−F]1,4:)6:/
|
|
||||||
return isIPv4.test(ip) || isIPv6.test(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
const randomStr = (len) =>{
|
|
||||||
len = len || 16
|
|
||||||
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
|
|
||||||
a = str.length,
|
|
||||||
res = ''
|
|
||||||
for (let i = 0; i < len; i++) res += str.charAt(Math.floor(Math.random() * a))
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取UTC-x时间
|
|
||||||
const getUTCDate = (num = 8) => {
|
|
||||||
let date = new Date()
|
|
||||||
let now_utc = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(),
|
|
||||||
date.getUTCDate(), date.getUTCHours() + num,
|
|
||||||
date.getUTCMinutes(), date.getUTCSeconds())
|
|
||||||
return new Date(now_utc)
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTimestamp = (timestamp = Date.now(), format = 'time') => {
|
|
||||||
if(typeof(timestamp) !== 'number') return '--'
|
|
||||||
let date = new Date(timestamp)
|
|
||||||
let padZero = (num) => String(num).padStart(2, '0')
|
|
||||||
let year = date.getFullYear()
|
|
||||||
let mounth = padZero(date.getMonth() + 1)
|
|
||||||
let day = padZero(date.getDate())
|
|
||||||
let hours = padZero(date.getHours())
|
|
||||||
let minute = padZero(date.getMinutes())
|
|
||||||
let second = padZero(date.getSeconds())
|
|
||||||
let weekday = ['周日', '周一', '周二', '周三', '周四', '周五', '周六' ]
|
|
||||||
let week = weekday[date.getDay()]
|
|
||||||
switch (format) {
|
|
||||||
case 'date':
|
|
||||||
return `${ year }-${ mounth }-${ day }`
|
|
||||||
case 'week':
|
|
||||||
return `${ year }-${ mounth }-${ day } ${ week }`
|
|
||||||
case 'hour':
|
|
||||||
return `${ year }-${ mounth }-${ day } ${ hours }`
|
|
||||||
case 'time':
|
|
||||||
return `${ year }-${ mounth }-${ day } ${ hours }:${ minute }:${ second }`
|
|
||||||
default:
|
|
||||||
return `${ year }-${ mounth }-${ day } ${ hours }:${ minute }:${ second }`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getNetIPInfo,
|
|
||||||
throwError,
|
|
||||||
isIP,
|
|
||||||
randomStr,
|
|
||||||
getUTCDate,
|
|
||||||
formatTimestamp
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
|
|
||||||
const { AESDecrypt } = require('./encrypt')
|
|
||||||
const { readKey } = require('./storage')
|
|
||||||
const jwt = require('jsonwebtoken')
|
|
||||||
|
|
||||||
const enumLoginCode = {
|
|
||||||
SUCCESS: 1,
|
|
||||||
EXPIRES: -1,
|
|
||||||
ERROR_TOKEN: -2
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校验token与登录IP
|
|
||||||
const verifyAuth = (token, clientIp) =>{
|
|
||||||
if(['::ffff:', '::1'].includes(clientIp)) clientIp = '127.0.0.1'
|
|
||||||
token = AESDecrypt(token) // 先aes解密
|
|
||||||
const { commonKey } = readKey()
|
|
||||||
try {
|
|
||||||
const { exp } = jwt.verify(token, commonKey)
|
|
||||||
if(Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // 过期
|
|
||||||
|
|
||||||
let lastLoginIp = global.loginRecord[0] ? global.loginRecord[0].ip : ''
|
|
||||||
consola.info('校验客户端IP:', clientIp)
|
|
||||||
consola.info('最后登录的IP:', lastLoginIp)
|
|
||||||
// 判断: (生产环境)clientIp与上次登录成功IP不一致
|
|
||||||
if(isProd() && (!lastLoginIp || !clientIp || !clientIp.includes(lastLoginIp))) {
|
|
||||||
return { code: enumLoginCode.EXPIRES, msg: '登录IP发生变化, 需重新登录' } // IP与上次登录访问的不一致
|
|
||||||
}
|
|
||||||
return { code: enumLoginCode.SUCCESS, msg: 'success' } // 验证成功
|
|
||||||
} catch (error) {
|
|
||||||
return { code: enumLoginCode.ERROR_TOKEN, msg: error } // token错误, 验证失败
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isProd = () => {
|
|
||||||
const EXEC_ENV = process.env.EXEC_ENV || 'production'
|
|
||||||
return EXEC_ENV === 'production'
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
verifyAuth,
|
|
||||||
isProd
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
console.log('start time', new Date())
|
|
||||||
require('../app/main.js')
|
|
@ -1 +0,0 @@
|
|||||||
require('./app/main.js')
|
|
3
server/middleware/auth.js
Normal file
3
server/middleware/auth.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default defineEventHandler((event) => {
|
||||||
|
console.log('New request: ' + getRequestURL(event))
|
||||||
|
})
|
@ -1,59 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "easynode-server",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "easynode-server",
|
|
||||||
"bin": "./bin/www",
|
|
||||||
"pkg": {
|
|
||||||
"outputPath": "dist",
|
|
||||||
"scripts": "./*",
|
|
||||||
"assets": "./*"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"local": "cross-env EXEC_ENV=local nodemon ./app/main.js",
|
|
||||||
"server": "cross-env EXEC_ENV=production nodemon ./app/main.js",
|
|
||||||
"start": "node ./app/main.js",
|
|
||||||
"pkgwin": "pkg . -t node16-win-x64",
|
|
||||||
"pkglinux:x86": "pkg . -t node16-linux-x64",
|
|
||||||
"pkglinux:arm": "pkg . -t node16-linux-arm64"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"nodemonConfig": {
|
|
||||||
"ignore": [
|
|
||||||
"*.json"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@koa/cors": "^5.0.0",
|
|
||||||
"axios": "^0.21.4",
|
|
||||||
"consola": "^2.15.3",
|
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"global": "^4.4.0",
|
|
||||||
"jsonwebtoken": "^9.0.0",
|
|
||||||
"koa": "^2.13.1",
|
|
||||||
"koa-body": "^4.2.0",
|
|
||||||
"koa-compose": "^4.1.0",
|
|
||||||
"koa-compress": "^5.1.0",
|
|
||||||
"koa-jwt": "^4.0.3",
|
|
||||||
"koa-router": "^10.0.0",
|
|
||||||
"koa-sslify": "^5.0.0",
|
|
||||||
"koa-static": "^5.0.0",
|
|
||||||
"koa2-connect-history-api-fallback": "^0.1.3",
|
|
||||||
"log4js": "^6.4.4",
|
|
||||||
"node-os-utils": "^1.3.6",
|
|
||||||
"node-rsa": "^1.1.1",
|
|
||||||
"node-schedule": "^2.1.0",
|
|
||||||
"nodemailer": "^6.7.5",
|
|
||||||
"socket.io": "^4.4.1",
|
|
||||||
"socket.io-client": "^4.5.1",
|
|
||||||
"ssh2": "^1.10.0",
|
|
||||||
"ssh2-sftp-client": "^9.0.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"eslint": "^7.32.0",
|
|
||||||
"nodemon": "^2.0.15",
|
|
||||||
"pkg": "5.6"
|
|
||||||
}
|
|
||||||
}
|
|
4
server/routes/test.js
Normal file
4
server/routes/test.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// no api prefix
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
return 'test'
|
||||||
|
})
|
3
server/tsconfig.json
Normal file
3
server/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../.nuxt/tsconfig.server.json"
|
||||||
|
}
|
3101
server/yarn.lock
3101
server/yarn.lock
File diff suppressed because it is too large
Load Diff
80
tailwind.config.js
Normal file
80
tailwind.config.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||||
|
import colors from 'tailwindcss/colors'
|
||||||
|
|
||||||
|
const MyTheme = {
|
||||||
|
colors: {
|
||||||
|
green: {
|
||||||
|
DEFAULT: '#3BA676',
|
||||||
|
'50': '#B4E4CF',
|
||||||
|
'100': '#A5DFC5',
|
||||||
|
'200': '#87D4B2',
|
||||||
|
'300': '#69CA9E',
|
||||||
|
'400': '#4BBF8B',
|
||||||
|
'500': '#3BA676',
|
||||||
|
'600': '#2C7D59',
|
||||||
|
'700': '#1E533B',
|
||||||
|
'800': '#0F2A1E',
|
||||||
|
'900': '#000000',
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
DEFAULT: '#0096FF',
|
||||||
|
'50': '#B8E2FF',
|
||||||
|
'100': '#A3D9FF',
|
||||||
|
'200': '#7AC8FF',
|
||||||
|
'300': '#52B8FF',
|
||||||
|
'400': '#29A7FF',
|
||||||
|
'500': '#0096FF',
|
||||||
|
'600': '#0075C7',
|
||||||
|
'700': '#00548F',
|
||||||
|
'800': '#003357',
|
||||||
|
'900': '#00121F',
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
DEFAULT: '#FF6464',
|
||||||
|
'50': '#FFFFFF',
|
||||||
|
'100': '#FFFFFF',
|
||||||
|
'200': '#FFDEDE',
|
||||||
|
'300': '#FFB6B6',
|
||||||
|
'400': '#FF8D8D',
|
||||||
|
'500': '#FF6464',
|
||||||
|
'600': '#FF2C2C',
|
||||||
|
'700': '#F30000',
|
||||||
|
'800': '#BB0000',
|
||||||
|
'900': '#830000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
darkMode: 'class',
|
||||||
|
content: [
|
||||||
|
'./components/**/*.{vue,js,ts}',
|
||||||
|
'./layouts/**/*.vue',
|
||||||
|
'./pages/**/*.vue',
|
||||||
|
'./composables/**/*.{js,ts}',
|
||||||
|
'./plugins/**/*.{js,ts}',
|
||||||
|
'./App.{js,ts,vue}',
|
||||||
|
'./app.{js,ts,vue}',
|
||||||
|
'./Error.{js,ts,vue}',
|
||||||
|
'./error.{js,ts,vue}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
maxWidth: {
|
||||||
|
'8xl': '90rem',
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: MyTheme.colors.green,
|
||||||
|
// if want to change primary color to blue
|
||||||
|
// primary: MyTheme.colors.blue,
|
||||||
|
green: MyTheme.colors.green,
|
||||||
|
blue: MyTheme.colors.blue,
|
||||||
|
red: MyTheme.colors.red,
|
||||||
|
slate: colors.slate,
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Nunito', ...defaultTheme.fontFamily.sans],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
3
tsconfig.json
Normal file
3
tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user