Compare commits

...

4 Commits

Author SHA1 Message Date
chaoszhu
fac9447910 update 2024-07-23 00:37:57 +08:00
chaoszhu
6dd06b9141 chore: update 2024-07-09 10:51:52 +08:00
chaoszhu
0255ef6c0b chore:update 2024-07-04 22:39:38 +08:00
chaoszhu
6a01a9e658 refactor: init 2024-07-03 17:57:48 +08:00
96 changed files with 9168 additions and 9669 deletions

View File

@ -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
View 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
View File

@ -1,7 +1,26 @@
node_modules
!.gitkeep
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
easynode-server.zip
server/app/static/upload/*
server/app/socket/temp/*
server/app/logs/*
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

View File

@ -1,58 +1,62 @@
## [1.2.1](https://github.com/chaos-zhu/easynode/releases) (2022-12-12)
### Features
* 新增支持终端长命令输入模式 ✔
* 新增前端静态文件缓存 ✔
* 【重要】v1.2.1开始移除创建https服务 ✔
### Bug Fixes
* v1.2的若干bug...
## [1.2.0](https://github.com/chaos-zhu/easynode/releases) (2022-09-12)
### Features
* 新增邮件通知: 包括登录面板、密码修改、服务器到期、服务器离线等 ✔
* 支持服务器分组(为新版UI作准备的) ✔
* 面板功能调整支持http延迟显示、支持服务器控制台直达与到期时间字段 ✔
* 优化终端输入、支持状态面板收缩 ✔
* **全新SFTP功能支持上传下载进度条展示**
* **支持在线文件编辑与保存**
### Bug Fixes
* v1.1的若干bug...
---
## [1.1.0](https://github.com/chaos-zhu/easynode/releases) (2022-06-27)
### Features
* ssh密钥/密码(采用对称AES+RSA非对称双加密传输与存储)、jwtToken(服务端对称加密传输) ✔
* 加密储存登录密码 ✔
* 登录IP检测机制&历史登录查询✔
* 终端多tab支持✔
* 终端页左侧栏信息✔
* 客户端支持ARM实例✔
### Bug Fixes
* 修复终端展示异常的Bug✔
* 修复保存私钥时第二次选择无效的bug✔
* 修复面板客户端探针断开更新不及时的bug✔
* 修复移除主机未移除ssh密钥信息的bug✔
* 修复服务器排序bug✔
* 解决https下无法socket连接到客户端bug✔
---
## [1.0.0](https://github.com/chaos-zhu/easynode/releases) (2022-06-08)
### Features
* 通过`websocker实时更新`服务器基本信息: 系统、公网IP、CPU、内存、硬盘、网卡等
* 解决`SSH跨端同步`问题——Web SSH
## [2.0.0-beta](https://github.com/chaos-zhu/easynode/releases) (2024-07-13)
底层代码重构
## [1.2.1](https://github.com/chaos-zhu/easynode/releases) (2022-12-12)
### Features
* 新增支持终端长命令输入模式 ✔
* 新增前端静态文件缓存 ✔
* 【重要】v1.2.1开始移除创建https服务 ✔
### Bug Fixes
* v1.2的若干bug...
## [1.2.0](https://github.com/chaos-zhu/easynode/releases) (2022-09-12)
### Features
* 新增邮件通知: 包括登录面板、密码修改、服务器到期、服务器离线等 ✔
* 支持服务器分组(为新版UI作准备的) ✔
* 面板功能调整支持http延迟显示、支持服务器控制台直达与到期时间字段 ✔
* 优化终端输入、支持状态面板收缩 ✔
* **全新SFTP功能支持上传下载进度条展示**
* **支持在线文件编辑与保存**
### Bug Fixes
* v1.1的若干bug...
---
## [1.1.0](https://github.com/chaos-zhu/easynode/releases) (2022-06-27)
### Features
* ssh密钥/密码(采用对称AES+RSA非对称双加密传输与存储)、jwtToken(服务端对称加密传输) ✔
* 加密储存登录密码 ✔
* 登录IP检测机制&历史登录查询✔
* 终端多tab支持✔
* 终端页左侧栏信息✔
* 客户端支持ARM实例✔
### Bug Fixes
* 修复终端展示异常的Bug✔
* 修复保存私钥时第二次选择无效的bug✔
* 修复面板客户端探针断开更新不及时的bug✔
* 修复移除主机未移除ssh密钥信息的bug✔
* 修复服务器排序bug✔
* 解决https下无法socket连接到客户端bug✔
---
## [1.0.0](https://github.com/chaos-zhu/easynode/releases) (2022-06-08)
### Features
* 通过`websocker实时更新`服务器基本信息: 系统、公网IP、CPU、内存、硬盘、网卡等
* 解决`SSH跨端同步`问题——Web SSH

40
Q&A.md
View File

@ -1,20 +1,20 @@
# Q&A
## CentOS7/8启动服务失败
> 先关闭SELinux
```shell
vi /etc/selinux/config
SELINUX=enforcing
// 修改为禁用
SELINUX=disabled
```
> 重启:`reboot`,再使用一键脚本安装
> 查看SELinux状态sestatus
## 客户端服务启动成功,无法连接?
> 端口未开放:`iptables -I INPUT -s 0.0.0.0/0 -p tcp --dport 22022 -j ACCEPT` 或者 `rm -rf /etc/iptables && reboot`
# Q&A
## CentOS7/8启动服务失败
> 先关闭SELinux
```shell
vi /etc/selinux/config
SELINUX=enforcing
// 修改为禁用
SELINUX=disabled
```
> 重启:`reboot`,再使用一键脚本安装
> 查看SELinux状态sestatus
## 客户端服务启动成功,无法连接?
> 端口未开放:`iptables -I INPUT -s 0.0.0.0/0 -p tcp --dport 22022 -j ACCEPT` 或者 `rm -rf /etc/iptables && reboot`

179
README.md
View File

@ -1,173 +1,6 @@
# EasyNode v1.2
> 一个简易的个人Linux服务器管理面板(基于Node.js).
> 前端仓库地址https://github.com/chaos-zhu/easynode_web
<!-- - [EasyNode](#easynode) -->
- [功能简介](#功能简介)
- [安装指南](#安装指南)
- [服务端安装](#服务端安装)
- [Docker镜像](#docker镜像)
- [一键脚本](#一键脚本)
- [手动部署](#手动部署)
- [客户端安装](#客户端安装)
- [X86架构](#x86架构)
- [ARM架构](#arm架构)
- [升级指南](#升级指南)
- [服务端](#服务端)
- [客户端](#客户端)
- [版本日志](#版本日志)
- [安全与说明](#安全与说明)
- [Q&A](#qa)
- [感谢Star](#感谢star)
- [License](#license)
## 功能简介
> 多服务器管理; 通过`websocket实时更新`服务器基本信息: **系统、公网IP、CPU、内存、硬盘、网卡**等
![服务器面板](./images/v1.2-1.png)
> 基于浏览器解决`SSH&SFTP跨端`烦恼——**Web SSH**&**Web SFTP**
![websftp功能](./images/v1.2-2.png)
> 在线编辑文件
![edit](./images/v1.2-3.png)
## 安装指南
### 服务端安装
- 依赖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).
# 重构中, 老版本切换v1.2分支部署
## EasyNode
> 一个简易的个人Linux服务器管理面板.

9
app.vue Normal file
View File

@ -0,0 +1,9 @@
<template>
<NuxtPage />
</template>
<script setup>
</script>

View File

@ -1,67 +1,67 @@
const { Server } = require('socket.io')
const schedule = require('node-schedule')
const axios = require('axios')
let getOsData = require('../utils/os-data')
let serverSockets = {}, ipInfo = {}, osData = {}
async function getIpInfo() {
try {
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
console.log('getIpInfo Success: ', new Date())
ipInfo = data
} catch (error) {
console.log('getIpInfo Error: ', new Date(), error)
}
}
function ipSchedule() {
let rule1 = new schedule.RecurrenceRule()
rule1.second = [0, 10, 20, 30, 40, 50]
schedule.scheduleJob(rule1, () => {
let { query, country, city } = ipInfo || {}
if(query && country && city) return
console.log('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, () => {
console.log('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) => {
serverSockets[socket.id] = setInterval(async () => {
try {
osData = await getOsData()
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
} catch (error) {
console.error('客户端错误:', error)
socket && socket.emit('client_error', { error })
}
}, 1500)
socket.on('disconnect', () => {
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
delete serverSockets[socket.id]
socket.close && socket.close()
socket = null
})
})
}
const { Server } = require('socket.io')
const schedule = require('node-schedule')
const axios = require('axios')
let getOsData = require('../utils/os-data')
let serverSockets = {}, ipInfo = {}, osData = {}
async function getIpInfo() {
try {
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
console.log('getIpInfo Success: ', new Date())
ipInfo = data
} catch (error) {
console.log('getIpInfo Error: ', new Date(), error)
}
}
function ipSchedule() {
let rule1 = new schedule.RecurrenceRule()
rule1.second = [0, 10, 20, 30, 40, 50]
schedule.scheduleJob(rule1, () => {
let { query, country, city } = ipInfo || {}
if(query && country && city) return
console.log('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, () => {
console.log('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) => {
serverSockets[socket.id] = setInterval(async () => {
try {
osData = await getOsData()
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
} catch (error) {
console.error('客户端错误:', error)
socket && socket.emit('client_error', { error })
}
}, 1500)
socket.on('disconnect', () => {
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
delete serverSockets[socket.id]
socket.close && socket.close()
socket = null
})
})
}

View File

@ -1,35 +1,35 @@
{
"name": "easynode-client",
"version": "1.0.0",
"description": "easynode-client",
"bin": "./bin/www",
"pkg": {
"outputPath": "dist"
},
"scripts": {
"client": "nodemon ./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": {
"axios": "^0.21.4",
"koa": "^2.13.1",
"node-os-utils": "^1.3.6",
"node-schedule": "^2.1.0",
"socket.io": "^4.4.1"
},
"devDependencies": {
"eslint": "^7.32.0",
"nodemon": "^2.0.15",
"pkg": "5.6"
}
}
{
"name": "easynode-client",
"version": "1.0.0",
"description": "easynode-client",
"bin": "./bin/www",
"pkg": {
"outputPath": "dist"
},
"scripts": {
"client": "nodemon ./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": {
"axios": "^0.21.4",
"koa": "^2.13.1",
"node-os-utils": "^1.3.6",
"node-schedule": "^2.1.0",
"socket.io": "^4.4.1"
},
"devDependencies": {
"eslint": "^7.32.0",
"nodemon": "^2.0.15",
"pkg": "5.6"
}
}

15
components.d.ts vendored Normal file
View 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']
}
}

View File

@ -0,0 +1,11 @@
export const useSyncProps = (props, key, emit) => {
return computed({
get() {
return props[key]
},
set(value) {
emit(`update:${key}`, value)
}
})
}

View File

@ -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 "***********************安装成功***********************"

View File

@ -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 "***********************安装成功***********************"

View File

@ -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"

View File

@ -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
View 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
View 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
View 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: {}
})

View File

@ -1,24 +1,33 @@
{
"name": "easynode",
"version": "1.0.0",
"description": "easy to manage the server",
"private": true,
"workspaces": ["server", "client"],
"description": "connect to your server via the easynode",
"private": false,
"repository": {
"type": "git",
"url": "git+https://github.com/chaos-zhu/easynode.git"
},
"keywords": [
"vps",
"node",
"easynode",
"chaos",
"chaoszhu"
],
"author": "chaoszhu",
"license": "ISC",
"bugs": {
"url": "https://github.com/chaos-zhu/easynode/issues"
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"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
View 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
View File

@ -0,0 +1,3 @@
export default defineNuxtPlugin((nuxt) => {
// console.log('plugin')
})

8575
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -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
View File

@ -1,7 +0,0 @@
node_modules
app/static/upload/*
app/socket/temp/*
app/socket/.sftp-cache/*
app/logs/*
!.gitkeep
dist

View File

@ -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"]

View File

@ -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()等读写apipkg打包后的环境不支持设置保存日志的目录需使用process.cwd()】
> win闪退: 在linux机器上构建可查看输出日志
## 客户端
> **构建客户端服务, 后台运行** `nohup ./easynode-server &`
> 功能服务器基本信息【ssh信息保存在主服务器】

View File

@ -0,0 +1,3 @@
export default defineEventHandler(async (event) => {
return 'get-user-info'
})

View File

@ -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 // 是否记录日志
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 = []
}

View File

@ -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()

View File

@ -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

View File

@ -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
}
})

View File

@ -1,5 +0,0 @@
const compress = require('koa-compress')
const options = { threshold: 2048 }
module.exports = compress(options)

View File

@ -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

View File

@ -1,3 +0,0 @@
const { historyApiFallback } = require('koa2-connect-history-api-fallback')
module.exports = historyApiFallback({ whiteList: ['/api'] })

View File

@ -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, // 先处理bodylog和router都要用到
useLog, // 日志记录开始【该module使用到了fs.mkdir()等读写api 设置保存日志的目录需使用process.cwd()】
useAuth,
useAllowedMethods,
useRoutes
]

View File

@ -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()

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)
}

View File

@ -1,7 +0,0 @@
const offlineInspect = require('./offline-inspect')
const expiredNotify = require('./expired-notify')
module.exports = () => {
offlineInspect()
expiredNotify()
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
})
})
})
}

View File

@ -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)
})
})
})
}

View File

@ -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)
})
})
}

View File

@ -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)
})
})
})
}

View File

@ -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

File diff suppressed because one or more lines are too long

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

View File

@ -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>

View File

@ -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
> 服务器分组配置

View File

@ -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": [
]
}

View File

@ -1,7 +0,0 @@
[
{
"id": "default",
"name": "默认分组",
"index": 0
}
]

View File

@ -1 +0,0 @@
[]

View File

@ -1,6 +0,0 @@
{
"pwd": "admin",
"commonKey": "",
"publicKey": "",
"privateKey": ""
}

View File

@ -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
}
]

View File

@ -1 +0,0 @@
[]

View File

@ -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>

View File

@ -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>
`
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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}|:((:[\dafAF]1,4)1,6|:)|:((:[\dafAF]1,4)1,6|:)|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)|([\dafAF]1,4:)2((:[\dafAF]1,4)1,4|:)|([\dafAF]1,4:)2((:[\dafAF]1,4)1,4|:)|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)|([\dafAF]1,4:)4((:[\dafAF]1,4)1,2|:)|([\dafAF]1,4:)4((:[\dafAF]1,4)1,2|:)|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?|([\dafAF]1,4:)6:|([\dafAF]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
}

View File

@ -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
}

View File

@ -1,3 +0,0 @@
#!/usr/bin/env node
console.log('start time', new Date())
require('../app/main.js')

View File

@ -1 +0,0 @@
require('./app/main.js')

View File

@ -0,0 +1,3 @@
export default defineEventHandler((event) => {
console.log('New request: ' + getRequestURL(event))
})

View File

@ -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
View File

@ -0,0 +1,4 @@
// no api prefix
export default defineEventHandler(async (event) => {
return 'test'
})

3
server/tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

File diff suppressed because it is too large Load Diff

80
tailwind.config.js Normal file
View 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
View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

3124
yarn.lock

File diff suppressed because it is too large Load Diff