Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c55d3bddd6 | ||
|
1b3b2892d0 | ||
|
1171d6e6cc | ||
|
ccec3868bb |
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,5 +13,3 @@ plan.md
|
|||||||
.env.local
|
.env.local
|
||||||
.env-encrypt-key
|
.env-encrypt-key
|
||||||
*clear.js
|
*clear.js
|
||||||
local-script
|
|
||||||
版本发布.md
|
|
||||||
|
31
CHANGELOG.md
31
CHANGELOG.md
@ -1,34 +1,3 @@
|
|||||||
## [3.0.3](https://github.com/chaos-zhu/easynode/releases) (2024-12-22)
|
|
||||||
|
|
||||||
* 支持keyboard-interactive服务器验证(serv00验证通过)
|
|
||||||
* 支持TG Bot通知方式
|
|
||||||
* 添加web端Plus授权功能
|
|
||||||
* 修复一些UI问题
|
|
||||||
* 修复MFA2登录验证码为0开头无法输入的bug
|
|
||||||
|
|
||||||
## [3.0.2](https://github.com/chaos-zhu/easynode/releases) (2024-11-20)
|
|
||||||
|
|
||||||
* 修复添加实例错误禁用的bug
|
|
||||||
|
|
||||||
|
|
||||||
## [3.0.1](https://github.com/chaos-zhu/easynode/releases) (2024-11-18)
|
|
||||||
|
|
||||||
* 修复同IP实例SFTP连接到其他的实例的bug
|
|
||||||
* 修复一些UI问题
|
|
||||||
|
|
||||||
|
|
||||||
## [3.0.0](https://github.com/chaos-zhu/easynode/releases) (2024-11-09)
|
|
||||||
|
|
||||||
* 新增跳板机功能,支持选择多台机器跳转
|
|
||||||
* 脚本库批量导出导入
|
|
||||||
* 本地socket断开自动重连,无需手动重新连接
|
|
||||||
* 支持脚本库模糊搜索功能
|
|
||||||
* 分组添加实例数量标识
|
|
||||||
* 优化登录逻辑
|
|
||||||
* 默认登录有效期更改为当天有效
|
|
||||||
* 优化脚本库新增脚本时序号自动累加
|
|
||||||
* 修复一些小bug
|
|
||||||
|
|
||||||
## [2.3.0](https://github.com/chaos-zhu/easynode/releases) (2024-10-24)
|
## [2.3.0](https://github.com/chaos-zhu/easynode/releases) (2024-10-24)
|
||||||
|
|
||||||
* 重构本地数据库存储方式(性能提升一个level~)
|
* 重构本地数据库存储方式(性能提升一个level~)
|
||||||
|
131
README.md
131
README.md
@ -1,61 +1,29 @@
|
|||||||
<div align="center">
|
|
||||||
|
|
||||||
# EasyNode
|
# EasyNode
|
||||||
|
|
||||||
_✨ 一个多功能Linux服务器WEB终端面板(webSSH&webSFTP) ✨_
|
<!-- - [功能](#功能)
|
||||||
|
- [安装](#安装)
|
||||||
</div>
|
- [监控服务安装](#监控服务安装)
|
||||||
|
- [版本日志](#版本日志)
|
||||||
<p align="center">
|
- [开发](#开发)
|
||||||
<a href="https://github.com/chaos-zhu/easynode/releases/latest">
|
- [QA](#QA)
|
||||||
<img src="https://img.shields.io/github/v/release/chaos-zhu/easynode?color=brightgreen" alt="release">
|
- [安全与建议](#安全与建议)
|
||||||
</a>
|
- [捐赠](#捐赠)
|
||||||
<a href="https://github.com/chaos-zhu/easynode/actions">
|
- [License](#license) -->
|
||||||
<img src="https://img.shields.io/github/actions/workflow/status/chaos-zhu/easynode/docker-builder.yml?branch=main" alt="deployment status">
|
|
||||||
</a>
|
|
||||||
<a href="https://hub.docker.com/repository/docker/chaoszhu/easynode">
|
|
||||||
<img src="https://img.shields.io/docker/pulls/chaoszhu/easynode?color=brightgreen" alt="docker pull">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/chaos-zhu/easynode/releases/latest">
|
|
||||||
<img src="https://img.shields.io/github/downloads/chaos-zhu/easynode/total?color=brightgreen&include_prereleases" alt="release">
|
|
||||||
</a>
|
|
||||||
<a href="https://raw.githubusercontent.com/chaos-zhu/easynode/main/LICENSE">
|
|
||||||
<img src="https://img.shields.io/github/license/chaos-zhu/easynode?color=brightgreen" alt="license">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="#功能">功能</a>
|
|
||||||
·
|
|
||||||
<a href="#面板展示">面板展示</a>
|
|
||||||
·
|
|
||||||
<a href="#项目部署">项目部署</a>
|
|
||||||
·
|
|
||||||
<a href="#监控服务安装">监控服务安装</a>
|
|
||||||
·
|
|
||||||
<a href="#安全与建议">安全与建议</a>
|
|
||||||
·
|
|
||||||
<a href="#常见问题">常见问题</a>
|
|
||||||
<!-- ·
|
|
||||||
<a href="#Plus功能">Plus版功能</a> -->
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
+ [x] 功能完善的**SSH终端**&**SFTP**
|
- [x] 功能完善的**SSH终端**&**SFTP**
|
||||||
+ [x] 批量导入、导出、编辑服务器配置、脚本等
|
- [x] 批量**导入导出**实例(Xshell&FinalShell&EasyNode)
|
||||||
+ [x] 脚本库
|
- [x] **实例分组**
|
||||||
+ [x] 实例分组
|
- [x] **凭据托管**
|
||||||
+ [x] 凭据托管
|
- [x] **多渠道通知**
|
||||||
+ [x] 多渠道通知
|
- [x] **脚本库**
|
||||||
+ [x] 批量下发指令
|
- [x] **批量指令**
|
||||||
+ [x] 自定义终端主题
|
- [x] **终端主题背景自定义**
|
||||||
|
|
||||||
## 面板展示
|

|
||||||
|
|
||||||

|
## 安装
|
||||||
|
|
||||||
## 项目部署
|
|
||||||
|
|
||||||
- 默认账户密码 `admin/admin`
|
- 默认账户密码 `admin/admin`
|
||||||
- web端口:8082
|
- web端口:8082
|
||||||
@ -63,15 +31,35 @@ _✨ 一个多功能Linux服务器WEB终端面板(webSSH&webSFTP) ✨_
|
|||||||
### docker镜像
|
### docker镜像
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -d -p 8082:8082 --restart=always -v /root/easynode/db:/easynode/app/db chaoszhu/easynode
|
docker run -d -p 8082:8082 --name=easynode --restart=always -v /root/easynode/db:/easynode/app/db chaoszhu/easynode
|
||||||
```
|
```
|
||||||
环境变量:
|
环境变量:
|
||||||
|
- `PLUS_KEY`: 激活PLUS功能的授权码
|
||||||
- `DEBUG`: 启动debug日志 0:关闭 1:开启, 默认关闭
|
- `DEBUG`: 启动debug日志 0:关闭 1:开启, 默认关闭
|
||||||
- `ALLOWED_IPS`: 可以访问服务的IP白名单, 多个使用逗号分隔, 支持填写部分ip前缀, 例如: `-e ALLOWED_IPS=127.0.0.1,196.168`
|
- `ALLOWED_IPS`: 可以访问服务的IP白名单, 多个使用逗号分隔, 支持填写部分ip前缀, 例如: `-e ALLOWED_IPS=127.0.0.1,196.168`
|
||||||
|
|
||||||
|
### 手动部署
|
||||||
|
|
||||||
|
依赖Nodejs版本 > 20+
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/chaos-zhu/easynode
|
||||||
|
cd easynode
|
||||||
|
yarn
|
||||||
|
cd web
|
||||||
|
yarn build
|
||||||
|
mv dist/* ../server/app/static
|
||||||
|
cd ../server
|
||||||
|
yarn start
|
||||||
|
# 后台运行需安装pm2
|
||||||
|
pm2 start index.js --name easynode-server
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 监控服务安装
|
## 监控服务安装
|
||||||
|
|
||||||
- 监控服务用于实时向服务端&web端推送**系统、公网IP、CPU、内存、硬盘、网卡**等基础信息
|
- 监控服务用于实时向服务端推送**系统、公网IP、CPU、内存、硬盘、网卡**等基础信息,不安装不影响使用面板,但是无法实时同步cpu占用、实时网速、硬盘容量等实用信息。
|
||||||
|
|
||||||
- 默认端口:**22022**
|
- 默认端口:**22022**
|
||||||
|
|
||||||
@ -79,16 +67,16 @@ docker run -d -p 8082:8082 --restart=always -v /root/easynode/db:/easynode/app/d
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
# 使用默认端口22022安装
|
# 使用默认端口22022安装
|
||||||
curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash
|
curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash
|
||||||
|
|
||||||
# 使用自定义端口安装, 例如54321
|
# 使用自定义端口安装, 例如54321
|
||||||
curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash -s -- 54321
|
curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash -s -- 54321
|
||||||
```
|
```
|
||||||
|
|
||||||
> 卸载
|
> 卸载
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash
|
curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
> 查看监控服务状态:`systemctl status easynode-client`
|
> 查看监控服务状态:`systemctl status easynode-client`
|
||||||
@ -99,6 +87,20 @@ curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easy
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
1. 拉取代码,环境 `nodejs>=20`
|
||||||
|
2. cd到项目根目录,`yarn install` 执行安装依赖
|
||||||
|
3. `yarn dev`启动项目
|
||||||
|
4. web: `http://localhost:18090/`
|
||||||
|
|
||||||
|
## 版本日志
|
||||||
|
|
||||||
|
- [CHANGELOG](./CHANGELOG.md)
|
||||||
|
|
||||||
|
## QA
|
||||||
|
|
||||||
|
- [QA](./Q%26A.md)
|
||||||
|
|
||||||
## 安全与建议
|
## 安全与建议
|
||||||
|
|
||||||
@ -109,15 +111,14 @@ curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easy
|
|||||||
|
|
||||||
webssh与监控服务都将以`该服务器作为中转`。中国大陆用户建议使用香港、新加坡、日本、韩国等地区的低延迟服务器来安装服务端面板
|
webssh与监控服务都将以`该服务器作为中转`。中国大陆用户建议使用香港、新加坡、日本、韩国等地区的低延迟服务器来安装服务端面板
|
||||||
|
|
||||||
## 常见问题
|
## 捐赠
|
||||||
|
|
||||||
- [QA](./Q%26A.md)
|
如果您认为此项目帮到了您, 您可以请我喝杯阔乐~
|
||||||
|
|
||||||
<!-- ## Plus版功能
|

|
||||||
|
|
||||||
- 跳板机功能,拯救被墙实例与龟速终端输入
|
## License
|
||||||
- 本地socket断开自动重连,无需手动重新连接
|
|
||||||
- 批量修改实例配置(优化版)
|
[MIT](LICENSE). Copyright (c).
|
||||||
- 脚本库批量导出导入
|
|
||||||
- 凭据管理支持解密带密码保护的密钥
|

|
||||||
- 提出的功能需求享有更高的开发优先级 -->
|
|
||||||
|
@ -10,7 +10,7 @@ SERVER_NAME=easynode-client
|
|||||||
FILE_PATH=/root/local/easynode-client
|
FILE_PATH=/root/local/easynode-client
|
||||||
SERVICE_PATH=/etc/systemd/system
|
SERVICE_PATH=/etc/systemd/system
|
||||||
CLIENT_VERSION=client-2024-10-13 # 目前监控客户端版本发布需手动更改为最新版本号
|
CLIENT_VERSION=client-2024-10-13 # 目前监控客户端版本发布需手动更改为最新版本号
|
||||||
SERVER_PROXY="https://git.221022.xyz/"
|
SERVER_PROXY="https://ghp.ci/"
|
||||||
|
|
||||||
if [ ! -z "$1" ]; then
|
if [ ! -z "$1" ]; then
|
||||||
clientPort=$1
|
clientPort=$1
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"yarn workspace web run dev\" \"yarn workspace server run local\"",
|
"dev": "concurrently \"yarn workspace web run dev\" \"yarn workspace server run local\"",
|
||||||
"clean": "rimraf web/node_modules server/node_modules client/node_modules node_modules",
|
"clean": "rimraf web/node_modules server/node_modules client/node_modules node_modules",
|
||||||
"encrypt": "node ./local-script/encrypt-file.js"
|
"encrypt": "node ./script/encrypt-file.js"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/chaos-zhu/easynode/issues"
|
"url": "https://github.com/chaos-zhu/easynode/issues"
|
||||||
|
49
script/encrypt-file.js
Normal file
49
script/encrypt-file.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
const fs = require('fs-extra')
|
||||||
|
const path = require('path')
|
||||||
|
const CryptoJS = require('crypto-js')
|
||||||
|
require('dotenv').config({ path: '.env-encrypt-key' })
|
||||||
|
const version = require('../server/package.json').version
|
||||||
|
|
||||||
|
console.log('加密版本:', version, '加密密钥:', process.env.PLUS_DECRYPT_KEY)
|
||||||
|
|
||||||
|
async function encryptPlusClearFiles(dir) {
|
||||||
|
try {
|
||||||
|
if (dir.includes('node_modules')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await fs.readdir(dir)
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = path.join(dir, file)
|
||||||
|
const stat = await fs.stat(fullPath)
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
await encryptPlusClearFiles(fullPath)
|
||||||
|
} else if (file === 'plus-clear.js') {
|
||||||
|
const content = await fs.readFile(fullPath, 'utf-8')
|
||||||
|
|
||||||
|
const encryptedContent = CryptoJS.AES.encrypt(content, process.env.PLUS_DECRYPT_KEY).toString()
|
||||||
|
|
||||||
|
const newPath = path.join(path.dirname(fullPath), 'plus.js')
|
||||||
|
|
||||||
|
await fs.writeFile(newPath, encryptedContent)
|
||||||
|
|
||||||
|
console.log(`已加密文件: ${fullPath}`)
|
||||||
|
console.log(`生成加密文件: ${newPath} `)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加密过程出错:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const appDir = path.join(__dirname, '../server')
|
||||||
|
|
||||||
|
encryptPlusClearFiles(appDir)
|
||||||
|
.then(() => {
|
||||||
|
console.log(`${version} 版本加密完成!`)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('程序执行出错:', error)
|
||||||
|
})
|
21
script/update-version.js
Normal file
21
script/update-version.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const newVersion = process.argv[2];
|
||||||
|
|
||||||
|
if (!newVersion) {
|
||||||
|
console.error('请提供新版本号,例如: node update-version.js 3.1.0');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
path.join(__dirname, 'server/package.json'),
|
||||||
|
path.join(__dirname, 'web/package.json')
|
||||||
|
];
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
const content = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||||
|
content.version = newVersion;
|
||||||
|
fs.writeFileSync(file, JSON.stringify(content, null, 2) + '\n');
|
||||||
|
console.log(`已更新 ${file} 的版本到 ${newVersion}`);
|
||||||
|
});
|
@ -1,12 +1,12 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "easynode监控服务安装",
|
"name": "easynode监控服务安装",
|
||||||
"command": "curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash",
|
"command": "curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash",
|
||||||
"description": "easynode-监控服务-安装脚本"
|
"description": "easynode-监控服务-安装脚本"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "easynode监控服务卸载",
|
"name": "easynode监控服务卸载",
|
||||||
"command": "curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash",
|
"command": "curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash",
|
||||||
"description": "easynode-监控服务-卸载脚本"
|
"description": "easynode-监控服务-卸载脚本"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
const path = require('path')
|
|
||||||
const decryptAndExecuteAsync = require('../utils/decrypt-file')
|
|
||||||
const { RSADecryptAsync, AESEncryptAsync, AESDecryptAsync } = require('../utils/encrypt')
|
const { RSADecryptAsync, AESEncryptAsync, AESDecryptAsync } = require('../utils/encrypt')
|
||||||
const { HostListDB } = require('../utils/db-class')
|
const { HostListDB } = require('../utils/db-class')
|
||||||
const hostListDB = new HostListDB().getInstance()
|
const hostListDB = new HostListDB().getInstance()
|
||||||
|
|
||||||
async function getHostList({ res }) {
|
async function getHostList({ res }) {
|
||||||
|
// console.log('get-host-list')
|
||||||
let data = await hostListDB.findAsync({})
|
let data = await hostListDB.findAsync({})
|
||||||
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
|
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
try {
|
try {
|
||||||
let { authType, _id: id, credential } = item
|
let { username, port, authType, _id: id, credential } = item
|
||||||
|
// console.log('解密凭证title: ', credential)
|
||||||
if (credential) credential = await AESDecryptAsync(credential)
|
if (credential) credential = await AESDecryptAsync(credential)
|
||||||
const isConfig = Boolean(authType && item[authType])
|
const isConfig = Boolean(username && port && (item[authType]))
|
||||||
Object.assign(item, { id, isConfig, password: '', privateKey: '', credential })
|
Object.assign(item, { id, isConfig, password: '', privateKey: '', credential })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
consola.error('getHostList error: ', error.message)
|
consola.error('getHostList error: ', error.message)
|
||||||
@ -21,55 +21,93 @@ async function getHostList({ res }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function addHost({ res, request }) {
|
async function addHost({ res, request }) {
|
||||||
let { body } = request
|
let {
|
||||||
if (!body.name || !body.host) return res.fail({ msg: 'missing params: name or host' })
|
body: {
|
||||||
let newRecord = { ...body }
|
name, host, index, expired, expiredNotify, group, consoleUrl, remark,
|
||||||
const { authType, tempKey } = newRecord
|
port: newPort, clientPort, username, authType, password, privateKey, credential, command, tempKey
|
||||||
if (newRecord[authType] && tempKey) {
|
|
||||||
const clearTempKey = await RSADecryptAsync(tempKey)
|
|
||||||
const clearSSHKey = await AESDecryptAsync(newRecord[authType], clearTempKey)
|
|
||||||
newRecord[authType] = await AESEncryptAsync(clearSSHKey)
|
|
||||||
}
|
}
|
||||||
await hostListDB.insertAsync(newRecord)
|
} = request
|
||||||
|
// console.log(request)
|
||||||
|
if (!host || !name) return res.fail({ msg: 'missing params: name or host' })
|
||||||
|
let record = {
|
||||||
|
name, host, index, expired, expiredNotify, group, consoleUrl, remark,
|
||||||
|
port: newPort, clientPort, username, authType, password, privateKey, credential, command
|
||||||
|
}
|
||||||
|
if (record[authType]) {
|
||||||
|
const clearTempKey = await RSADecryptAsync(tempKey)
|
||||||
|
console.log('clearTempKey:', clearTempKey)
|
||||||
|
const clearSSHKey = await AESDecryptAsync(record[authType], clearTempKey)
|
||||||
|
console.log(`${ authType }原密文: `, clearSSHKey)
|
||||||
|
record[authType] = await AESEncryptAsync(clearSSHKey)
|
||||||
|
// console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
||||||
|
}
|
||||||
|
await hostListDB.insertAsync(record)
|
||||||
res.success()
|
res.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateHost({ res, request }) {
|
async function updateHost({ res, request }) {
|
||||||
let {
|
let {
|
||||||
body
|
body: {
|
||||||
} = request
|
hosts,
|
||||||
if (typeof body !== 'object') return res.fail({ msg: '参数错误' })
|
id,
|
||||||
const updateFiled = { ...body }
|
host: newHost, name: newName, index, oldHost, expired, expiredNotify, group, consoleUrl, remark,
|
||||||
const { id, authType, tempKey } = updateFiled
|
port, clientPort, username, authType, password, privateKey, credential, command, tempKey, jumpHosts = []
|
||||||
if (authType && updateFiled[authType]) {
|
|
||||||
const clearTempKey = await RSADecryptAsync(tempKey)
|
|
||||||
const clearSSHKey = await AESDecryptAsync(updateFiled[authType], clearTempKey)
|
|
||||||
updateFiled[authType] = await AESEncryptAsync(clearSSHKey)
|
|
||||||
delete updateFiled.tempKey
|
|
||||||
} else {
|
|
||||||
delete updateFiled.authType
|
|
||||||
delete updateFiled.password
|
|
||||||
delete updateFiled.privateKey
|
|
||||||
delete updateFiled.credential
|
|
||||||
}
|
}
|
||||||
console.log('updateFiled: ', updateFiled)
|
} = request
|
||||||
await hostListDB.updateAsync({ _id: id }, { $set: { ...updateFiled } })
|
let isBatch = Array.isArray(hosts)
|
||||||
res.success({ msg: '修改成功' })
|
if (isBatch) {
|
||||||
|
if (!hosts.length) return res.fail({ msg: 'hosts为空' })
|
||||||
|
let hostList = await hostListDB.findAsync({})
|
||||||
|
for (let oldRecord of hostList) {
|
||||||
|
let target = hosts.find(item => item.id === oldRecord._id)
|
||||||
|
if (!target) continue
|
||||||
|
let { authType } = target
|
||||||
|
// 如果存在原认证方式则保存下来
|
||||||
|
if (!target[authType]) {
|
||||||
|
target[authType] = oldRecord[authType]
|
||||||
|
} else {
|
||||||
|
const clearTempKey = await RSADecryptAsync(target.tempKey)
|
||||||
|
// console.log('批量解密tempKey:', clearTempKey)
|
||||||
|
const clearSSHKey = await AESDecryptAsync(target[authType], clearTempKey)
|
||||||
|
// console.log(`${ authType }原密文: `, clearSSHKey)
|
||||||
|
target[authType] = await AESEncryptAsync(clearSSHKey)
|
||||||
|
// console.log(`${ authType }__commonKey加密存储: `, target[authType])
|
||||||
|
}
|
||||||
|
delete target.monitorData
|
||||||
|
delete target.tempKey
|
||||||
|
Object.assign(oldRecord, target)
|
||||||
|
await hostListDB.updateAsync({ _id: oldRecord._id }, oldRecord)
|
||||||
|
}
|
||||||
|
return res.success({ msg: '批量修改成功' })
|
||||||
|
}
|
||||||
|
if (!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
||||||
|
|
||||||
|
let updateRecord = {
|
||||||
|
name: newName, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
|
||||||
|
port, clientPort, username, authType, password, privateKey, credential, command, jumpHosts
|
||||||
}
|
}
|
||||||
|
|
||||||
async function batchUpdateHost({ res, request }) {
|
let oldRecord = await hostListDB.findOneAsync({ _id: id })
|
||||||
let { updateHosts } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
|
// 如果存在原认证方式则保存下来
|
||||||
if (updateHosts) {
|
if (!updateRecord[authType] && oldRecord[authType]) {
|
||||||
await updateHosts({ res, request })
|
updateRecord[authType] = oldRecord[authType]
|
||||||
} else {
|
} else {
|
||||||
return res.fail({ data: false, msg: 'Plus专属功能!' })
|
const clearTempKey = await RSADecryptAsync(tempKey)
|
||||||
|
// console.log('clearTempKey:', clearTempKey)
|
||||||
|
const clearSSHKey = await AESDecryptAsync(updateRecord[authType], clearTempKey)
|
||||||
|
// console.log(`${ authType }原密文: `, clearSSHKey)
|
||||||
|
updateRecord[authType] = await AESEncryptAsync(clearSSHKey)
|
||||||
|
// console.log(`${ authType }__commonKey加密存储: `, updateRecord[authType])
|
||||||
}
|
}
|
||||||
|
await hostListDB.updateAsync({ _id: oldRecord._id }, updateRecord)
|
||||||
|
res.success({ msg: '修改成功' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeHost({ res, request }) {
|
async function removeHost({ res, request }) {
|
||||||
let { body: { ids } } = request
|
let { body: { ids } } = request
|
||||||
if (!Array.isArray(ids)) return res.fail({ msg: '参数错误' })
|
if (!Array.isArray(ids)) return res.fail({ msg: '参数错误' })
|
||||||
const numRemoved = await hostListDB.removeAsync({ _id: { $in: ids } }, { multi: true })
|
const numRemoved = await hostListDB.removeAsync({ _id: { $in: ids } }, { multi: true })
|
||||||
|
// console.log('numRemoved: ', numRemoved)
|
||||||
res.success({ data: `已移除,数量: ${ numRemoved }` })
|
res.success({ data: `已移除,数量: ${ numRemoved }` })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +149,5 @@ module.exports = {
|
|||||||
addHost,
|
addHost,
|
||||||
updateHost,
|
updateHost,
|
||||||
removeHost,
|
removeHost,
|
||||||
importHost,
|
importHost
|
||||||
batchUpdateHost
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
const path = require('path')
|
|
||||||
const decryptAndExecuteAsync = require('../utils/decrypt-file')
|
|
||||||
const { sendServerChan, sendEmail } = require('../utils/notify')
|
const { sendServerChan, sendEmail } = require('../utils/notify')
|
||||||
const { NotifyConfigDB, NotifyDB } = require('../utils/db-class')
|
const { NotifyConfigDB, NotifyDB } = require('../utils/db-class')
|
||||||
const notifyDB = new NotifyDB().getInstance()
|
const notifyDB = new NotifyDB().getInstance()
|
||||||
@ -7,6 +5,7 @@ const notifyConfigDB = new NotifyConfigDB().getInstance()
|
|||||||
|
|
||||||
async function getNotifyConfig({ res }) {
|
async function getNotifyConfig({ res }) {
|
||||||
const data = await notifyConfigDB.findOneAsync({})
|
const data = await notifyConfigDB.findOneAsync({})
|
||||||
|
console.log(data)
|
||||||
return res.success({ data })
|
return res.success({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,7 +13,6 @@ async function updateNotifyConfig({ res, request }) {
|
|||||||
let { body: { noticeConfig } } = request
|
let { body: { noticeConfig } } = request
|
||||||
let { type } = noticeConfig
|
let { type } = noticeConfig
|
||||||
try {
|
try {
|
||||||
// console.log('noticeConfig: ', noticeConfig[type])
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'sct':
|
case 'sct':
|
||||||
await sendServerChan(noticeConfig[type]['sendKey'], 'EasyNode通知测试', '这是一条测试通知')
|
await sendServerChan(noticeConfig[type]['sendKey'], 'EasyNode通知测试', '这是一条测试通知')
|
||||||
@ -22,12 +20,6 @@ async function updateNotifyConfig({ res, request }) {
|
|||||||
case 'email':
|
case 'email':
|
||||||
await sendEmail(noticeConfig[type], 'EasyNode通知测试', '这是一条测试通知')
|
await sendEmail(noticeConfig[type], 'EasyNode通知测试', '这是一条测试通知')
|
||||||
break
|
break
|
||||||
case 'tg':
|
|
||||||
let { sendTg } = await decryptAndExecuteAsync(path.join(__dirname, '../utils/plus.js')) || {}
|
|
||||||
console.log('sendTg: ', sendTg)
|
|
||||||
if (!sendTg) return res.fail({ msg: 'Plus专属功能点,请激活Plus' })
|
|
||||||
await sendTg(noticeConfig[type], 'EasyNode通知测试', '这是一条测试通知')
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
await notifyConfigDB.update({}, { $set: noticeConfig }, { upsert: true })
|
await notifyConfigDB.update({}, { $set: noticeConfig }, { upsert: true })
|
||||||
return res.success({ msg: '测试通过 | 保存成功' })
|
return res.success({ msg: '测试通过 | 保存成功' })
|
||||||
|
@ -1 +0,0 @@
|
|||||||
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
|
|
@ -1,8 +1,6 @@
|
|||||||
const path = require('path')
|
const localShellJson = require('../config/shell.json')
|
||||||
const decryptAndExecuteAsync = require('../utils/decrypt-file')
|
|
||||||
const { randomStr } = require('../utils/tools')
|
const { randomStr } = require('../utils/tools')
|
||||||
const { ScriptsDB } = require('../utils/db-class')
|
const { ScriptsDB } = require('../utils/db-class')
|
||||||
const localShellJson = require('../config/shell.json')
|
|
||||||
const scriptsDB = new ScriptsDB().getInstance()
|
const scriptsDB = new ScriptsDB().getInstance()
|
||||||
|
|
||||||
let localShell = JSON.parse(JSON.stringify(localShellJson)).map((item) => {
|
let localShell = JSON.parse(JSON.stringify(localShellJson)).map((item) => {
|
||||||
@ -46,28 +44,10 @@ const removeScript = async ({ res, request }) => {
|
|||||||
res.success({ data: '移除成功' })
|
res.success({ data: '移除成功' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const batchRemoveScript = async ({ res, request }) => {
|
|
||||||
let { body: { ids } } = request
|
|
||||||
if (!Array.isArray(ids)) return res.fail({ msg: '参数错误' })
|
|
||||||
const numRemoved = await scriptsDB.removeAsync({ _id: { $in: ids } }, { multi: true })
|
|
||||||
res.success({ data: `批量移除成功,数量: ${ numRemoved }` })
|
|
||||||
}
|
|
||||||
|
|
||||||
const importScript = async ({ res, request }) => {
|
|
||||||
let { impScript } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
|
|
||||||
if (impScript) {
|
|
||||||
await impScript({ res, request })
|
|
||||||
} else {
|
|
||||||
return res.fail({ data: false, msg: 'Plus专属功能!' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
addScript,
|
addScript,
|
||||||
getScriptList,
|
getScriptList,
|
||||||
getLocalScriptList,
|
getLocalScriptList,
|
||||||
updateScriptList,
|
updateScriptList,
|
||||||
removeScript,
|
removeScript
|
||||||
batchRemoveScript,
|
|
||||||
importScript
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
const path = require('path')
|
|
||||||
const { RSADecryptAsync, AESEncryptAsync, AESDecryptAsync } = require('../utils/encrypt')
|
const { RSADecryptAsync, AESEncryptAsync, AESDecryptAsync } = require('../utils/encrypt')
|
||||||
const { HostListDB, CredentialsDB } = require('../utils/db-class')
|
const { HostListDB, CredentialsDB } = require('../utils/db-class')
|
||||||
const decryptAndExecuteAsync = require('../utils/decrypt-file')
|
|
||||||
const hostListDB = new HostListDB().getInstance()
|
const hostListDB = new HostListDB().getInstance()
|
||||||
const credentialsDB = new CredentialsDB().getInstance()
|
const credentialsDB = new CredentialsDB().getInstance()
|
||||||
|
|
||||||
async function getSSHList({ res }) {
|
async function getSSHList({ res }) {
|
||||||
|
// console.log('get-host-list')
|
||||||
let data = await credentialsDB.findAsync({})
|
let data = await credentialsDB.findAsync({})
|
||||||
data = data?.map(item => {
|
data = data?.map(item => {
|
||||||
const { name, authType, _id: id, date } = item
|
const { name, authType, _id: id, date } = item
|
||||||
@ -84,19 +83,10 @@ const getCommand = async ({ res, request }) => {
|
|||||||
let hostInfo = await hostListDB.findAsync({})
|
let hostInfo = await hostListDB.findAsync({})
|
||||||
let record = hostInfo?.find(item => item._id === hostId)
|
let record = hostInfo?.find(item => item._id === hostId)
|
||||||
consola.info('查询登录后执行的指令:', hostId)
|
consola.info('查询登录后执行的指令:', hostId)
|
||||||
if (!record) return res.fail({ data: false, msg: 'host not found' })
|
if (!record) return res.fail({ data: false, msg: 'host not found' }) // host不存在
|
||||||
const { command } = record
|
const { command } = record
|
||||||
if (!command) return res.success({ data: false })
|
if (!command) return res.success({ data: false }) // command不存在
|
||||||
res.success({ data: command })
|
res.success({ data: command }) // 存在
|
||||||
}
|
|
||||||
|
|
||||||
const decryptPrivateKey = async ({ res, request }) => {
|
|
||||||
let { dePrivateKey } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
|
|
||||||
if (dePrivateKey) {
|
|
||||||
await dePrivateKey({ res, request })
|
|
||||||
} else {
|
|
||||||
return res.fail({ data: false, msg: 'Plus专属功能,无法解密私钥!' })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -104,6 +94,5 @@ module.exports = {
|
|||||||
addSSH,
|
addSSH,
|
||||||
updateSSH,
|
updateSSH,
|
||||||
removeSSH,
|
removeSSH,
|
||||||
getCommand,
|
getCommand
|
||||||
decryptPrivateKey
|
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,10 @@ const jwt = require('jsonwebtoken')
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const speakeasy = require('speakeasy')
|
const speakeasy = require('speakeasy')
|
||||||
const QRCode = require('qrcode')
|
const QRCode = require('qrcode')
|
||||||
const version = require('../../package.json').version
|
|
||||||
const getLicenseInfo = require('../utils/get-plus')
|
|
||||||
const { plusServer1, plusServer2 } = require('../utils/plus-server')
|
|
||||||
const { sendNoticeAsync } = require('../utils/notify')
|
const { sendNoticeAsync } = require('../utils/notify')
|
||||||
const { RSADecryptAsync, AESEncryptAsync, SHA1Encrypt } = require('../utils/encrypt')
|
const { RSADecryptAsync, AESEncryptAsync, SHA1Encrypt } = require('../utils/encrypt')
|
||||||
const { getNetIPInfo } = require('../utils/tools')
|
const { getNetIPInfo } = require('../utils/tools')
|
||||||
const { KeyDB, LogDB, PlusDB } = require('../utils/db-class')
|
const { KeyDB, LogDB, PlusDB } = require('../utils/db-class')
|
||||||
|
|
||||||
const keyDB = new KeyDB().getInstance()
|
const keyDB = new KeyDB().getInstance()
|
||||||
const logDB = new LogDB().getInstance()
|
const logDB = new LogDB().getInstance()
|
||||||
const plusDB = new PlusDB().getInstance()
|
const plusDB = new PlusDB().getInstance()
|
||||||
@ -90,7 +86,7 @@ const beforeLoginHandler = async (clientIp, jwtExpires) => {
|
|||||||
let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token
|
let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token
|
||||||
token = await AESEncryptAsync(token) // 对称加密token后再传输给前端
|
token = await AESEncryptAsync(token) // 对称加密token后再传输给前端
|
||||||
|
|
||||||
// 记录客户端登录IP(用于判断是否异地且只保留最近10<EFBFBD><EFBFBD>)
|
// 记录客户端登录IP(用于判断是否异地且只保留最近10条)
|
||||||
const clientIPInfo = await getNetIPInfo(clientIp)
|
const clientIPInfo = await getNetIPInfo(clientIp)
|
||||||
const { ip, country, city } = clientIPInfo || {}
|
const { ip, country, city } = clientIPInfo || {}
|
||||||
consola.info('登录成功:', new Date(), { ip, country, city })
|
consola.info('登录成功:', new Date(), { ip, country, city })
|
||||||
@ -176,40 +172,6 @@ const getPlusInfo = async ({ res }) => {
|
|||||||
res.success({ data, msg: 'success' })
|
res.success({ data, msg: 'success' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPlusDiscount = async ({ res } = {}) => {
|
|
||||||
if (process.env.EXEC_ENV === 'local') return res.success({ discount: false })
|
|
||||||
const servers = [plusServer1, plusServer2]
|
|
||||||
for (const server of servers) {
|
|
||||||
try {
|
|
||||||
const url = `${ server }/api/announcement/public?version=${ version }`
|
|
||||||
const response = await fetch(url)
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${ response.status }`)
|
|
||||||
}
|
|
||||||
const data = await response.json()
|
|
||||||
return res.success({ data, msg: 'success' })
|
|
||||||
} catch (error) {
|
|
||||||
if (server === servers[servers.length - 1]) {
|
|
||||||
consola.error('All servers failed:', error.message)
|
|
||||||
return res.success({ discount: false })
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPlusConf = async ({ res }) => {
|
|
||||||
const { key } = await plusDB.findOneAsync({}) || {}
|
|
||||||
res.success({ data: key || '', msg: 'success' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePlusKey = async ({ res, request }) => {
|
|
||||||
const { body: { key } } = request
|
|
||||||
const { success, msg } = await getLicenseInfo(key)
|
|
||||||
if (!success) return res.fail({ msg })
|
|
||||||
res.success({ msg: 'success' })
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
login,
|
login,
|
||||||
getpublicKey,
|
getpublicKey,
|
||||||
@ -219,8 +181,5 @@ module.exports = {
|
|||||||
getMFA2Code,
|
getMFA2Code,
|
||||||
enableMFA2,
|
enableMFA2,
|
||||||
disableMFA2,
|
disableMFA2,
|
||||||
getPlusInfo,
|
getPlusInfo
|
||||||
getPlusDiscount,
|
|
||||||
getPlusConf,
|
|
||||||
updatePlusKey
|
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,8 @@ async function initNotifyDB() {
|
|||||||
|
|
||||||
async function initNotifyConfigDB() {
|
async function initNotifyConfigDB() {
|
||||||
const notifyConfigDB = new NotifyConfigDB().getInstance()
|
const notifyConfigDB = new NotifyConfigDB().getInstance()
|
||||||
let notifyConfig = await notifyConfigDB.findOneAsync({})
|
let count = await notifyConfigDB.countAsync({})
|
||||||
|
if (count !== 0) return
|
||||||
consola.log('初始化NotifyConfigDB✔')
|
consola.log('初始化NotifyConfigDB✔')
|
||||||
const defaultData = {
|
const defaultData = {
|
||||||
type: 'sct',
|
type: 'sct',
|
||||||
@ -82,17 +83,8 @@ async function initNotifyConfigDB() {
|
|||||||
service: 'QQ',
|
service: 'QQ',
|
||||||
user: '',
|
user: '',
|
||||||
pass: ''
|
pass: ''
|
||||||
},
|
|
||||||
tg: {
|
|
||||||
token: '',
|
|
||||||
chatId: ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (notifyConfig) {
|
|
||||||
await notifyConfigDB.removeAsync({ _id: notifyConfig._id })
|
|
||||||
delete notifyConfig._id
|
|
||||||
return notifyConfigDB.insertAsync(Object.assign({}, defaultData, notifyConfig))
|
|
||||||
}
|
|
||||||
return notifyConfigDB.insertAsync(defaultData)
|
return notifyConfigDB.insertAsync(defaultData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand, decryptPrivateKey } = require('../controller/ssh')
|
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand } = require('../controller/ssh')
|
||||||
const { getHostList, addHost, updateHost, batchUpdateHost, removeHost, importHost } = require('../controller/host')
|
const { getHostList, addHost, updateHost, removeHost, importHost } = require('../controller/host')
|
||||||
const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMFA2Code, enableMFA2, disableMFA2, getPlusInfo, getPlusDiscount, getPlusConf, updatePlusKey } = require('../controller/user')
|
const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMFA2Code, enableMFA2, disableMFA2, getPlusInfo } = require('../controller/user')
|
||||||
const { getNotifyConfig, updateNotifyConfig, getNotifyList, updateNotifyList } = require('../controller/notify')
|
const { getNotifyConfig, updateNotifyConfig, getNotifyList, updateNotifyList } = require('../controller/notify')
|
||||||
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
||||||
const { getScriptList, getLocalScriptList, addScript, updateScriptList, removeScript, batchRemoveScript, importScript } = require('../controller/scripts')
|
const { getScriptList, getLocalScriptList, addScript, updateScriptList, removeScript } = require('../controller/scripts')
|
||||||
const { getOnekeyRecord, removeOnekeyRecord } = require('../controller/onekey')
|
const { getOnekeyRecord, removeOnekeyRecord } = require('../controller/onekey')
|
||||||
const { getLog } = require('../controller/log')
|
const { getLog } = require('../controller/log')
|
||||||
|
|
||||||
@ -32,11 +32,6 @@ const ssh = [
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/command',
|
path: '/command',
|
||||||
controller: getCommand
|
controller: getCommand
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/decrypt-private-key',
|
|
||||||
controller: decryptPrivateKey
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
const host = [
|
const host = [
|
||||||
@ -55,11 +50,6 @@ const host = [
|
|||||||
path: '/host-save',
|
path: '/host-save',
|
||||||
controller: updateHost
|
controller: updateHost
|
||||||
},
|
},
|
||||||
{
|
|
||||||
method: 'put',
|
|
||||||
path: '/batch-update-host',
|
|
||||||
controller: batchUpdateHost
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/host-remove',
|
path: '/host-remove',
|
||||||
@ -116,21 +106,6 @@ const user = [
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/plus-info',
|
path: '/plus-info',
|
||||||
controller: getPlusInfo
|
controller: getPlusInfo
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/plus-discount',
|
|
||||||
controller: getPlusDiscount
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/plus-conf',
|
|
||||||
controller: getPlusConf
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/plus-conf',
|
|
||||||
controller: updatePlusKey
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
const notify = [
|
const notify = [
|
||||||
@ -200,20 +175,10 @@ const scripts = [
|
|||||||
path: '/script/:id',
|
path: '/script/:id',
|
||||||
controller: removeScript
|
controller: removeScript
|
||||||
},
|
},
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/batch-remove-script',
|
|
||||||
controller: batchRemoveScript
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
method: 'put',
|
method: 'put',
|
||||||
path: '/script/:id',
|
path: '/script/:id',
|
||||||
controller: updateScriptList
|
controller: updateScriptList
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/import-script',
|
|
||||||
controller: importScript
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
U2FsdGVkX1+P63MsIfF2N9/XM16sWy0/pZMWy+0Ptf+FhySv/AiawI4Pcf/HQU7Auxde+GszGb7+t+i1Ckngo6VK9PkwALR87GbqCJtGeMazZTkEGkmNuePdpej0O3oAuwITI1FOKPW4Xe4RIFAkJfghqCgUD0Ps0Y6sPwIxOX7fTi4TopNksRMQ5X+UvezrGnPsF5EC2CAPmKwtRFWVqx5csAFhifvMxwEA+WCA7l9KLLvcybGtY4RZf0uSLb6qGxrJBN/zbQ1MMxVw9JbUML09uQ3VKLvQMJZmjctIpZDr4YEMMMdDD/qDk64feV8Tc5VPENsyl2i9kxZ4Z3s5pUu6oDm+/GE2ag0OMITgg7Wc6QpeqlWJwgAeGuqxz2nnuQfGbhHv4g800Hwc7C6ylYgHHeSY+gx39PyDQy1tE7vtW/83ZQUSgWRXxMYTMHUgYKh6P3XG+HxJz6vRjpZqwjPIc3jd253EQnHVG4YZ7VxjBFwpcmidnkvKMa+dvQSZypOL3XLKlMSGdMWFbtVuw4MMYnTBadWkS3eekdVFtvpG5NRqga0TBxPeoISLsn717u9BYcROHvPzvX/MKG7S9CGClZb4mYbOPxKmENPT6AvQbCrzOlK6X/kHTLOxivc2O5uzL4CRXBKFDeaAUJqs/PLZuCfvdmMBKPiMQpCLtFBYCXoxDarggu2D8EZYcoxFPJ/YE7sy/bFa52hQ0V1wYXkhwez1Q4Q3PH1dqdzOESOI30KEOk9TwkEhdV69ymZ6rb6cXWy1KOph2Vo8dQhgNItgvNCCyEuojR54eXnh8x7R1FHmITrobfRZZOYmpFFRZntirTpkEDvt8sPR7G+STcfKc+OgNVoCSV2Ca79Ex0BVACRxWTrafC+VX80mQGldt3wwTk2P+7mcTl0NqH6F7hfzaDjHLy/pbd/78lJLToMz7K1/PaPfOAlVsD5MTh2hefAmqTDZRKfBHyQQjwgh2hNqpmzP2tcQqO5qeigk3fzvZjyXAX2Zqu85hc6QI0Quc4zRS1hb0uY93mnOLnOksTgCNPZHDCTmzT7N0v4D/oABq2VyFFDIvMlzy1kD1WnpmhPldfm+J268QnGfMoR7ob6quZB660xc3V/9zN16ZeVKAhFfzwVHApubxTOYtIIQWLzGj9Q2eqYCXf2p3n8CYF8YV3L1LT6FxEK//MOBXZZbf7JC7tkSWb0EdRf2wuxlDocrxXzSTuIFlpBuG4Dcjf9wVCn3NuQJhqVAdxSX98K/sJpKYPELyYwTvSwG8v2MApAAgv7v+NfgSswXGCVlYpeildrVM4AafoZjBVb4OJK5YvDgmhyhzKP8copwAwi2/tGK8x/fiT0oTQhR60c7wZBuaj3/D0uDxNCcRct5/DYvb3dU1qMDmPfw0BNE7xhHouVLM12tdSnZxxFWgq5747aOpD+yYfgQBxToXsAbfEZLZLfRXPqcNjJWdnY1ZlqARyrzFKbBlGu/HhdEpldDxhdOFLkjytrA7NzvKJKfWvxbtv+jkVYL6SNdLfRoaUe0ecPJH/Gq12FhRXXudPRQT/xqQ5TXCJZ41gkmd7B9oMuVrmpDhaSKkX1loSj/Mhezzq5vIrZPd3+qK7wMeFrxvY7E4wIlK1gZdyQXBzUh8VPQzY6KFEDV4fekqkxkdKm4oX8Ij3IJfuX52+lIJdhL3t3p/j7mLLc1L9hvRudgWnujLEOi8syJv8EkKARC9OmO0S1okf1R805fenbS6MiM7zoFqza6iNn/9uJwgzCjxoTqpLoPwU2OYQyN3h4LnRoV5qsCo/RUunrdkAAQp85iTMSTWm0ux9R/jy+IHe62zwfrCq9xsdqMZ/0MZPRAyGIh5Cr0JbhXwT5s8meI5MgjJ107+hfSdo0ufSqGnceTfvfNVGUt8KO2PWFwgb7fyAiirnIL53lgKOo8dD4tDVZjfClGdIG7KJFty5M3rzyCyPl5ZeQJRD6YZVZG3HPFt4BDPDecPMK63zvrX9zSK3FV5GyBgZQQVMQbou+gpnGdSGw3NI0agN47hRYjFsbvGPrVjBKiPrfnBKkd2lyqKvd2cns/yYmgGXtehYasnPLU01dvRHicQ+TduvMR4NYMCytiDbiKqwBaYI+yt7cKNSf4cQQT9b3DD8pl9dQhzrnAY0tHxSLCVZzl7LCbYzaITQRLzBoIzCmPrFGLj6j2HuGc+gQ3Hd2/HisVAYxbW6Fu0SXQa9nnGW7k+3AJFRs5EvF++QffNW2rMYaBqr+c1jfwURzc7dGD8PNtDJkx3T6eSKx9l4iWR5/LDW+SXrk2OCzMdznsBNCJJbaKzUqs1HlIEuwQ/hHMjk4M+f4/SmbECE3cQXd+nseEoUnKbn/MiWi2lNyjacXuLTtrjDITK9jIZD0Ixfci5Eer2fuPJw7RXr1rb8bjlZWttpjN45NORLB8usjiiEdklPjQMUmTjXgALATorkWt6vc76zB4Fbk5jOwJ53+W1RqvUxt5S+WIhH55jOxYHqrEppguVMw1w4RIceaL/CYunwHxiD5w58hjJ4bR5fXjwARwM/EDGHgPvfV0dDxhBxbBtKrcemUYlBI40wwbOuDgfdnfcJNVCxaVX0Hto/VuqXvfP3eoQT2Vasz96y2/GbSS0rBGtNn9EzPwGZvBrumbl4ezQJp+HjSN65GLXjDNY+ZajGaOs+pK1O8ooAqODnfAhPd3LkMxQxlkqNy8l4KFUPA1lqd8ohK+4UThmfRCQDKPLTdFV/oTvEdBo0d/oN3xJM9ValH+0e3w86b+cmXnLzUxUzeIhmXWW8ctGtNmzV2fewh8ChPl75MCU5d88uzC03YbfZHOOtokTqkhLYhz4il7KSnA+EFEWB7GOvVqnkM/LAwDcJ/1qvqBah+WnDs9uQXTF+QXn/N1q/+83lu9JQWK0crV2mTJHk8efTHBn6oEN8P9pXf2BCJQzbukA/q50QvQU33p8P2VOqL/bHwsaolIhWrys5lyM4pwpMqxvnP5YIFjEhGhcCHLtqL5jvETh3X78HHiEhjmNzwtj3wa/NrgvzJhepqWWGGBK1DEYoMj21cFPpUeB/+2
|
U2FsdGVkX19h0XaqdvQ7zFFD/TieCzyGSDBxYljl4nGOA6++kCE+P0pkq1kyGUWF93m4jgaEUSx1dIdzYZEFREw96lT4zCmqOIVDTCvIQX9XdmevpspeCIYsOqdHQhtqDDq15lay5awRd1m3VuXuXTo10DGgDIprcYV3JfBAsmVxORpxoE8VCYKxU6lEvArgHeiCobK/jI1Xf2+kS1Ehyq0ya9haTkz6/XqctZq9AEUY2NxTjsOp4FJ5iYDrFXvT5Tv58JFAysuN2Nq9rrkUZl2MjFY975xQ19JBwKsMoSt2UuBpJDDJJ6izgswtpSYRE3m2uGbkPEnDc7ThtOqc+KcTORvizP3WnpQcF85ouhzPSW7RTAIxSatIrirWpv0iv2hI7ur+ue2Z9tfKJPqTfVFs3cCescWf90mFTfCiZqgdKNLeV02hY5SvKJ6/6Aotynwhc0a+kwvh2d9b3BMtQE1cTlz9XROmdIDoJTxTlblYiax8wLMz9mRzkG3Pe9h51gmaj88lk00HeUJOoD49Qd7wcIleqjotMuMWZuU3E9TCRcVrUj6XNBa3JFNE5WwF5YvcYijYHVhyBxFX0hZfuvezE8fMG3II26HiZvZE5497hJ+MtKectqoWBByMyXyhi3GVuJ2RSjKEIh8F6FEPcJFaCAWpBIoj5WK6Hvq/K3wPfxD9gA40OmCySwyY5NsNUxXuqeUp+cMwa5UXsmXBCmV7hn6ov/jJDSYV9+x1hX5RZ6eborR2fD9UlLUd6tOTeopYoKqR7x+RXy+JQAVOn+mJEZkr2GL7qLoawni7PRKm0XcRmRCXEAYzN65+OEjuQnFVNGWHwRo5qB+FfTl6095DmTiGNm9HIwTN0DaGLw8S+s2wniCRiCh75xBy4vM5GAOIq5qDDmPwAb30TY22qJMvLx5AgnFmSrcqVspUmIg8KCxvM36xsn07lwvjNMt+Fe+yuVPrsJ2X6jC/FmJb/q9rxPPLxatdFGm00oQVoLvdSiLuJ7rGNUCt6TsIhoqzo3mU8VUUDBIqavv6SGItd2w8PjDuVTCV6wxgufv1qSG0PvESf1hxaqv09JPQX05hKXKDjXmzI1J2sDplkKLeerUnbFG0/JTJHxCSLzd2+pPyX8BsjFJdtLOj2DhFUi3NMNaU7vF7sPT+sDFde4c6Pm8EnQQziN10VSLGJbwCu3KUVGIPW4/6BMINzcGK+uuP24JJdFZWeUKCB8N8I8nxS71DuCkdeb6L6Y7S44a19KO05eLNJ7dtksWdDsnQDbmlByou0/tFyNC55NMHt4gZbq/HBAGDcMoCfJCLMk9RlmUzIeV2eVD+pDTMI09uC+/q88yB29FK/2q4eIq27iN5qwtnIsy85GdKAmiuTXc5yVIqmJAj+cOJdrgztCQb4XV1sxW0pCKni+ciEZHdeNJF/zI4KYbGuwTwZFUxqzVIroSbUeDb8R1VQKs5riRyWuPShInWuNgSWOwrC37EMVI06r9CCggTOua0PdBXiy3vHEKc33NMYEcex6iNU77dHZavYpyOPsnGZicFNXkT1BtgeXirfviR7SOIlTcOEWwJobLVOid9yF6qdEkps7BtrRJ2DvpekUQxR7vKPSmywnXEm19J10tYf0RdROoUrMVj7KuRh4pEA4EG6Rgb+SvJKqaPjIe49hMoXHFJYrZzCCP2cGuv2g0kwlJVtV3H0Q3SW/nraJsVK+D+ljtREvztnYuobMxCBLmRu3OCQhLsWTIUWQ2S1vPqIrL/wdN64wJBbsOBKeYTpAWYRW0CvZWf5zvtcqoC03H4g/XUnkh6UQwW94FxBk1zViOjGJw7FUk/NTaV4eCtezKeOxYpJgNIHdrZ91XrFPcySikwHn2puw3Xk42iSzmROj3MKZrW05myzidpb7CcP6CmBzd2HqtBcolliB5jIgCGLUndXf/peJxqsCymkNrQoHWrv/P9o9t7hQL5rGR1zfRHELaz4+GbLBS+wH1ogaQjAFUtcsIKixrtmGj5OBz3RHMDNZ/lHi19ZKDDC5KxQbxQqXWC7zaFi4ZLwFm5Y4Ml09aB9PqbWhJw2t2hDLE7WIhhPuNXqgBzWuyYY8ZIbIKIohB/DX2LMt3aZoZFBguR7kqa+6o/VPm7ofKbcxYt8RnackncFy46kTleX8SdgcvGIOUhUd1YDyPKWHEKlpdc7JcYm3L/Ywgj42DCdWHNQ2JGvlgMgYtbW4m6qcTZmq0MZAQG33ypnMFzZmGL3Wbu1jm5OFzH6dNt3qsENXWPhuULiqtLk6REpsqzleV5pRDPc9O4t6uQaDMeb3iGBJxAf5bDpvVYkCnpCc6C5d6TDsdNnQtpUBHdYGpBZFl5/xMsjw5r5KpYFayQ90eN9sBQqlpM2l6d/gFA1amDJyjd9kjEmT+s1mXYlW/smpxVeV1HCmtrEEGIpCjpHb2ugFpEpVQFW2RAb8vw+Gx9htsPedPofGD+oMrQjIy+uz249Q8rCx/k5DQMXqqDMTO8LYVOSFVElkYgCYbR1QoCnw0z6aCPzu0Kk7SipFUuui9g7bkYAXoHVdlooosbNfbVUintXraDqigAY7s+41/LluRBZnwntTld0qPaioFwHPysputYqdpO4EzT+azRES92bGOhxFQEABzX3moF6Lz/l82JXuSef1q26yMuG3sofVY0NIAI0wG1SY6DSSctmSYgH4pwnhB0OXKzvhcMB8RlBJUBJTJ+RkgnXjqUXXBzzb56huobw6OqVYWir0yG8gZl9o+3GR9GBR4o0onZM0Nl1+gLd7gaIf2yetUpZ/9JqucTpYCwfW5cvRO11qQMIyWMpkPuEmYQb3ZJ6P5vHFZaMOa15qomLOcOREfYGb226YyWOPK76neyJwenKbwk9DNlTe2nj+4WZBXFv2HpI55ysmmo1CdJkmNm0OD28lLhBj06khLw407EfuVIGBK3gD2qo+Mg1HT3T5TstaE8JYtICdVdgkHEejuepaAPZqKNoDY5kHkp0exlRmMNFU4+q/Gz3zgSuUS5G9DMzcTvXp9OaSxkMKYaSg4I9TIXgFAdHy5whbpCR6Evx4tTxaXh
|
@ -224,18 +224,18 @@ module.exports = (httpServer) => {
|
|||||||
let sftpClient = new SFTPClient()
|
let sftpClient = new SFTPClient()
|
||||||
consola.success('terminal websocket 已连接')
|
consola.success('terminal websocket 已连接')
|
||||||
|
|
||||||
socket.on('create', async ({ hostId, token }) => {
|
socket.on('create', async ({ host: ip, token }) => {
|
||||||
const { code } = await verifyAuthSync(token, requestIP)
|
const { code } = await verifyAuthSync(token, requestIP)
|
||||||
consola.log('code:', code)
|
|
||||||
if (code !== 1) {
|
if (code !== 1) {
|
||||||
socket.emit('token_verify_fail')
|
socket.emit('token_verify_fail')
|
||||||
socket.disconnect()
|
socket.disconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const targetHostInfo = await hostListDB.findOneAsync({ _id: hostId })
|
|
||||||
if (!targetHostInfo) throw new Error(`Host with ID ${ hostId } not found`)
|
const hostList = await hostListDB.findAsync({})
|
||||||
|
const targetHostInfo = hostList.find(item => item.host === ip) || {}
|
||||||
let { authType, host, port, username } = targetHostInfo
|
let { authType, host, port, username } = targetHostInfo
|
||||||
if (!host) return socket.emit('create_fail', `查找id【${ hostId }】凭证信息失败`)
|
if (!host) return socket.emit('create_fail', `查找【${ ip }】凭证信息失败`)
|
||||||
let authInfo = { host, port, username }
|
let authInfo = { host, port, username }
|
||||||
|
|
||||||
// 解密放到try里面,防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】
|
// 解密放到try里面,防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】
|
||||||
|
@ -52,11 +52,15 @@ async function createTerminal(hostId, socket, targetSSHClient) {
|
|||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
const targetHostInfo = await hostListDB.findOneAsync({ _id: hostId })
|
const targetHostInfo = await hostListDB.findOneAsync({ _id: hostId })
|
||||||
if (!targetHostInfo) return socket.emit('create_fail', `查找hostId【${ hostId }】凭证信息失败`)
|
if (!targetHostInfo) return socket.emit('create_fail', `查找hostId【${ hostId }】凭证信息失败`)
|
||||||
let { connectByJumpHosts = null } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
|
let connectByJumpHosts = null
|
||||||
|
let data = await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))
|
||||||
|
if (data) {
|
||||||
|
connectByJumpHosts = data.connectByJumpHosts
|
||||||
|
}
|
||||||
let { authType, host, port, username, name, jumpHosts } = targetHostInfo
|
let { authType, host, port, username, name, jumpHosts } = targetHostInfo
|
||||||
try {
|
try {
|
||||||
let { authInfo: targetConnectionOptions } = await getConnectionOptions(hostId)
|
let { authInfo: targetConnectionOptions } = await getConnectionOptions(hostId)
|
||||||
let jumpHostResult = connectByJumpHosts && (await connectByJumpHosts(jumpHosts, targetConnectionOptions.host, targetConnectionOptions.port, socket))
|
let jumpHostResult = connectByJumpHosts && await connectByJumpHosts(jumpHosts, targetConnectionOptions.host, targetConnectionOptions.port, socket)
|
||||||
if (jumpHostResult) {
|
if (jumpHostResult) {
|
||||||
targetConnectionOptions.sock = jumpHostResult.sock
|
targetConnectionOptions.sock = jumpHostResult.sock
|
||||||
}
|
}
|
||||||
@ -88,14 +92,11 @@ async function createTerminal(hostId, socket, targetSSHClient) {
|
|||||||
consola.error('连接终端失败:', host, err.message)
|
consola.error('连接终端失败:', host, err.message)
|
||||||
socket.emit('connect_terminal_fail', err.message)
|
socket.emit('connect_terminal_fail', err.message)
|
||||||
})
|
})
|
||||||
.on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) {
|
|
||||||
finish([targetConnectionOptions[authType]])
|
|
||||||
})
|
|
||||||
.connect({
|
.connect({
|
||||||
tryKeyboard: true,
|
|
||||||
...targetConnectionOptions
|
...targetConnectionOptions
|
||||||
// debug: (info) => console.log(info)
|
// debug: (info) => console.log(info)
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
consola.error('创建终端失败: ', host, err.message)
|
consola.error('创建终端失败: ', host, err.message)
|
||||||
socket.emit('create_terminal_fail', err.message)
|
socket.emit('create_terminal_fail', err.message)
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
|
const schedule = require('node-schedule')
|
||||||
const { getLocalNetIP } = require('./tools')
|
const { getLocalNetIP } = require('./tools')
|
||||||
const { AESEncryptAsync } = require('./encrypt')
|
const { AESEncryptAsync } = require('./encrypt')
|
||||||
const version = require('../../package.json').version
|
const version = require('../../package.json').version
|
||||||
const { plusServer1, plusServer2 } = require('./plus-server')
|
|
||||||
const { PlusDB } = require('./db-class')
|
|
||||||
const plusDB = new PlusDB().getInstance()
|
|
||||||
|
|
||||||
async function getLicenseInfo(key = '') {
|
async function getLicenseInfo() {
|
||||||
const { key: plusKey } = await plusDB.findOneAsync({}) || {}
|
let key = process.env.PLUS_KEY
|
||||||
// console.log('plusKey: ', plusKey)
|
if (!key || typeof key !== 'string' || key.length < 20) return
|
||||||
// console.log('key: ', key)
|
|
||||||
// console.log('process.env.PLUS_KEY: ', process.env.PLUS_KEY)
|
|
||||||
key = key || plusKey || process.env.PLUS_KEY
|
|
||||||
if (!key || key.length < 16) return { success: false, msg: 'Invalid Plus Key' }
|
|
||||||
let ip = ''
|
let ip = ''
|
||||||
if (global.serverIp && (Date.now() - global.getServerIpLastTime) / 1000 / 60 < 60) {
|
if (global.serverIp && (Date.now() - global.getServerIpLastTime) / 1000 / 60 < 60) {
|
||||||
ip = global.serverIp
|
ip = global.serverIp
|
||||||
@ -25,7 +19,7 @@ async function getLicenseInfo(key = '') {
|
|||||||
if (!ip) {
|
if (!ip) {
|
||||||
consola.error('activate plus failed: get public ip failed')
|
consola.error('activate plus failed: get public ip failed')
|
||||||
global.serverIp = ''
|
global.serverIp = ''
|
||||||
return { success: false, msg: 'get public ip failed' }
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let response
|
let response
|
||||||
@ -34,7 +28,7 @@ async function getLicenseInfo(key = '') {
|
|||||||
let headers = { 'Content-Type': 'application/json' }
|
let headers = { 'Content-Type': 'application/json' }
|
||||||
let timeout = 10000
|
let timeout = 10000
|
||||||
try {
|
try {
|
||||||
response = await fetch(plusServer1 + '/api/licenses/activate', {
|
response = await fetch('https://en1.221022.xyz/api/licenses/activate', {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
body,
|
body,
|
||||||
@ -47,7 +41,7 @@ async function getLicenseInfo(key = '') {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
consola.log('retry to activate plus by backup server')
|
consola.log('retry to activate plus by backup server')
|
||||||
response = await fetch(plusServer2 + '/api/licenses/activate', {
|
response = await fetch('https://en2.221022.xyz/api/licenses/activate', {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
body,
|
body,
|
||||||
@ -69,7 +63,9 @@ async function getLicenseInfo(key = '') {
|
|||||||
let { decryptKey, expiryDate, usedIPCount, maxIPs, usedIPs } = data
|
let { decryptKey, expiryDate, usedIPCount, maxIPs, usedIPs } = data
|
||||||
decryptKey = await AESEncryptAsync(decryptKey)
|
decryptKey = await AESEncryptAsync(decryptKey)
|
||||||
consola.success('activate plus success')
|
consola.success('activate plus success')
|
||||||
|
const { PlusDB } = require('./db-class')
|
||||||
const plusData = { key, decryptKey, expiryDate, usedIPCount, maxIPs, usedIPs }
|
const plusData = { key, decryptKey, expiryDate, usedIPCount, maxIPs, usedIPs }
|
||||||
|
const plusDB = new PlusDB().getInstance()
|
||||||
let count = await plusDB.countAsync({})
|
let count = await plusDB.countAsync({})
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
await plusDB.insertAsync(plusData)
|
await plusDB.insertAsync(plusData)
|
||||||
@ -77,17 +73,21 @@ async function getLicenseInfo(key = '') {
|
|||||||
await plusDB.removeAsync({}, { multi: true })
|
await plusDB.removeAsync({}, { multi: true })
|
||||||
await plusDB.insertAsync(plusData)
|
await plusDB.insertAsync(plusData)
|
||||||
}
|
}
|
||||||
return { success: true, msg: '激活成功' }
|
|
||||||
}
|
}
|
||||||
consola.error('activate plus failed: ', data)
|
|
||||||
return { success: false, msg: '激活失败' }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
consola.error(`activate plus failed: ${ error.message || error.errMsg?.message }`)
|
consola.error(`activate plus failed: ${ error.message || error.errMsg?.message }`)
|
||||||
if (error.clear) {
|
if (error.clear) {
|
||||||
|
const { PlusDB } = require('./db-class')
|
||||||
|
const plusDB = new PlusDB().getInstance()
|
||||||
await plusDB.removeAsync({}, { multi: true })
|
await plusDB.removeAsync({}, { multi: true })
|
||||||
}
|
}
|
||||||
return { success: false, msg: error.message || error.errMsg?.message }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const randomHour = Math.floor(Math.random() * 24)
|
||||||
|
const randomMinute = Math.floor(Math.random() * 60)
|
||||||
|
const randomDay = Math.floor(Math.random() * 7)
|
||||||
|
const cronExpression = `${ randomMinute } ${ randomHour } * * ${ randomDay }`
|
||||||
|
schedule.scheduleJob(cronExpression, getLicenseInfo)
|
||||||
|
|
||||||
module.exports = getLicenseInfo
|
module.exports = getLicenseInfo
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
const path = require('path')
|
|
||||||
const decryptAndExecuteAsync = require('./decrypt-file')
|
|
||||||
const nodemailer = require('nodemailer')
|
const nodemailer = require('nodemailer')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const commonTemp = require('../template/commonTemp')
|
const commonTemp = require('../template/commonTemp')
|
||||||
@ -8,6 +6,7 @@ const notifyConfigDB = new NotifyConfigDB().getInstance()
|
|||||||
const notifyDB = new NotifyDB().getInstance()
|
const notifyDB = new NotifyDB().getInstance()
|
||||||
|
|
||||||
function sendServerChan(sendKey, title, content) {
|
function sendServerChan(sendKey, title, content) {
|
||||||
|
if (!sendKey) return consola.error('发送server酱通知失败, sendKey 为空')
|
||||||
return new Promise((async (resolve, reject) => {
|
return new Promise((async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
consola.info('server酱通知预发送: ', title)
|
consola.info('server酱通知预发送: ', title)
|
||||||
@ -29,6 +28,7 @@ function sendServerChan(sendKey, title, content) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendEmail({ service, user, pass }, title, content) {
|
function sendEmail({ service, user, pass }, title, content) {
|
||||||
|
if (!service || !user || !pass) return consola.info('发送通知失败, 邮箱配置信息不完整: ', { service, user, pass })
|
||||||
return new Promise((async (resolve, reject) => {
|
return new Promise((async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
consola.info('邮箱通知预发送: ', title)
|
consola.info('邮箱通知预发送: ', title)
|
||||||
@ -60,8 +60,8 @@ async function sendNoticeAsync(noticeAction, title, content) {
|
|||||||
try {
|
try {
|
||||||
let notifyList = await notifyDB.findAsync({})
|
let notifyList = await notifyDB.findAsync({})
|
||||||
let { sw } = notifyList.find((item) => item.type === noticeAction) // 获取对应动作的通知开关
|
let { sw } = notifyList.find((item) => item.type === noticeAction) // 获取对应动作的通知开关
|
||||||
// console.log('notify swtich: ', noticeAction, sw)
|
console.log('notify swtich: ', noticeAction, sw)
|
||||||
if (!sw) return consola.info('通知开关关闭, 不发送通知: ', noticeAction)
|
if (!sw) return
|
||||||
let notifyConfig = await notifyConfigDB.findOneAsync({})
|
let notifyConfig = await notifyConfigDB.findOneAsync({})
|
||||||
let { type } = notifyConfig
|
let { type } = notifyConfig
|
||||||
if (!type) return consola.error('通知类型不存在: ', type)
|
if (!type) return consola.error('通知类型不存在: ', type)
|
||||||
@ -78,15 +78,7 @@ async function sendNoticeAsync(noticeAction, title, content) {
|
|||||||
if (!service || !user || !pass) return consola.info('未发送邮件通知通知, 未配置邮箱: ', { service, user, pass })
|
if (!service || !user || !pass) return consola.info('未发送邮件通知通知, 未配置邮箱: ', { service, user, pass })
|
||||||
await sendEmail({ service, user, pass }, title, content)
|
await sendEmail({ service, user, pass }, title, content)
|
||||||
break
|
break
|
||||||
case 'tg':
|
|
||||||
let { token, chatId } = notifyConfig['tg']
|
|
||||||
if (!token || !chatId) return consola.info('未发送Telegram通知, 未配置token或chatId: ', { token, chatId })
|
|
||||||
let { sendTg } = await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))
|
|
||||||
if (!sendTg) return consola.info('未发送Telegram通知, Plus功能解析失败')
|
|
||||||
await sendTg({ token, chatId }, title, content)
|
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
consola.info('未配置通知类型: ', type)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plusServer1: 'https://en1.221022.xyz',
|
|
||||||
plusServer2: 'https://en2.221022.xyz'
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
U2FsdGVkX1+Gus2FIC0WsNp0rUXPA+Ui1NQUjtnqP6Ycb1pyHglCADvKu51oxYaGJ0ZdoRZYo7YP3tQgIhp3f96WxP1/QFdypVrVlS7+jbAH6Gzc4CPlD3UeFsCm1j32ArFX60tPSSkq6+DJ3OF6pIVxstGIbCkmv5NQaf0J95zCxgqGm+fo/nZmZ6oj21uspGWZjhHssFRol0KpzINFDSWE9+/hJ43ybT5G6OHvEiaF83YH6h3CXAa6zz2zV18LKvnO8A4nTYR2/EBmGiP6NE3YqQ7hTE7SFmEDtRaxKJfyBxs0bHDCcFifVZh8GE25VyDwvOihUHztgvIRMh9vkgehzx9YN3sZdAsBJqcWyqi1mEPZU/l+zq2tbO+EczCvz6JQ77RZToQxm0vXzJc/ctcCEoVvjDx1pJhsQiTj5tJirFgcYz4VC7ihFYIq2XUQNISZaLynpYUUPdjvIfXGcvk0500SK9VAKb6603Z3fABdsENDGuxl2UKXMed4sL/PFwLy9siEX3BgMg1hFFiwoqqEp/x75341BoeRavEIJBEv8BdTS66mel1lUa/L3so7LyjGpdgfzOZlv+0t6Uhzy82HwYkAWmvuYpK6s6JItsG1ftYrOBzHZbpu36wn0e4N4NLqBnm6Hx1+tQJY7lTmgokgUy+5sVtp4LEsTbgE64HbDLYhME4m/3Yw5ij5D1OhoNwm/9r6MEYyJOyv8j8nDjudLRe1YQ0D2JLQsr04LYpVrjU1+Tsg780K0j0JdnFfVhe/SdkVU8nbkIIfRkv/86N6U2ZQaCYaScZmKYdBQmsK//I2yuYym0tM5q2d5kesYTxy8uAtVIXL1rE065eZFPlg/7Mgu0sqUsspG+EeDJE=
|
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "3.0.3",
|
"version": "3.0.0",
|
||||||
"description": "easynode-server",
|
"description": "easynode-server",
|
||||||
"bin": "./bin/www",
|
"bin": "./bin/www",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -43,7 +43,6 @@
|
|||||||
"node-os-utils": "^1.3.7",
|
"node-os-utils": "^1.3.7",
|
||||||
"node-rsa": "^1.1.1",
|
"node-rsa": "^1.1.1",
|
||||||
"node-schedule": "^2.1.1",
|
"node-schedule": "^2.1.1",
|
||||||
"node-telegram-bot-api": "^0.66.0",
|
|
||||||
"nodemailer": "^6.9.14",
|
"nodemailer": "^6.9.14",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"socket.io": "^4.7.5",
|
"socket.io": "^4.7.5",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "3.0.3",
|
"version": "3.0.0",
|
||||||
"description": "easynode-web",
|
"description": "easynode-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -22,15 +22,9 @@ export default {
|
|||||||
getPlusInfo() {
|
getPlusInfo() {
|
||||||
return axios({ url: '/plus-info', method: 'get' })
|
return axios({ url: '/plus-info', method: 'get' })
|
||||||
},
|
},
|
||||||
getPlusDiscount() {
|
|
||||||
return axios({ url: '/plus-discount', method: 'get' })
|
|
||||||
},
|
|
||||||
getCommand(hostId) {
|
getCommand(hostId) {
|
||||||
return axios({ url: '/command', method: 'get', params: { hostId } })
|
return axios({ url: '/command', method: 'get', params: { hostId } })
|
||||||
},
|
},
|
||||||
decryptPrivateKey(data) {
|
|
||||||
return axios({ url: '/decrypt-private-key', method: 'post', data })
|
|
||||||
},
|
|
||||||
getHostList() {
|
getHostList() {
|
||||||
return axios({ url: '/host-list', method: 'get' })
|
return axios({ url: '/host-list', method: 'get' })
|
||||||
},
|
},
|
||||||
@ -40,9 +34,6 @@ export default {
|
|||||||
updateHost(data) {
|
updateHost(data) {
|
||||||
return axios({ url: '/host-save', method: 'put', data })
|
return axios({ url: '/host-save', method: 'put', data })
|
||||||
},
|
},
|
||||||
batchUpdateHost(data) {
|
|
||||||
return axios({ url: '/batch-update-host', method: 'put', data })
|
|
||||||
},
|
|
||||||
removeHost(data) {
|
removeHost(data) {
|
||||||
return axios({ url: '/host-remove', method: 'post', data })
|
return axios({ url: '/host-remove', method: 'post', data })
|
||||||
},
|
},
|
||||||
@ -97,11 +88,8 @@ export default {
|
|||||||
deleteGroup(id) {
|
deleteGroup(id) {
|
||||||
return axios({ url: `/group/${ id }`, method: 'delete' })
|
return axios({ url: `/group/${ id }`, method: 'delete' })
|
||||||
},
|
},
|
||||||
getScriptList(params = {}) {
|
getScriptList() {
|
||||||
return axios({ url: '/script', method: 'get', params })
|
return axios({ url: '/script', method: 'get' })
|
||||||
},
|
|
||||||
importScript(data) {
|
|
||||||
return axios({ url: '/import-script', method: 'post', data })
|
|
||||||
},
|
},
|
||||||
getLocalScriptList() {
|
getLocalScriptList() {
|
||||||
return axios({ url: '/local-script', method: 'get' })
|
return axios({ url: '/local-script', method: 'get' })
|
||||||
@ -115,9 +103,6 @@ export default {
|
|||||||
deleteScript(id) {
|
deleteScript(id) {
|
||||||
return axios({ url: `/script/${ id }`, method: 'delete' })
|
return axios({ url: `/script/${ id }`, method: 'delete' })
|
||||||
},
|
},
|
||||||
batchRemoveScript(data) {
|
|
||||||
return axios({ url: '/batch-remove-script', method: 'post', data })
|
|
||||||
},
|
|
||||||
getOnekeyRecord() {
|
getOnekeyRecord() {
|
||||||
return axios({ url: '/onekey', method: 'get' })
|
return axios({ url: '/onekey', method: 'get' })
|
||||||
},
|
},
|
||||||
@ -126,11 +111,5 @@ export default {
|
|||||||
},
|
},
|
||||||
getEasynodeVersion() {
|
getEasynodeVersion() {
|
||||||
return axios({ url: '/version', method: 'get' })
|
return axios({ url: '/version', method: 'get' })
|
||||||
},
|
|
||||||
getPlusConf() {
|
|
||||||
return axios({ url: '/plus-conf', method: 'get' })
|
|
||||||
},
|
|
||||||
updatePlusKey(data) {
|
|
||||||
return axios({ url: '/plus-conf', method: 'post', data })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 4.3 KiB |
@ -10,7 +10,7 @@
|
|||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
@click="gotoPlusPage"
|
@click="handlePlusSupport"
|
||||||
>
|
>
|
||||||
去激活
|
去激活
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -22,15 +22,13 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, getCurrentInstance } from 'vue'
|
import { computed, getCurrentInstance } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
const { proxy: { $store } } = getCurrentInstance()
|
const { proxy: { $store } } = getCurrentInstance()
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const isPlusActive = computed(() => $store.isPlusActive)
|
const isPlusActive = computed(() => $store.isPlusActive)
|
||||||
|
|
||||||
const gotoPlusPage = () => {
|
const handlePlusSupport = () => {
|
||||||
router.push('/setting?tabKey=plus')
|
window.open('https://en.221022.xyz/buy-plus', '_blank')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,206 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="comparison-container">
|
|
||||||
<!-- 基础版卡片 -->
|
|
||||||
<el-card class="comparison-card basic-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span class="title">基础功能(免费)</span>
|
|
||||||
<el-tag size="small">Basic</el-tag>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="feature-list">
|
|
||||||
<div v-for="(feature, index) in basicFeatures" :key="index" class="feature-item">
|
|
||||||
<el-icon>
|
|
||||||
<Check />
|
|
||||||
</el-icon>
|
|
||||||
<span>{{ feature }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- Plus版卡片 -->
|
|
||||||
<el-card class="comparison-card plus-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<div>
|
|
||||||
<span class="title">Plus专属功能</span>
|
|
||||||
<span class="link" style="margin-right: 15px;" @click="() => plusTipsShow = true">Plus说明</span>
|
|
||||||
</div>
|
|
||||||
<el-tag type="success" size="small">PLUS</el-tag>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="feature-list">
|
|
||||||
<div v-for="(feature, index) in plusFeatures" :key="index" class="feature-item plus">
|
|
||||||
<el-icon color="#67c23a">
|
|
||||||
<Check />
|
|
||||||
</el-icon>
|
|
||||||
<span>{{ feature }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
<el-dialog
|
|
||||||
v-model="plusTipsShow"
|
|
||||||
title="Plus说明"
|
|
||||||
top="20vh"
|
|
||||||
width="30%"
|
|
||||||
:append-to-body="false"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
>
|
|
||||||
<div class="about_content">
|
|
||||||
<p style="line-height: 2;letter-spacing: 1px;">
|
|
||||||
<!-- <strong style="color: #F56C6C;font-weight: 600;">PLUS说明:</strong><br> -->
|
|
||||||
<strong>EasyNode</strong>最初是一个简单的Web终端工具,随着用户群的不断扩大,功能需求也日益增长,为了实现大家的功能需求,我投入了大量的业余时间进行开发和维护。
|
|
||||||
一直在为爱发电,渐渐的也没了开发的动力。
|
|
||||||
<br>
|
|
||||||
为了项目的可持续发展,<strong>后续</strong>版本开始推出<strong>PLUS</strong>版本,后续特性功能开发也会优先在<strong>PLUS</strong>版本中实现,但即使不升级到<strong>PLUS</strong>,也不会影响到<strong>EasyNode</strong>的基础功能使用【注意:
|
|
||||||
暂不支持纯内网用户激活PLUS功能】。
|
|
||||||
<br>
|
|
||||||
<span style="text-decoration: underline;">
|
|
||||||
为了感谢前期赞赏过的用户, 在<strong>PLUS</strong>功能正式发布前,所有进行过赞赏的用户,无论金额大小,均可联系作者TG: <a
|
|
||||||
class="link"
|
|
||||||
href="https://t.me/chaoszhu"
|
|
||||||
target="_blank"
|
|
||||||
>@chaoszhu</a> 凭打赏记录免费获取永久<strong>PLUS</strong>授权码。
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div class="about_footer">
|
|
||||||
<el-button type="info" @click="plusTipsShow = false">关闭</el-button>
|
|
||||||
<el-button type="primary" @click="handlePlusSupport">购买Plus Key</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { Check } from '@element-plus/icons-vue'
|
|
||||||
import { handlePlusSupport } from '@/utils'
|
|
||||||
|
|
||||||
const plusTipsShow = ref(false)
|
|
||||||
|
|
||||||
// 基础版功能列表
|
|
||||||
const basicFeatures = [
|
|
||||||
'服务器管理',
|
|
||||||
'服务器导入导出',
|
|
||||||
'服务器分组',
|
|
||||||
'凭据管理',
|
|
||||||
'脚本库',
|
|
||||||
'批量连接',
|
|
||||||
'批量指令',
|
|
||||||
'通知方式(有限制)',
|
|
||||||
]
|
|
||||||
|
|
||||||
// Plus版专属功能列表
|
|
||||||
const plusFeatures = [
|
|
||||||
'包含基础版全部功能',
|
|
||||||
'服务器跳板机功能,支持任意数量服务器的连续跳板',
|
|
||||||
'批量修改实例配置(优化版)',
|
|
||||||
'脚本库批量导出导入',
|
|
||||||
'凭据管理支持解密带密码保护的密钥',
|
|
||||||
'通知方式无限制',
|
|
||||||
'本地socket断开自动重连',
|
|
||||||
'功能需求更高开发优先级',
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.comparison-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
max-width: 1200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comparison-card {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-list {
|
|
||||||
// padding: 10px 0;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px 0;
|
|
||||||
/* border-bottom: 1px solid #eee; */
|
|
||||||
border-bottom: 1px solid var(--el-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-item .el-icon {
|
|
||||||
color: #409EFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plus-card .feature-item.plus {
|
|
||||||
color: #67c23a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about_content {
|
|
||||||
h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
line-height: 1.8;
|
|
||||||
margin: 12px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
color: #409EFF;
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.conspicuous {
|
|
||||||
color: #F56C6C;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about_footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.comparison-container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comparison-card {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -20,7 +20,7 @@
|
|||||||
link
|
link
|
||||||
@click="visible = true"
|
@click="visible = true"
|
||||||
>
|
>
|
||||||
版本更新 <span class="new_version">{{ isNew ? `(新版本可用)` : '' }}</span>
|
关于 <span class="new_version">{{ isNew ? `(新版本可用)` : '' }}</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-dropdown trigger="click">
|
<el-dropdown trigger="click">
|
||||||
<span class="username_wrap">
|
<span class="username_wrap">
|
||||||
@ -37,34 +37,100 @@
|
|||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
<div class="plus_icon_wrapper" @click="gotoPlusPage">
|
|
||||||
|
<el-popover placement="left" :width="320" trigger="hover">
|
||||||
|
<template #reference>
|
||||||
<img
|
<img
|
||||||
class="plus_icon"
|
class="plus_icon"
|
||||||
src="@/assets/plus.png"
|
src="@/assets/plus.png"
|
||||||
alt="PLUS"
|
alt="PLUS"
|
||||||
:style="{ filter: isPlusActive ? 'grayscale(0%)' : 'grayscale(100%)' }"
|
:style="{ filter: isPlusActive ? 'grayscale(0%)' : 'grayscale(100%)' }"
|
||||||
>
|
>
|
||||||
<img
|
</template>
|
||||||
v-if="!isPlusActive && discount"
|
<template #default>
|
||||||
class="discount_badge"
|
<div class="plus_content_wrap">
|
||||||
src="@/assets/discount.png"
|
<!-- Plus 激活状态信息 -->
|
||||||
alt="Discount"
|
<div v-if="isPlusActive" class="plus_status">
|
||||||
>
|
<div class="status_header">
|
||||||
|
<el-icon>
|
||||||
|
<CircleCheckFilled />
|
||||||
|
</el-icon>
|
||||||
|
<span>Plus专属功能已激活</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="status_info">
|
||||||
|
<div class="info_item">
|
||||||
|
<span class="label">到期时间:</span>
|
||||||
|
<span class="value holder">{{ plusInfo.expiryDate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info_item">
|
||||||
|
<span class="label">授权IP数:</span>
|
||||||
|
<span class="value">{{ plusInfo.maxIPs }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info_item">
|
||||||
|
<span class="label">已授权IP数:</span>
|
||||||
|
<span class="value">{{ plusInfo.usedIPCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info_item ip_list">
|
||||||
|
<span class="label">已授权IP:</span>
|
||||||
|
<div class="ip_tags">
|
||||||
|
<el-tag
|
||||||
|
v-for="ip in plusInfo.usedIPs"
|
||||||
|
:key="ip"
|
||||||
|
size="small"
|
||||||
|
class="ip_tag"
|
||||||
|
>
|
||||||
|
{{ ip }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="plus_benefits" :class="{ active: isPlusActive }" @click="handlePlus">
|
||||||
|
<span v-if="!isPlusActive" class="support_btn" @click="handlePlusSupport">去支持</span>
|
||||||
|
<div class="benefits_header">
|
||||||
|
<el-icon>
|
||||||
|
<el-icon><StarFilled /></el-icon>
|
||||||
|
</el-icon>
|
||||||
|
<span>Plus功能介绍</span>
|
||||||
|
</div>
|
||||||
|
<div class="current_benefits">
|
||||||
|
<div v-for="plusFeature in plusFeatures" :key="plusFeature" class="benefit_item">
|
||||||
|
<el-icon>
|
||||||
|
<Star />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ plusFeature }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="coming_soon">
|
||||||
|
<div class="soon_header">开发中的PLUS功能</div>
|
||||||
|
<div class="current_benefits">
|
||||||
|
<div v-for="soonFeature in soonFeatures" :key="soonFeature" class="benefit_item">
|
||||||
|
<el-icon>
|
||||||
|
<Star />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ soonFeature }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="版本更新"
|
title="关于"
|
||||||
top="20vh"
|
top="10vh"
|
||||||
width="30%"
|
width="30%"
|
||||||
:append-to-body="false"
|
:append-to-body="false"
|
||||||
:close-on-click-modal="false"
|
|
||||||
>
|
>
|
||||||
<div class="about_content">
|
<div class="about_content">
|
||||||
<!-- <h1>EasyNode</h1> -->
|
<h1>EasyNode</h1>
|
||||||
<p>当前版本: {{ currentVersion }} <span v-show="!isNew">(最新)</span> </p>
|
<p>当前版本: {{ currentVersion }} <span v-show="!isNew">(最新)</span> </p>
|
||||||
<p v-if="checkVersionErr" class="conspicuous">Error:版本更新检测失败(版本检测API需要外网环境),请手动访问GitHub查看</p>
|
<p v-if="checkVersionErr" class="conspicuous">Error:版本更新检测失败(版本检测API需要外网环境)</p>
|
||||||
<p v-if="isNew" class="conspicuous">
|
<p v-if="isNew" class="conspicuous">
|
||||||
新版本可用: {{ latestVersion }} -> <a
|
新版本可用: {{ latestVersion }} -> <a
|
||||||
class="link"
|
class="link"
|
||||||
@ -73,17 +139,28 @@
|
|||||||
>https://github.com/chaos-zhu/easynode/releases</a>
|
>https://github.com/chaos-zhu/easynode/releases</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
功能更新日志:<a
|
更新日志:<a
|
||||||
class="link"
|
class="link"
|
||||||
href="https://github.com/chaos-zhu/easynode/blob/main/CHANGELOG.md"
|
href="https://github.com/chaos-zhu/easynode/blob/main/CHANGELOG.md"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>https://github.com/chaos-zhu/easynode/blob/main/CHANGELOG.md</a>
|
>https://github.com/chaos-zhu/easynode/blob/main/CHANGELOG.md</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
TG更新通知频道:<a class="link" href="https://t.me/easynode_notify" target="_blank">https://t.me/easynode_notify</a>
|
tg更新通知:<a class="link" href="https://t.me/easynode_notify" target="_blank">https://t.me/easynode_notify</a>
|
||||||
</p>
|
</p>
|
||||||
<div class="about_footer">
|
<p style="line-height: 2;letter-spacing: 1px;">
|
||||||
<el-button type="info" @click="visible = false">关闭</el-button>
|
<strong style="color: #F56C6C;font-weight: 600;">PLUS说明:</strong><br>
|
||||||
|
<strong>EasyNode</strong>最初是一个简单的Web终端工具,随着用户群的不断扩大,功能需求也日益增长,为了实现大家的功能需求,我投入了大量的业余时间进行开发和维护。
|
||||||
|
一直在为爱发电,渐渐的也没了开发的动力。
|
||||||
|
<br>
|
||||||
|
为了项目的可持续发展,从<strong>3.0.0</strong>版本开始推出了<strong>PLUS</strong>版本,具体特性鼠标悬浮右上角PLUS图标查看,后续特性功能开发也会优先在<strong>PLUS</strong>版本中实现,但即使不升级到<strong>PLUS</strong>,也不会影响到<strong>EasyNode</strong>的基础功能使用【注意: 暂不支持纯内网用户激活PLUS功能】。
|
||||||
|
<br>
|
||||||
|
<span style="text-decoration: underline;">
|
||||||
|
为了感谢前期赞赏过的用户, 在<strong>PLUS</strong>功能正式发布前,所有进行过赞赏的用户,无论金额大小,均可联系 TG: <a class="link" href="https://t.me/chaoszhu" target="_blank">@chaoszhu</a> 凭打赏记录获取永久<strong>PLUS</strong>授权码。
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div v-if="!isPlusActive" class="about_footer">
|
||||||
|
<el-button type="primary" @click="handlePlusSupport">去支持</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@ -104,24 +181,36 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, getCurrentInstance, computed, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, getCurrentInstance, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { User, Sunny, Moon, Fold, CircleCheckFilled, Star, StarFilled } from '@element-plus/icons-vue'
|
||||||
import { User, Sunny, Moon, Fold } from '@element-plus/icons-vue'
|
|
||||||
import packageJson from '../../package.json'
|
import packageJson from '../../package.json'
|
||||||
import MenuList from './menuList.vue'
|
import MenuList from './menuList.vue'
|
||||||
|
|
||||||
const { proxy: { $router, $store, $api, $message } } = getCurrentInstance()
|
const { proxy: { $router, $store, $message } } = getCurrentInstance()
|
||||||
const router = useRouter()
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
const checkVersionErr = ref(false)
|
const checkVersionErr = ref(false)
|
||||||
const currentVersion = ref(`v${ packageJson.version }`)
|
const currentVersion = ref(`v${ packageJson.version }`)
|
||||||
const latestVersion = ref(null)
|
const latestVersion = ref(null)
|
||||||
const menuCollapse = ref(false)
|
const menuCollapse = ref(false)
|
||||||
const discount = ref(false)
|
|
||||||
|
const plusFeatures = [
|
||||||
|
'跳板机功能,拯救被墙实例与龟速终端输入',
|
||||||
|
'本地socket断开自动重连,无需手动重新连接',
|
||||||
|
'提出的功能需求享有更高的开发优先级',
|
||||||
|
]
|
||||||
|
const soonFeatures = [
|
||||||
|
'终端分屏功能(plus)',
|
||||||
|
'终端脚本变量支持(plus)',
|
||||||
|
'脚本库批量导出导入(plus)',
|
||||||
|
'密码密钥解密功能(plus)',
|
||||||
|
'系统操作日志审计(plus)',
|
||||||
|
]
|
||||||
|
|
||||||
const isNew = computed(() => latestVersion.value && latestVersion.value !== currentVersion.value)
|
const isNew = computed(() => latestVersion.value && latestVersion.value !== currentVersion.value)
|
||||||
const user = computed(() => $store.user)
|
const user = computed(() => $store.user)
|
||||||
const title = computed(() => $store.title)
|
const title = computed(() => $store.title)
|
||||||
|
const plusInfo = computed(() => $store.plusInfo)
|
||||||
const isPlusActive = computed(() => $store.isPlusActive)
|
const isPlusActive = computed(() => $store.isPlusActive)
|
||||||
|
|
||||||
const isDark = computed({
|
const isDark = computed({
|
||||||
@ -141,8 +230,8 @@ const handleLogout = () => {
|
|||||||
$router.push('/login')
|
$router.push('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
const gotoPlusPage = () => {
|
const handlePlusSupport = () => {
|
||||||
router.push('/setting?tabKey=plus')
|
window.open('https://en.221022.xyz/buy-plus', '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkLatestVersion() {
|
async function checkLatestVersion() {
|
||||||
@ -178,32 +267,6 @@ async function checkLatestVersion() {
|
|||||||
|
|
||||||
checkLatestVersion()
|
checkLatestVersion()
|
||||||
|
|
||||||
let timer = null
|
|
||||||
const checkFirstVisit = () => {
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
const visitedVersion = localStorage.getItem('visitedVersion')
|
|
||||||
if (!visitedVersion || visitedVersion !== currentVersion.value) {
|
|
||||||
visible.value = true
|
|
||||||
localStorage.setItem('visitedVersion', currentVersion.value)
|
|
||||||
}
|
|
||||||
}, 1500)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPlusDiscount = async () => {
|
|
||||||
const { data } = await $api.getPlusDiscount()
|
|
||||||
if (data?.discount) {
|
|
||||||
discount.value = data.discount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
checkFirstVisit()
|
|
||||||
getPlusDiscount()
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
clearTimeout(timer)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -250,26 +313,10 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.plus_icon_wrapper {
|
|
||||||
margin-left: 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.plus_icon {
|
.plus_icon {
|
||||||
|
margin-left: 15px;
|
||||||
width: 35px;
|
width: 35px;
|
||||||
margin-right: 5px;
|
cursor: pointer;
|
||||||
}
|
|
||||||
|
|
||||||
.discount_badge {
|
|
||||||
width: 22px;
|
|
||||||
color: white;
|
|
||||||
font-size: 12px;
|
|
||||||
transform: rotate(25deg);
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,18 +353,6 @@ onBeforeUnmount(() => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
transform: rotate(25deg) scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: rotate(25deg) scale(1.1);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(25deg) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@ -424,19 +459,5 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.discount_content {
|
|
||||||
margin: 8px 0;
|
|
||||||
|
|
||||||
.el-tag {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 6px 12px;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -101,7 +101,7 @@ const useStore = defineStore({
|
|||||||
this.$patch({ localScriptList })
|
this.$patch({ localScriptList })
|
||||||
},
|
},
|
||||||
async getPlusInfo() {
|
async getPlusInfo() {
|
||||||
const { data: plusInfo = {} } = await $api.getPlusInfo()
|
const { data: plusInfo } = await $api.getPlusInfo()
|
||||||
if (plusInfo?.expiryDate) {
|
if (plusInfo?.expiryDate) {
|
||||||
const isPlusActive = new Date(plusInfo.expiryDate) > new Date()
|
const isPlusActive = new Date(plusInfo.expiryDate) > new Date()
|
||||||
this.$patch({ isPlusActive })
|
this.$patch({ isPlusActive })
|
||||||
@ -111,9 +111,6 @@ const useStore = defineStore({
|
|||||||
}
|
}
|
||||||
plusInfo.expiryDate = dayjs(plusInfo.expiryDate).format('YYYY-MM-DD')
|
plusInfo.expiryDate = dayjs(plusInfo.expiryDate).format('YYYY-MM-DD')
|
||||||
plusInfo.expiryDate?.startsWith('9999') && (plusInfo.expiryDate = '永久授权')
|
plusInfo.expiryDate?.startsWith('9999') && (plusInfo.expiryDate = '永久授权')
|
||||||
this.$patch({ plusInfo })
|
|
||||||
} else {
|
|
||||||
this.$patch({ isPlusActive: false })
|
|
||||||
}
|
}
|
||||||
this.$patch({ plusInfo })
|
this.$patch({ plusInfo })
|
||||||
},
|
},
|
||||||
|
@ -128,7 +128,7 @@ export const getSuffix = (name = '') => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const exportFile = (data, filename, mimeType = 'application/json') =>{
|
export const exportFile = (data, filename, mimeType = 'application/json') =>{
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2),], { type: mimeType })
|
const blob = new Blob([JSON.stringify(data),], { type: mimeType })
|
||||||
const url = window.URL.createObjectURL(blob)
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
@ -149,7 +149,3 @@ export const isMobile = () => {
|
|||||||
let userAgent = navigator.userAgent || navigator.vendor || window.opera
|
let userAgent = navigator.userAgent || navigator.vendor || window.opera
|
||||||
return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent)
|
return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handlePlusSupport = () => {
|
|
||||||
window.open('https://en.221022.xyz/buy-plus', '_blank')
|
|
||||||
}
|
|
||||||
|
@ -84,41 +84,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
<el-dialog
|
|
||||||
v-model="keyPasswordVisible"
|
|
||||||
title="输入密钥密码"
|
|
||||||
width="400px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
>
|
|
||||||
<el-form @submit.prevent>
|
|
||||||
<el-form-item label="密码">
|
|
||||||
<el-input
|
|
||||||
v-model="keyPassword"
|
|
||||||
type="password"
|
|
||||||
placeholder="请输入密钥密码"
|
|
||||||
show-password
|
|
||||||
autocomplete="off"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="handleDecryptKey"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<span>
|
|
||||||
<el-button @click="keyPasswordVisible = false">取消</el-button>
|
|
||||||
<PlusSupportTip>
|
|
||||||
<el-button type="primary" :disabled="!isPlusActive" @click="handleDecryptKey">确认</el-button>
|
|
||||||
</PlusSupportTip>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, nextTick, getCurrentInstance } from 'vue'
|
import { ref, reactive, computed, nextTick, getCurrentInstance } from 'vue'
|
||||||
import { randomStr, AESEncrypt, RSAEncrypt } from '@utils/index.js'
|
import { randomStr, AESEncrypt, RSAEncrypt } from '@utils/index.js'
|
||||||
import PlusSupportTip from '@/components/common/PlusSupportTip.vue'
|
|
||||||
|
|
||||||
const { proxy: { $api, $message, $messageBox, $store } } = getCurrentInstance()
|
const { proxy: { $api, $message, $messageBox, $store } } = getCurrentInstance()
|
||||||
|
|
||||||
@ -144,7 +115,6 @@ const updateFormRef = ref(null)
|
|||||||
const privateKeyRef = ref(null)
|
const privateKeyRef = ref(null)
|
||||||
|
|
||||||
let sshList = computed(() => $store.sshList)
|
let sshList = computed(() => $store.sshList)
|
||||||
let isPlusActive = computed(() => $store.isPlusActive)
|
|
||||||
|
|
||||||
let addCredentials = () => {
|
let addCredentials = () => {
|
||||||
sshForm.id = null
|
sshForm.id = null
|
||||||
@ -189,9 +159,9 @@ const removeSSH = ({ id, name }) => {
|
|||||||
type: 'warning'
|
type: 'warning'
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await $api.removeSSH(id)
|
await $api.removeSSH(id) // 后台会同步删除关联此凭证的credential字段
|
||||||
await $store.getSSHList()
|
await $store.getSSHList()
|
||||||
await $store.getHostList()
|
await $store.getHostList() // 刷新主机字段 isConfig
|
||||||
$message.success('success')
|
$message.success('success')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -200,40 +170,16 @@ const handleClickUploadBtn = () => {
|
|||||||
privateKeyRef.value.click()
|
privateKeyRef.value.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyPasswordVisible = ref(false)
|
|
||||||
const keyPassword = ref('')
|
|
||||||
const tempPrivateKey = ref('')
|
|
||||||
|
|
||||||
const handleSelectPrivateKeyFile = (event) => {
|
const handleSelectPrivateKeyFile = (event) => {
|
||||||
let file = event.target.files[0]
|
let file = event.target.files[0]
|
||||||
let reader = new FileReader()
|
let reader = new FileReader()
|
||||||
reader.onload = async (e) => {
|
reader.onload = (e) => {
|
||||||
const content = e.target.result
|
sshForm.privateKey = e.target.result
|
||||||
// 检查是否是加密的私钥
|
|
||||||
if (content.includes('ENCRYPTED')) {
|
|
||||||
tempPrivateKey.value = content
|
|
||||||
keyPasswordVisible.value = true
|
|
||||||
} else {
|
|
||||||
sshForm.privateKey = content
|
|
||||||
}
|
|
||||||
privateKeyRef.value.value = ''
|
privateKeyRef.value.value = ''
|
||||||
}
|
}
|
||||||
reader.readAsText(file)
|
reader.readAsText(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDecryptKey = async () => {
|
|
||||||
if (!keyPassword.value) return $message.error('请输入密钥密码')
|
|
||||||
const { data } = await $api.decryptPrivateKey({
|
|
||||||
privateKey: tempPrivateKey.value,
|
|
||||||
password: keyPassword.value
|
|
||||||
})
|
|
||||||
sshForm.privateKey = data
|
|
||||||
keyPasswordVisible.value = false
|
|
||||||
keyPassword.value = ''
|
|
||||||
tempPrivateKey.value = ''
|
|
||||||
$message.success('密钥解密成功')
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="mfa2Token" label="MFA2验证码">
|
<el-form-item prop="mfa2Token" label="MFA2验证码">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="loginForm.mfa2Token"
|
v-model.trim.number="loginForm.mfa2Token"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="MFA2应用上的6位数字(未设置可忽略)"
|
placeholder="MFA2应用上的6位数字(未设置可忽略)"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@ -103,7 +103,7 @@ const loginForm = reactive({
|
|||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
loginName: { required: true, message: '需输入用户名', trigger: 'change' },
|
loginName: { required: true, message: '需输入用户名', trigger: 'change' },
|
||||||
pwd: { required: true, message: '需输入密码', trigger: 'change' },
|
pwd: { required: true, message: '需输入密码', trigger: 'change' },
|
||||||
mfa2Token: { required: false, message: '需输入MFA2验证码', trigger: 'change' }
|
mfa2Token: { required: false, message: '需输入密码', trigger: 'change' }
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleLogin = () => {
|
const handleLogin = () => {
|
||||||
@ -124,7 +124,7 @@ const handleLogin = () => {
|
|||||||
if (ciphertext === -1) return $message.error({ message: '公钥加载失败', center: true })
|
if (ciphertext === -1) return $message.error({ message: '公钥加载失败', center: true })
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
let { data, msg } = await $api.login({ loginName, ciphertext, jwtExpires, mfa2Token: Number(mfa2Token) })
|
let { data, msg } = await $api.login({ loginName, ciphertext, jwtExpires, mfa2Token })
|
||||||
const { token } = data
|
const { token } = data
|
||||||
$store.setJwtToken(token, expireEnum.ONE_SESSION === expireTime.value)
|
$store.setJwtToken(token, expireEnum.ONE_SESSION === expireTime.value)
|
||||||
$store.setUser(loginName)
|
$store.setUser(loginName)
|
||||||
|
@ -426,7 +426,7 @@ onActivated(async () => {
|
|||||||
const { hostIds, execClientInstallScript } = route.query
|
const { hostIds, execClientInstallScript } = route.query
|
||||||
if (!hostIds) return
|
if (!hostIds) return
|
||||||
if (execClientInstallScript === 'true') {
|
if (execClientInstallScript === 'true') {
|
||||||
let clientInstallScript = 'curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash\n'
|
let clientInstallScript = 'curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash\n'
|
||||||
console.log(hostIds.split(','))
|
console.log(hostIds.split(','))
|
||||||
createExecShell(hostIds.split(','), clientInstallScript, 300)
|
createExecShell(hostIds.split(','), clientInstallScript, 300)
|
||||||
// $messageBox.confirm(`准备安装客户端服务监控应用:${ host }`, 'Warning', {
|
// $messageBox.confirm(`准备安装客户端服务监控应用:${ host }`, 'Warning', {
|
||||||
@ -435,7 +435,7 @@ onActivated(async () => {
|
|||||||
// type: 'warning'
|
// type: 'warning'
|
||||||
// })
|
// })
|
||||||
// .then(async () => {
|
// .then(async () => {
|
||||||
// let clientInstallScript = 'curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash\n'
|
// let clientInstallScript = 'curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash\n'
|
||||||
// createExecShell([host,], clientInstallScript, 300)
|
// createExecShell([host,], clientInstallScript, 300)
|
||||||
// })
|
// })
|
||||||
}
|
}
|
||||||
|
@ -1,226 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
v-model="visible"
|
|
||||||
width="600px"
|
|
||||||
top="225px"
|
|
||||||
modal-class="import_form_dialog"
|
|
||||||
append-to-body
|
|
||||||
title="导入脚本配置"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
>
|
|
||||||
<h2>选择要导入的文件类型</h2>
|
|
||||||
<ul class="type_list">
|
|
||||||
<li @click="handleFromJson">
|
|
||||||
<svg-icon name="icon-json" class="icon" />
|
|
||||||
<span class="from">JSON</span>
|
|
||||||
<input
|
|
||||||
ref="jsonInputRef"
|
|
||||||
type="file"
|
|
||||||
accept=".json"
|
|
||||||
multiple
|
|
||||||
name="jsonInput"
|
|
||||||
style="display: none;"
|
|
||||||
@change="handleJsonFile"
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li @click="manualInputVisible = true">
|
|
||||||
<svg-icon name="icon-bianji1" class="icon" />
|
|
||||||
<span class="from">手动输入</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
v-model="manualInputVisible"
|
|
||||||
width="600px"
|
|
||||||
top="150px"
|
|
||||||
title="手动输入"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
append-to-body
|
|
||||||
>
|
|
||||||
<el-input
|
|
||||||
v-model="manualInput"
|
|
||||||
type="textarea"
|
|
||||||
:autosize="{ minRows: 15 }"
|
|
||||||
placeholder="请输入脚本内容,每行一条脚本"
|
|
||||||
/>
|
|
||||||
<template #footer>
|
|
||||||
<div class="manual-input-footer">
|
|
||||||
<el-button @click="manualInputVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="handleManualImport">导入</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy: { $api, $message, $store } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
show: {
|
|
||||||
required: true,
|
|
||||||
type: Boolean
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:show', 'update-list',])
|
|
||||||
|
|
||||||
const jsonInputRef = ref(null)
|
|
||||||
const manualInputVisible = ref(false)
|
|
||||||
const manualInput = ref('')
|
|
||||||
|
|
||||||
let visible = computed({
|
|
||||||
get: () => props.show,
|
|
||||||
set: (newVal) => emit('update:show', newVal)
|
|
||||||
})
|
|
||||||
|
|
||||||
let scriptList = computed(() => $store.scriptList)
|
|
||||||
|
|
||||||
function handleFromJson() {
|
|
||||||
jsonInputRef.value.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleJsonFile = (event) => {
|
|
||||||
let files = event.target.files
|
|
||||||
let jsonFiles = Array.from(files).filter(file => file.name.endsWith('.json'))
|
|
||||||
if (jsonFiles.length === 0) return $message.warning('未选择有效的JSON文件')
|
|
||||||
|
|
||||||
let readerPromises = jsonFiles.map(file => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
try {
|
|
||||||
let jsonContent = JSON.parse(e.target.result)
|
|
||||||
resolve(jsonContent)
|
|
||||||
} catch (error) {
|
|
||||||
reject(new Error(`Failed to parse JSON file: ${ file.name }`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.onerror = () => {
|
|
||||||
reject(new Error(`Failed to read file: ${ file.name }`))
|
|
||||||
}
|
|
||||||
reader.readAsText(file)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
Promise.all(readerPromises)
|
|
||||||
.then(async jsonContents => {
|
|
||||||
let formatJson = jsonContents.flat(Infinity)
|
|
||||||
let existCommand = scriptList.value.map(item => item.command)
|
|
||||||
let existId = scriptList.value.map(item => item.id)
|
|
||||||
formatJson = formatJson.filter(({ _id, command }) => {
|
|
||||||
return !existCommand.includes(command) && !existId.includes(_id)
|
|
||||||
})
|
|
||||||
if (formatJson.length === 0) return $message.warning('导入的脚本已存在')
|
|
||||||
try {
|
|
||||||
let { data: { len } } = await $api.importScript({ scripts: formatJson })
|
|
||||||
$message({ type: 'success', center: true, message: `成功导入脚本: ${ len }条` })
|
|
||||||
emit('update-list')
|
|
||||||
visible.value = false
|
|
||||||
} catch (error) {
|
|
||||||
$message.error('导入失败: ' + error.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
$message.error('导入失败: ' + error.message)
|
|
||||||
console.error('导入失败: ', error)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
event.target.value = null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleManualImport = async () => {
|
|
||||||
if (!manualInput.value.trim()) {
|
|
||||||
return $message.warning('请输入脚本内容')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let scripts = manualInput.value.split('\n')
|
|
||||||
scripts = [...new Set(scripts),]
|
|
||||||
.filter(line => line.trim())
|
|
||||||
.map((command) => ({ command: command.trim() }))
|
|
||||||
if (scripts.length === 0) {
|
|
||||||
return $message.warning('未检测到有效的脚本内容')
|
|
||||||
}
|
|
||||||
|
|
||||||
let existCommand = scriptList.value.map(item => item.command)
|
|
||||||
let filterScripts = scripts.filter(({ command }) => {
|
|
||||||
return !existCommand.includes(command)
|
|
||||||
})
|
|
||||||
let filterScriptsLen = filterScripts.length
|
|
||||||
if (filterScriptsLen !== 0 && filterScriptsLen < scripts.length) $message.warning('已过滤重复的脚本')
|
|
||||||
if (filterScriptsLen === 0) return $message.warning('导入的脚本已存在')
|
|
||||||
filterScripts = filterScripts.map((item, index) => {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
name: `${ item.command.slice(0, 15) || `脚本${ index + 1 }` }`,
|
|
||||||
index: scriptList.value.length + index + 1,
|
|
||||||
description: '手动输入'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let { data: { len } } = await $api.importScript({ scripts: filterScripts })
|
|
||||||
$message({ type: 'success', center: true, message: `成功导入脚本: ${ len }条` })
|
|
||||||
emit('update-list')
|
|
||||||
manualInputVisible.value = false
|
|
||||||
visible.value = false
|
|
||||||
manualInput.value = ''
|
|
||||||
} catch (error) {
|
|
||||||
$message.error('导入失败: ' + error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.import_form_dialog {
|
|
||||||
h2 {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
margin: 15px 0 25px 0;
|
|
||||||
}
|
|
||||||
.type_list {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
user-select: none;
|
|
||||||
li {
|
|
||||||
margin: 0 25px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 3px;
|
|
||||||
&:hover {
|
|
||||||
color: var(--el-menu-active-color);
|
|
||||||
}
|
|
||||||
.icon {
|
|
||||||
width: 35px;
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.from {
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
.type {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.manual-input-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,52 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="scripts_container">
|
<div class="scripts_container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<el-input
|
<el-button type="primary" @click="addScript">添加脚本</el-button>
|
||||||
v-model="searchKeyword"
|
|
||||||
placeholder="搜索名称、描述或指令内容"
|
|
||||||
class="search_input"
|
|
||||||
clearable
|
|
||||||
@input="handleSearch"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<el-icon><Search /></el-icon>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
<el-button
|
|
||||||
v-show="selectScripts.length"
|
|
||||||
type="danger"
|
|
||||||
class="batch_remove_btn"
|
|
||||||
@click="handleBatchRemove"
|
|
||||||
>
|
|
||||||
批量删除
|
|
||||||
</el-button>
|
|
||||||
<el-button type="primary" class="add_script_btn" @click="addScript">添加脚本</el-button>
|
|
||||||
<PlusSupportTip>
|
|
||||||
<el-dropdown trigger="click" :disabled="!isPlusActive">
|
|
||||||
<el-button type="primary" class="group_action_btn" :disabled="!isPlusActive">
|
|
||||||
导入导出<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item @click="importVisible = true">导入脚本</el-dropdown-item>
|
|
||||||
<el-dropdown-item @click="handleExport">导出脚本</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
</PlusSupportTip>
|
|
||||||
</div>
|
</div>
|
||||||
<el-table
|
<el-table v-loading="loading" :data="scriptList">
|
||||||
v-loading="loading"
|
|
||||||
:data="paginatedFilteredList"
|
|
||||||
@selection-change="handleSelectionChange"
|
|
||||||
>
|
|
||||||
<el-table-column
|
|
||||||
type="selection"
|
|
||||||
width="55"
|
|
||||||
:selectable="(row) => {
|
|
||||||
return row.index !== '--' && row.index !== '-' && row.index !== undefined && row.index !== null
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<el-table-column prop="index" label="序号" width="100px" />
|
<el-table-column prop="index" label="序号" width="100px" />
|
||||||
<el-table-column prop="name" label="名称" />
|
<el-table-column prop="name" label="名称" />
|
||||||
<el-table-column prop="description" label="描述" />
|
<el-table-column prop="description" label="描述" />
|
||||||
@ -61,19 +18,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="pagination-container">
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="currentPage"
|
|
||||||
v-model:page-size="pageSize"
|
|
||||||
:page-sizes="[20, 50, 100]"
|
|
||||||
:total="scriptList.length"
|
|
||||||
layout="total, sizes, prev, pager, next"
|
|
||||||
@size-change="handleSizeChange"
|
|
||||||
@current-change="handleCurrentChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="formVisible"
|
v-model="formVisible"
|
||||||
width="600px"
|
width="600px"
|
||||||
@ -134,44 +78,17 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<ImportScript
|
|
||||||
v-model:show="importVisible"
|
|
||||||
@update-list="() => $store.getScriptList()"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, nextTick, getCurrentInstance, h } from 'vue'
|
import { ref, reactive, computed, nextTick, getCurrentInstance } from 'vue'
|
||||||
import ImportScript from './components/import-script.vue'
|
|
||||||
import PlusSupportTip from '@/components/common/PlusSupportTip.vue'
|
|
||||||
import { ArrowDown, Search } from '@element-plus/icons-vue'
|
|
||||||
import { exportFile } from '@/utils'
|
|
||||||
|
|
||||||
const { proxy: { $api, $message, $messageBox, $store, $tools } } = getCurrentInstance()
|
const { proxy: { $api, $message, $messageBox, $store } } = getCurrentInstance()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const formVisible = ref(false)
|
const formVisible = ref(false)
|
||||||
let isModify = ref(false)
|
let isModify = ref(false)
|
||||||
const selectScripts = ref([])
|
|
||||||
const handleSelectionChange = (val) => {
|
|
||||||
selectScripts.value = val
|
|
||||||
}
|
|
||||||
const handleBatchRemove = () => {
|
|
||||||
if (!selectScripts.value.length) return $message.warning('请选择要批量删除的脚本')
|
|
||||||
let ids = selectScripts.value.map(item => item.id)
|
|
||||||
let names = selectScripts.value.map(item => item.name)
|
|
||||||
$messageBox.confirm(() => h('p', { style: 'line-height: 18px;' }, `确认删除\n${ names.join(', ') }吗?`), 'Warning', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}).then(async () => {
|
|
||||||
await $api.batchRemoveScript({ ids })
|
|
||||||
await $store.getScriptList()
|
|
||||||
$message.success('success')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let formData = reactive({
|
let formData = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
@ -191,12 +108,10 @@ const rules = computed(() => {
|
|||||||
|
|
||||||
const updateFormRef = ref(null)
|
const updateFormRef = ref(null)
|
||||||
|
|
||||||
const scriptList = computed(() => $store.scriptList)
|
let scriptList = computed(() => $store.scriptList)
|
||||||
const isPlusActive = computed(() => $store.isPlusActive)
|
|
||||||
|
|
||||||
let addScript = () => {
|
let addScript = () => {
|
||||||
formData.id = null
|
formData.id = null
|
||||||
formData.index = scriptList.value.reduce((acc, cur) => Math.max(acc, Number(cur.index) || 0), 0) + 1
|
|
||||||
isModify.value = false
|
isModify.value = false
|
||||||
formVisible.value = true
|
formVisible.value = true
|
||||||
}
|
}
|
||||||
@ -239,49 +154,6 @@ const handleRemove = ({ id, name }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const importVisible = ref(false)
|
|
||||||
|
|
||||||
const handleExport = () => {
|
|
||||||
if (!scriptList.value.length) return $message.warning('暂无可导出的脚本')
|
|
||||||
const fileName = `easynode-scripts-${ $tools.formatTimestamp(Date.now(), 'time', '.') }.json`
|
|
||||||
exportFile(scriptList.value, fileName, 'application/json')
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPage = ref(1)
|
|
||||||
const pageSize = ref(20)
|
|
||||||
|
|
||||||
const searchKeyword = ref('')
|
|
||||||
|
|
||||||
const filteredScriptList = computed(() => {
|
|
||||||
if (!searchKeyword.value) return scriptList.value
|
|
||||||
|
|
||||||
const keyword = searchKeyword.value.toLowerCase()
|
|
||||||
return scriptList.value.filter(item =>
|
|
||||||
(item.name && item.name.toLowerCase().includes(keyword)) ||
|
|
||||||
(item.description && item.description.toLowerCase().includes(keyword)) ||
|
|
||||||
(item.command && item.command.toLowerCase().includes(keyword))
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const paginatedFilteredList = computed(() => {
|
|
||||||
const start = (currentPage.value - 1) * pageSize.value
|
|
||||||
const end = start + pageSize.value
|
|
||||||
return filteredScriptList.value.slice(start, end)
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleSizeChange = (val) => {
|
|
||||||
pageSize.value = val
|
|
||||||
currentPage.value = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCurrentChange = (val) => {
|
|
||||||
currentPage.value = val
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
|
||||||
currentPage.value = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -292,13 +164,6 @@ const handleSearch = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
.add_script_btn {
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
.search_input {
|
|
||||||
width: 300px;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,10 +175,4 @@ const handleSearch = () => {
|
|||||||
color: #87cf63;
|
color: #87cf63;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
@ -30,6 +30,7 @@
|
|||||||
label-width="100px"
|
label-width="100px"
|
||||||
:show-message="false"
|
:show-message="false"
|
||||||
>
|
>
|
||||||
|
<transition-group name="list" mode="out-in" tag="div">
|
||||||
<el-form-item key="group" label="分组" prop="group">
|
<el-form-item key="group" label="分组" prop="group">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="hostForm.group"
|
v-model="hostForm.group"
|
||||||
@ -113,12 +114,14 @@
|
|||||||
<el-button type="primary" size="small" @click="handleClickUploadBtn">
|
<el-button type="primary" size="small" @click="handleClickUploadBtn">
|
||||||
本地私钥...
|
本地私钥...
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<!-- <el-button type="primary" size="small" @click="handleClickUploadBtn">
|
||||||
|
从凭据导入...
|
||||||
|
</el-button> -->
|
||||||
<input
|
<input
|
||||||
ref="privateKeyRef"
|
ref="privateKeyRef"
|
||||||
type="file"
|
type="file"
|
||||||
name="privateKey"
|
name="privateKey"
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
autocomplete="off"
|
|
||||||
@change="handleSelectPrivateKeyFile"
|
@change="handleSelectPrivateKeyFile"
|
||||||
>
|
>
|
||||||
<el-input
|
<el-input
|
||||||
@ -141,7 +144,7 @@
|
|||||||
v-model.trim="hostForm.password"
|
v-model.trim="hostForm.password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
autocomplete="new-password"
|
autocomplete="off"
|
||||||
clearable
|
clearable
|
||||||
show-password
|
show-password
|
||||||
/>
|
/>
|
||||||
@ -277,14 +280,12 @@
|
|||||||
placeholder="简单记录实例用途"
|
placeholder="简单记录实例用途"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</transition-group>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
<el-button @click="visible = false">关闭</el-button>
|
<el-button @click="visible = false">关闭</el-button>
|
||||||
<el-button v-if="!isBatchModify" type="primary" @click="handleSave">确认</el-button>
|
<el-button type="primary" @click="handleSave">确认</el-button>
|
||||||
<PlusSupportTip v-else>
|
|
||||||
<el-button type="primary" :disabled="!isPlusActive" @click="handleSave">确认</el-button>
|
|
||||||
</PlusSupportTip>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@ -326,7 +327,7 @@ const formField = {
|
|||||||
host: '',
|
host: '',
|
||||||
port: 22,
|
port: 22,
|
||||||
username: 'root',
|
username: 'root',
|
||||||
authType: 'privateKey', // privateKey, password, credential
|
authType: 'privateKey',
|
||||||
password: '',
|
password: '',
|
||||||
privateKey: '',
|
privateKey: '',
|
||||||
credential: '', // credentials -> _id
|
credential: '', // credentials -> _id
|
||||||
@ -342,6 +343,7 @@ const formField = {
|
|||||||
|
|
||||||
let hostForm = ref({ ...formField })
|
let hostForm = ref({ ...formField })
|
||||||
let privateKeyRef = ref(null)
|
let privateKeyRef = ref(null)
|
||||||
|
let oldHost = ref('')
|
||||||
let formRef = ref(null)
|
let formRef = ref(null)
|
||||||
|
|
||||||
let isBatchModify = computed(() => props.isBatchModify)
|
let isBatchModify = computed(() => props.isBatchModify)
|
||||||
@ -385,11 +387,9 @@ let confHostList = computed(() => {
|
|||||||
const setDefaultData = () => {
|
const setDefaultData = () => {
|
||||||
if (!defaultData.value) return
|
if (!defaultData.value) return
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
let { id, ...rest } = defaultData.value
|
let { host, monitorData, ...rest } = defaultData.value
|
||||||
for (let [key,] of Object.entries(hostForm.value)) {
|
oldHost.value = host
|
||||||
if (rest[key] !== undefined) hostForm.value[key] = rest[key]
|
Object.assign(hostForm.value, { host, ...rest })
|
||||||
}
|
|
||||||
hostForm.value.id = id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setBatchDefaultData = () => {
|
const setBatchDefaultData = () => {
|
||||||
@ -397,18 +397,15 @@ const setBatchDefaultData = () => {
|
|||||||
Object.assign(hostForm.value, { ...formField }, { group: '', port: '', username: '', authType: '', clientPort: '', jumpHosts: [] })
|
Object.assign(hostForm.value, { ...formField }, { group: '', port: '', username: '', authType: '', clientPort: '', jumpHosts: [] })
|
||||||
}
|
}
|
||||||
const handleOpen = async () => {
|
const handleOpen = async () => {
|
||||||
if (isBatchModify.value) {
|
|
||||||
setBatchDefaultData()
|
|
||||||
} else {
|
|
||||||
setDefaultData()
|
setDefaultData()
|
||||||
}
|
setBatchDefaultData()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
formRef.value.clearValidate()
|
formRef.value.clearValidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClosed = async () => {
|
const handleClosed = async () => {
|
||||||
emit('closed')
|
emit('closed')
|
||||||
hostForm.value = { ...formField }
|
Object.assign(hostForm.value, { ...formField })
|
||||||
await nextTick()
|
await nextTick()
|
||||||
formRef.value.resetFields()
|
formRef.value.resetFields()
|
||||||
}
|
}
|
||||||
@ -455,39 +452,30 @@ const handleSave = () => {
|
|||||||
let formData = { ...hostForm.value }
|
let formData = { ...hostForm.value }
|
||||||
if (isBatchModify.value) {
|
if (isBatchModify.value) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
let updateFieldData = Object.fromEntries(Object.entries(formData).filter(([key, value]) => {
|
let updateFileData = Object.fromEntries(Object.entries(formData).filter(([key, value]) => Boolean(value))) // 剔除掉未更改的值
|
||||||
if (Array.isArray(value)) return value.length > 0
|
if (Object.keys(updateFileData).length === 0) return $message.warning('没有任何修改')
|
||||||
return Boolean(value)
|
console.log(updateFileData)
|
||||||
})) // 剔除掉未更改的值
|
let newHosts = batchHosts.value
|
||||||
let { authType = '' } = updateFieldData
|
.map(item => ({ ...item, ...updateFileData }))
|
||||||
if (authType && !updateFieldData[authType]) {
|
.map(item => {
|
||||||
delete updateFieldData.authType
|
const { authType } = item
|
||||||
delete updateFieldData.privateKey
|
|
||||||
delete updateFieldData.password
|
|
||||||
delete updateFieldData.credential
|
|
||||||
}
|
|
||||||
if (Object.keys(updateFieldData).length === 0) return $message.warning('没有任何修改')
|
|
||||||
console.log(updateFieldData)
|
|
||||||
if (updateFieldData.authType) {
|
|
||||||
let tempKey = randomStr(16)
|
let tempKey = randomStr(16)
|
||||||
updateFieldData[authType] = AESEncrypt(updateFieldData[authType], tempKey)
|
if (item[authType]) item[authType] = AESEncrypt(item[authType], tempKey)
|
||||||
updateFieldData.tempKey = RSAEncrypt(tempKey)
|
item.tempKey = RSAEncrypt(tempKey)
|
||||||
}
|
return item
|
||||||
let updateIds = batchHosts.value.map(item => item.id)
|
})
|
||||||
let { msg } = await $api.batchUpdateHost({ updateIds, updateFieldData })
|
let { msg } = await $api.updateHost({ hosts: newHosts })
|
||||||
$message({ type: 'success', center: true, message: msg })
|
$message({ type: 'success', center: true, message: msg })
|
||||||
} else {
|
} else {
|
||||||
|
let tempKey = randomStr(16)
|
||||||
let { authType } = formData
|
let { authType } = formData
|
||||||
if (formData[authType]) {
|
if (formData[authType]) formData[authType] = AESEncrypt(formData[authType], tempKey)
|
||||||
let tempKey = randomStr(16)
|
|
||||||
formData[authType] = AESEncrypt(formData[authType], tempKey)
|
|
||||||
formData.tempKey = RSAEncrypt(tempKey)
|
formData.tempKey = RSAEncrypt(tempKey)
|
||||||
}
|
|
||||||
if (defaultData.value) {
|
if (defaultData.value) {
|
||||||
let { msg } = await $api.updateHost({ ...formData })
|
let { msg } = await $api.updateHost(Object.assign({}, formData, { oldHost: oldHost.value }))
|
||||||
$message({ type: 'success', center: true, message: msg })
|
$message({ type: 'success', center: true, message: msg })
|
||||||
} else {
|
} else {
|
||||||
let { msg } = await $api.addHost({ ...formData })
|
let { msg } = await $api.addHost(formData)
|
||||||
$message({ type: 'success', center: true, message: msg })
|
$message({ type: 'success', center: true, message: msg })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,7 +133,7 @@ const handleCsvFile = (event) => {
|
|||||||
|
|
||||||
const handleJsonFile = (event) => {
|
const handleJsonFile = (event) => {
|
||||||
let files = event.target.files
|
let files = event.target.files
|
||||||
let jsonFiles = Array.from(files).filter(file => file.name.endsWith('.json'))
|
let jsonFiles = Array.from(files).filter(file => file.type === 'application/json')
|
||||||
if (jsonFiles.length === 0) return $message.warning('未选择有效的JSON文件')
|
if (jsonFiles.length === 0) return $message.warning('未选择有效的JSON文件')
|
||||||
|
|
||||||
let readerPromises = jsonFiles.map(file => {
|
let readerPromises = jsonFiles.map(file => {
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
<div class="server_group_header">
|
<div class="server_group_header">
|
||||||
<!-- <el-button v-show="selectHosts.length" type="primary" @click="hostFormVisible = true">批量操作</el-button> -->
|
<!-- <el-button v-show="selectHosts.length" type="primary" @click="hostFormVisible = true">批量操作</el-button> -->
|
||||||
<el-button type="primary" class="add_host_btn" @click="hostFormVisible = true">添加实例</el-button>
|
<el-button type="primary" class="add_host_btn" @click="hostFormVisible = true">添加实例</el-button>
|
||||||
|
<!-- <el-button type="primary" @click="handleHiddenIP">
|
||||||
|
{{ hiddenIp ? '显示IP' : '隐藏IP' }}
|
||||||
|
</el-button> -->
|
||||||
|
<!-- <el-button type="primary" @click="importVisible = true">导入实例</el-button> -->
|
||||||
<el-dropdown trigger="click">
|
<el-dropdown trigger="click">
|
||||||
<el-button type="primary" class="group_action_btn">
|
<el-button type="primary" class="group_action_btn">
|
||||||
导入导出<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
导入导出<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||||
@ -78,15 +82,17 @@ import { exportFile } from '@/utils'
|
|||||||
|
|
||||||
const { proxy: { $api, $store, $router, $message, $messageBox, $tools } } = getCurrentInstance()
|
const { proxy: { $api, $store, $router, $message, $messageBox, $tools } } = getCurrentInstance()
|
||||||
|
|
||||||
const updateHostData = ref(null)
|
let updateHostData = ref(null)
|
||||||
const hostFormVisible = ref(false)
|
let hostFormVisible = ref(false)
|
||||||
const importVisible = ref(false)
|
let importVisible = ref(false)
|
||||||
const selectHosts = ref([])
|
let selectHosts = ref([])
|
||||||
const isBatchModify = ref(false)
|
let isBatchModify = ref(false)
|
||||||
const hostTableRefs = ref([])
|
const hostTableRefs = ref([])
|
||||||
const activeGroup = ref([])
|
|
||||||
|
|
||||||
const handleUpdateList = async () => {
|
let hiddenIp = ref(Number(localStorage.getItem('hiddenIp') || 0))
|
||||||
|
let activeGroup = ref([])
|
||||||
|
|
||||||
|
let handleUpdateList = async () => {
|
||||||
try {
|
try {
|
||||||
await $store.getHostList()
|
await $store.getHostList()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -164,6 +170,11 @@ let handleBatchExport = () => {
|
|||||||
hostTableRefs.value.forEach(item => item.clearSelection())
|
hostTableRefs.value.forEach(item => item.clearSelection())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let handleHiddenIP = () => {
|
||||||
|
hiddenIp.value = hiddenIp.value ? 0 : 1
|
||||||
|
localStorage.setItem('hiddenIp', String(hiddenIp.value))
|
||||||
|
}
|
||||||
|
|
||||||
let hostList = computed(() => $store.hostList)
|
let hostList = computed(() => $store.hostList)
|
||||||
|
|
||||||
let groupHostList = computed(() => {
|
let groupHostList = computed(() => {
|
||||||
|
@ -69,45 +69,20 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
<!-- Telegram -->
|
|
||||||
<template v-if="noticeConfig.type === 'tg'">
|
|
||||||
<el-form-item label="Token" prop="tg.token" class="form_item">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="noticeConfig.tg.token"
|
|
||||||
clearable
|
|
||||||
placeholder="Telegram Token"
|
|
||||||
autocomplete="off"
|
|
||||||
class="input"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="ChatId" prop="tg.chatId" class="form_item">
|
|
||||||
<el-input
|
|
||||||
v-model="noticeConfig.tg.chatId"
|
|
||||||
clearable
|
|
||||||
placeholder="Telegram ChatId"
|
|
||||||
autocomplete="off"
|
|
||||||
class="input"
|
|
||||||
/>
|
|
||||||
<span class="tips">Telegram Token/ChatId 获取: <a class="link" href="https://easynode.chaoszhu.com/zh/guide/get-tg-token" target="_blank">查看教程</a> </span>
|
|
||||||
</el-form-item>
|
|
||||||
</template>
|
|
||||||
<el-form-item label="" class="form_item">
|
<el-form-item label="" class="form_item">
|
||||||
<el-button
|
<el-button type="primary" :loading="loading" @click="handleSave">
|
||||||
type="primary"
|
|
||||||
:loading="loading"
|
|
||||||
@click="handleSave"
|
|
||||||
>
|
|
||||||
测试并保存
|
测试并保存
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<!-- <el-tooltip effect="dark" content="重复添加的邮箱将会被覆盖" placement="right">
|
||||||
|
</el-tooltip> -->
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, getCurrentInstance, computed } from 'vue'
|
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
|
||||||
// import PlusSupportTip from '@/components/common/PlusSupportTip.vue'
|
|
||||||
|
|
||||||
const { proxy: { $api, $notification, $store } } = getCurrentInstance()
|
const { proxy: { $api, $notification } } = getCurrentInstance()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const noticeConfig = ref({})
|
const noticeConfig = ref({})
|
||||||
@ -120,28 +95,14 @@ const noticeTypeList = ref([
|
|||||||
type: 'sct',
|
type: 'sct',
|
||||||
desc: 'Server酱'
|
desc: 'Server酱'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'tg',
|
|
||||||
desc: 'Telegram'
|
|
||||||
},
|
|
||||||
])
|
])
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
const isPlusActive = computed(() => $store.isPlusActive)
|
|
||||||
|
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
'sct.sendKey': { required: true, message: '需输入sendKey', trigger: 'change' },
|
'sct.sendKey': { required: true, message: '需输入sendKey', trigger: 'change' },
|
||||||
'email.service': { required: true, message: '需输入邮箱提供商', trigger: 'change' },
|
'email.service': { required: true, message: '需输入邮箱提供商', trigger: 'change' },
|
||||||
'email.user': { required: true, type: 'email', message: '需输入邮箱', trigger: 'change' },
|
'email.user': { required: true, type: 'email', message: '需输入邮箱', trigger: 'change' },
|
||||||
'email.pass': { required: true, message: '需输入邮箱SMTP授权码', trigger: 'change' },
|
'email.pass': { required: true, message: '需输入邮箱SMTP授权码', trigger: 'change' }
|
||||||
'tg.token': { required: true, message: '需输入Telegram Token', trigger: 'change' },
|
|
||||||
'tg.chatId': [
|
|
||||||
{ required: true, message: '需输入Telegram ChatId', trigger: 'change' },
|
|
||||||
{
|
|
||||||
pattern: /^-?\d+$/,
|
|
||||||
message: 'ChatId必须为数字',
|
|
||||||
trigger: ['blur', 'change',]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
@ -1,234 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-form
|
|
||||||
ref="formRef"
|
|
||||||
class="plus-form"
|
|
||||||
:model="formData"
|
|
||||||
:rules="rules"
|
|
||||||
:hide-required-asterisk="true"
|
|
||||||
label-suffix=":"
|
|
||||||
label-width="86px"
|
|
||||||
:show-message="false"
|
|
||||||
@submit.prevent
|
|
||||||
>
|
|
||||||
<el-form-item label="Plus Key" prop="key" class="form_item">
|
|
||||||
<el-input
|
|
||||||
v-model.trim="formData.key"
|
|
||||||
clearable
|
|
||||||
placeholder=""
|
|
||||||
autocomplete="off"
|
|
||||||
class="input"
|
|
||||||
@keyup.enter.prevent="handleUpdate"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<div class="form_footer">
|
|
||||||
<el-button type="primary" :loading="loading" @click="handleUpdate">立即激活</el-button>
|
|
||||||
<el-button type="success" @click="handlePlusSupport">
|
|
||||||
购买Plus
|
|
||||||
<el-icon class="el-icon--right"><TopRight /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
<span v-if="!isPlusActive && discount" class="discount_wrapper" @click="handlePlusSupport">
|
|
||||||
<img
|
|
||||||
class="discount_badge"
|
|
||||||
src="@/assets/discount.png"
|
|
||||||
alt="Discount"
|
|
||||||
>
|
|
||||||
<span class="discount_content">{{ discountContent }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<!-- Plus 激活状态信息 -->
|
|
||||||
<div v-if="isPlusActive" class="plus_status">
|
|
||||||
<div class="status_header">
|
|
||||||
<span>Plus专属功能已激活</span>
|
|
||||||
</div>
|
|
||||||
<div class="status_info">
|
|
||||||
<div class="info_item">
|
|
||||||
<span class="label">到期时间:</span>
|
|
||||||
<span class="value holder">{{ plusInfo.expiryDate }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info_item">
|
|
||||||
<span class="label">授权IP数:</span>
|
|
||||||
<span class="value">{{ plusInfo.maxIPs }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info_item">
|
|
||||||
<span class="label">已授权IP数:</span>
|
|
||||||
<span class="value">{{ plusInfo.usedIPCount }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info_item ip_list">
|
|
||||||
<span class="label">已授权IP:</span>
|
|
||||||
<div class="ip_tags">
|
|
||||||
<el-tag
|
|
||||||
v-for="ip in plusInfo.usedIPs"
|
|
||||||
:key="ip"
|
|
||||||
size="small"
|
|
||||||
class="ip_tag"
|
|
||||||
>
|
|
||||||
{{ ip }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<PlusTable />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, onMounted, getCurrentInstance, computed } from 'vue'
|
|
||||||
import { ElMessageBox } from 'element-plus'
|
|
||||||
import { TopRight } from '@element-plus/icons-vue'
|
|
||||||
import { handlePlusSupport } from '@/utils'
|
|
||||||
import PlusTable from '@/components/plus-table.vue'
|
|
||||||
|
|
||||||
const { proxy: { $api, $message, $store } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const errCount = ref(Number(localStorage.getItem('plusErrCount') || 0))
|
|
||||||
const loading = ref(false)
|
|
||||||
const formRef = ref(null)
|
|
||||||
const formData = reactive({
|
|
||||||
key: ''
|
|
||||||
})
|
|
||||||
const rules = reactive({
|
|
||||||
key: { required: true, message: '输入Plus Key', trigger: 'change' }
|
|
||||||
})
|
|
||||||
const discount = ref(false)
|
|
||||||
const discountContent = ref('')
|
|
||||||
|
|
||||||
const plusInfo = computed(() => $store.plusInfo)
|
|
||||||
const isPlusActive = computed(() => $store.isPlusActive)
|
|
||||||
|
|
||||||
const handleUpdate = () => {
|
|
||||||
formRef.value.validate()
|
|
||||||
.then(async () => {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
let { key } = formData
|
|
||||||
await $api.updatePlusKey({ key })
|
|
||||||
$message({ type: 'success', center: true, message: '激活成功,感谢支持' })
|
|
||||||
localStorage.setItem('plusErrCount', 0)
|
|
||||||
} catch (error) {
|
|
||||||
localStorage.setItem('plusErrCount', ++errCount.value)
|
|
||||||
if (errCount.value > 3) {
|
|
||||||
ElMessageBox.confirm(
|
|
||||||
'激活失败,请确认key正确(20位不规则字符串),有疑问请tg联系@chaoszhu。',
|
|
||||||
'Warning',
|
|
||||||
{
|
|
||||||
showCancelButton : false,
|
|
||||||
confirmButtonText: '确认',
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
loading.value = false
|
|
||||||
$store.getPlusInfo()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPlusConf = async () => {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
let { data } = await $api.getPlusConf()
|
|
||||||
formData.key = data
|
|
||||||
} catch (error) {
|
|
||||||
$message({ type: 'error', center: true, message: error.message })
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPlusDiscount = async () => {
|
|
||||||
const { data } = await $api.getPlusDiscount()
|
|
||||||
if (data?.discount) {
|
|
||||||
discount.value = data.discount
|
|
||||||
discountContent.value = data.content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getPlusConf()
|
|
||||||
getPlusDiscount()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.form_item {
|
|
||||||
.input {
|
|
||||||
width: 450px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form_footer {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: start;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
.discount_wrapper {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
.discount_badge {
|
|
||||||
margin: 0 5px 0 10px;
|
|
||||||
width: 22px;
|
|
||||||
color: white;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.discount_content {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.3;
|
|
||||||
color: #ff4806;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.plus_status {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
|
|
||||||
.status_header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: #67c23a;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status_info {
|
|
||||||
.info_item {
|
|
||||||
display: flex;
|
|
||||||
margin: 5px 0;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
color: #909399;
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.holder {
|
|
||||||
color: #EED183;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.ip_list {
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.ip_tags {
|
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
.ip_tag {
|
|
||||||
margin: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -9,16 +9,15 @@
|
|||||||
label-width="86px"
|
label-width="86px"
|
||||||
:show-message="false"
|
:show-message="false"
|
||||||
>
|
>
|
||||||
<el-form-item label="原用户名" prop="oldLoginName" class="form_item">
|
<el-form-item label="原用户名" prop="oldLoginName">
|
||||||
<el-input
|
<el-input
|
||||||
v-model.trim="formData.oldLoginName"
|
v-model.trim="formData.oldLoginName"
|
||||||
clearable
|
clearable
|
||||||
placeholder=""
|
placeholder=""
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="input"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="原密码" prop="oldPwd" class="form_item">
|
<el-form-item label="原密码" prop="oldPwd">
|
||||||
<el-input
|
<el-input
|
||||||
v-model.trim="formData.oldPwd"
|
v-model.trim="formData.oldPwd"
|
||||||
type="password"
|
type="password"
|
||||||
@ -26,19 +25,17 @@
|
|||||||
show-password
|
show-password
|
||||||
placeholder=""
|
placeholder=""
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="input"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="新用户名" prop="newLoginName" class="form_item">
|
<el-form-item label="新用户名" prop="newLoginName">
|
||||||
<el-input
|
<el-input
|
||||||
v-model.trim="formData.newLoginName"
|
v-model.trim="formData.newLoginName"
|
||||||
clearable
|
clearable
|
||||||
placeholder=""
|
placeholder=""
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="input"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="新密码" prop="newPwd" class="form_item">
|
<el-form-item label="新密码" prop="newPwd">
|
||||||
<el-input
|
<el-input
|
||||||
v-model.trim="formData.newPwd"
|
v-model.trim="formData.newPwd"
|
||||||
type="password"
|
type="password"
|
||||||
@ -46,7 +43,6 @@
|
|||||||
clearable
|
clearable
|
||||||
placeholder=""
|
placeholder=""
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="input"
|
|
||||||
@keyup.enter="handleUpdate"
|
@keyup.enter="handleUpdate"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -163,11 +159,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.password-form {
|
.password-form {
|
||||||
.form_item {
|
width: 500px;
|
||||||
.input {
|
|
||||||
width: 450px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.mfa2_title {
|
.mfa2_title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
@ -1,56 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="setting_container">
|
<div class="setting_container">
|
||||||
<el-tabs v-model="tabKey" tab-position="top">
|
<el-tabs tab-position="top">
|
||||||
<el-tab-pane label="修改密码" name="user">
|
<el-tab-pane label="修改密码" lazy>
|
||||||
<User />
|
<User />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="登录日志" name="record" lazy>
|
<el-tab-pane label="登录日志">
|
||||||
<Record />
|
<Record />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="全局通知" name="notify">
|
<el-tab-pane label="全局通知" lazy>
|
||||||
<GlobalNotify />
|
<GlobalNotify />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="通知配置" name="notify-config">
|
<el-tab-pane label="通知配置" lazy>
|
||||||
<NotifyConfig />
|
<NotifyConfig />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="Plus激活" name="plus">
|
|
||||||
<UserPlus />
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { watch, computed } from 'vue'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import GlobalNotify from './components/global-notify.vue'
|
import GlobalNotify from './components/global-notify.vue'
|
||||||
|
// import EmailList from './components/email-list.vue'
|
||||||
import Record from './components/record.vue'
|
import Record from './components/record.vue'
|
||||||
import User from './components/user.vue'
|
import User from './components/user.vue'
|
||||||
import NotifyConfig from './components/notify-config.vue'
|
import NotifyConfig from './components/notify-config.vue'
|
||||||
import UserPlus from './components/user-plus.vue'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const tabKey = computed({
|
|
||||||
get() {
|
|
||||||
return route.query.tabKey || 'user'
|
|
||||||
},
|
|
||||||
set(newVal) {
|
|
||||||
router.push({ query: { tabKey: newVal } })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => tabKey.value, (newVal) => {
|
|
||||||
router.push({ query: { tabKey: newVal } })
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.setting_container {
|
.setting_container {
|
||||||
height: 100%;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow: auto;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -168,7 +168,7 @@ import unknowIcon from '@/assets/image/system/unknow.png'
|
|||||||
const { io } = socketIo
|
const { io } = socketIo
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
hostId: {
|
host: {
|
||||||
required: true,
|
required: true,
|
||||||
type: String
|
type: String
|
||||||
}
|
}
|
||||||
@ -270,7 +270,7 @@ const connectSftp = () => {
|
|||||||
socket.value.on('connect', () => {
|
socket.value.on('connect', () => {
|
||||||
console.log('/sftp socket已连接:', socket.value.id)
|
console.log('/sftp socket已连接:', socket.value.id)
|
||||||
listenSftp()
|
listenSftp()
|
||||||
socket.value.emit('create', { hostId: props.hostId, token: token.value })
|
socket.value.emit('create', { host: props.host, token: token.value })
|
||||||
socket.value.on('root_ls', (tree) => {
|
socket.value.on('root_ls', (tree) => {
|
||||||
let temp = sortDirTree(tree).filter((item) => isDir(item.type))
|
let temp = sortDirTree(tree).filter((item) => isDir(item.type))
|
||||||
temp.unshift({ name: '/', type: 'd' })
|
temp.unshift({ name: '/', type: 'd' })
|
||||||
|
@ -190,8 +190,8 @@ const connectIO = () => {
|
|||||||
case 'io server disconnect':
|
case 'io server disconnect':
|
||||||
reconnectTerminal(true, '服务端主动断开连接')
|
reconnectTerminal(true, '服务端主动断开连接')
|
||||||
break
|
break
|
||||||
case 'io client disconnect': // 客户端主动断开连接
|
// case 'io client disconnect': // 客户端主动断开连接
|
||||||
break
|
// break
|
||||||
case 'transport close':
|
case 'transport close':
|
||||||
reconnectTerminal(true, '本地网络连接异常')
|
reconnectTerminal(true, '本地网络连接异常')
|
||||||
break
|
break
|
||||||
@ -208,10 +208,9 @@ const connectIO = () => {
|
|||||||
|
|
||||||
socket.value.on('connect_error', (err) => {
|
socket.value.on('connect_error', (err) => {
|
||||||
console.error('EasyNode服务端连接错误:', err)
|
console.error('EasyNode服务端连接错误:', err)
|
||||||
curStatus.value = CONNECT_FAIL
|
term.value.write('\r\n\x1b[91mError: 连接失败,请检查EasyNode服务端是否正常\x1b[0m \r\n')
|
||||||
term.value.write('\r\n\x1b[91mError: 连接失败,请检查EasyNode服务端是否正常, 回车重新发起连接\x1b[0m \r\n')
|
|
||||||
$notification({
|
$notification({
|
||||||
title: '服务端连接失败',
|
title: '连接失败',
|
||||||
message: '请检查EasyNode服务端是否正常',
|
message: '请检查EasyNode服务端是否正常',
|
||||||
type: 'error'
|
type: 'error'
|
||||||
})
|
})
|
||||||
@ -222,6 +221,7 @@ const reconnectTerminal = (isCommonTips = false, tips) => {
|
|||||||
socket.value.removeAllListeners()
|
socket.value.removeAllListeners()
|
||||||
socket.value.close()
|
socket.value.close()
|
||||||
socket.value = null
|
socket.value = null
|
||||||
|
curStatus.value = CONNECT_FAIL
|
||||||
socketConnected.value = false
|
socketConnected.value = false
|
||||||
if (isCommonTips) {
|
if (isCommonTips) {
|
||||||
if (isPlusActive.value && autoReconnect.value) {
|
if (isPlusActive.value && autoReconnect.value) {
|
||||||
@ -353,11 +353,12 @@ function extractLastCdPath(text) {
|
|||||||
|
|
||||||
const onData = () => {
|
const onData = () => {
|
||||||
term.value.onData((key) => {
|
term.value.onData((key) => {
|
||||||
|
if (!socket.value || !socketConnected.value) return
|
||||||
|
|
||||||
if ('\r' === key && curStatus.value === CONNECT_FAIL) {
|
if ('\r' === key && curStatus.value === CONNECT_FAIL) {
|
||||||
reconnectTerminal(false, '重新连接中...')
|
reconnectTerminal(false, '重新连接中...')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!socket.value || !socketConnected.value) return
|
|
||||||
|
|
||||||
if (isLongPressCtrl.value || isLongPressAlt.value) {
|
if (isLongPressCtrl.value || isLongPressAlt.value) {
|
||||||
const keyCode = key.toUpperCase().charCodeAt(0)
|
const keyCode = key.toUpperCase().charCodeAt(0)
|
||||||
|
@ -167,7 +167,7 @@
|
|||||||
<Sftp
|
<Sftp
|
||||||
v-if="showSftp"
|
v-if="showSftp"
|
||||||
ref="sftpRefs"
|
ref="sftpRefs"
|
||||||
:host-id="item.id"
|
:host="item.host"
|
||||||
@resize="resizeTerminal"
|
@resize="resizeTerminal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user