Compare commits

...

15 Commits
v3.0.2 ... main

Author SHA1 Message Date
chaos-zhu
4808f6e218 🆕 更新readme 2025-02-23 12:11:54 +08:00
chaos-zhu
9bd1cca518 🆕 切换到自建git代理 2025-02-18 22:08:41 +08:00
chaos-zhu
64d5db8c56 🐛 修复git代理host 2025-02-05 22:14:25 +08:00
chaos-zhu
37e1b891d3 🐛 移除任务 2024-12-30 21:56:52 +08:00
chaos-zhu
50ed2a8569 📝 更新描述 2024-12-24 22:49:53 +08:00
chaos-zhu
84b5f1beb6 🐛 修复通知测试按钮&自动重连机制 2024-12-24 22:40:14 +08:00
chaos-zhu
5f0e6e9ecc 📝 更新文档 2024-12-22 23:11:07 +08:00
chaos-zhu
0cbe43ecdd 优化移动端UI 2024-12-22 22:48:16 +08:00
chaos-zhu
0bef9b53af 前端支持激活plus 2024-12-22 22:42:00 +08:00
chaos-zhu
cbc6fa02ac 🐛 修复mfa2登录首字符为0时无法输入的bug&前端支持激活plus 2024-12-22 22:20:53 +08:00
chaos-zhu
9df142ccde 新增tg通知 2024-12-22 17:39:12 +08:00
chaos-zhu
6252f481d5 支持keyboard-interactive认证 2024-12-22 15:31:48 +08:00
chaos-zhu
aaf79fe60a
Merge pull request #122 from chaos-zhu/dependabot/npm_and_yarn/cross-spawn-7.0.6
⬆️ Bump cross-spawn from 7.0.3 to 7.0.6
2024-11-19 23:01:44 +08:00
chaos-zhu
d149e947bc 🐛 修复添加实例错误禁用的bug 2024-11-19 22:52:55 +08:00
dependabot[bot]
59b9938809
⬆️ Bump cross-spawn from 7.0.3 to 7.0.6
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-19 14:33:35 +00:00
30 changed files with 1681 additions and 213 deletions

View File

@ -1,3 +1,11 @@
## [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

View File

@ -79,16 +79,16 @@ docker run -d -p 8082:8082 --restart=always -v /root/easynode/db:/easynode/app/d
```shell
# 使用默认端口22022安装
curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash
curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash
# 使用自定义端口安装, 例如54321
curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash -s -- 54321
curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash -s -- 54321
```
> 卸载
```shell
curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash
curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash
```
> 查看监控服务状态:`systemctl status easynode-client`

View File

@ -10,7 +10,7 @@ SERVER_NAME=easynode-client
FILE_PATH=/root/local/easynode-client
SERVICE_PATH=/etc/systemd/system
CLIENT_VERSION=client-2024-10-13 # 目前监控客户端版本发布需手动更改为最新版本号
SERVER_PROXY="https://ghp.ci/"
SERVER_PROXY="https://git.221022.xyz/"
if [ ! -z "$1" ]; then
clientPort=$1

View File

@ -1,12 +1,12 @@
[
{
"name": "easynode监控服务安装",
"command": "curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash",
"command": "curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash",
"description": "easynode-监控服务-安装脚本"
},
{
"name": "easynode监控服务卸载",
"command": "curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash",
"command": "curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash",
"description": "easynode-监控服务-卸载脚本"
},
{

View File

@ -1,3 +1,5 @@
const path = require('path')
const decryptAndExecuteAsync = require('../utils/decrypt-file')
const { sendServerChan, sendEmail } = require('../utils/notify')
const { NotifyConfigDB, NotifyDB } = require('../utils/db-class')
const notifyDB = new NotifyDB().getInstance()
@ -5,7 +7,6 @@ const notifyConfigDB = new NotifyConfigDB().getInstance()
async function getNotifyConfig({ res }) {
const data = await notifyConfigDB.findOneAsync({})
console.log(data)
return res.success({ data })
}
@ -13,6 +14,7 @@ async function updateNotifyConfig({ res, request }) {
let { body: { noticeConfig } } = request
let { type } = noticeConfig
try {
// console.log('noticeConfig: ', noticeConfig[type])
switch (type) {
case 'sct':
await sendServerChan(noticeConfig[type]['sendKey'], 'EasyNode通知测试', '这是一条测试通知')
@ -20,6 +22,12 @@ async function updateNotifyConfig({ res, request }) {
case 'email':
await sendEmail(noticeConfig[type], 'EasyNode通知测试', '这是一条测试通知')
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 })
return res.success({ msg: '测试通过 | 保存成功' })

View File

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

View File

@ -3,11 +3,13 @@ const axios = require('axios')
const speakeasy = require('speakeasy')
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 { RSADecryptAsync, AESEncryptAsync, SHA1Encrypt } = require('../utils/encrypt')
const { getNetIPInfo } = require('../utils/tools')
const { KeyDB, LogDB, PlusDB } = require('../utils/db-class')
const keyDB = new KeyDB().getInstance()
const logDB = new LogDB().getInstance()
const plusDB = new PlusDB().getInstance()
@ -175,6 +177,7 @@ const getPlusInfo = async ({ res }) => {
}
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 {
@ -195,6 +198,18 @@ const getPlusDiscount = async ({ res } = {}) => {
}
}
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 = {
login,
getpublicKey,
@ -205,5 +220,7 @@ module.exports = {
enableMFA2,
disableMFA2,
getPlusInfo,
getPlusDiscount
getPlusDiscount,
getPlusConf,
updatePlusKey
}

View File

@ -71,8 +71,7 @@ async function initNotifyDB() {
async function initNotifyConfigDB() {
const notifyConfigDB = new NotifyConfigDB().getInstance()
let count = await notifyConfigDB.countAsync({})
if (count !== 0) return
let notifyConfig = await notifyConfigDB.findOneAsync({})
consola.log('初始化NotifyConfigDB✔')
const defaultData = {
type: 'sct',
@ -83,8 +82,17 @@ async function initNotifyConfigDB() {
service: 'QQ',
user: '',
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)
}

View File

@ -1,6 +1,6 @@
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand, decryptPrivateKey } = require('../controller/ssh')
const { getHostList, addHost, updateHost, batchUpdateHost, removeHost, importHost } = require('../controller/host')
const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMFA2Code, enableMFA2, disableMFA2, getPlusInfo, getPlusDiscount } = require('../controller/user')
const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMFA2Code, enableMFA2, disableMFA2, getPlusInfo, getPlusDiscount, getPlusConf, updatePlusKey } = require('../controller/user')
const { getNotifyConfig, updateNotifyConfig, getNotifyList, updateNotifyList } = require('../controller/notify')
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
const { getScriptList, getLocalScriptList, addScript, updateScriptList, removeScript, batchRemoveScript, importScript } = require('../controller/scripts')
@ -121,6 +121,16 @@ const user = [
method: 'get',
path: '/plus-discount',
controller: getPlusDiscount
},
{
method: 'get',
path: '/plus-conf',
controller: getPlusConf
},
{
method: 'post',
path: '/plus-conf',
controller: updatePlusKey
}
]
const notify = [

View File

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

View File

@ -88,11 +88,14 @@ async function createTerminal(hostId, socket, targetSSHClient) {
consola.error('连接终端失败:', host, err.message)
socket.emit('connect_terminal_fail', err.message)
})
.on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) {
finish([targetConnectionOptions[authType]])
})
.connect({
tryKeyboard: true,
...targetConnectionOptions
// debug: (info) => console.log(info)
})
} catch (err) {
consola.error('创建终端失败: ', host, err.message)
socket.emit('create_terminal_fail', err.message)

View File

@ -1,12 +1,17 @@
const schedule = require('node-schedule')
const { getLocalNetIP } = require('./tools')
const { AESEncryptAsync } = require('./encrypt')
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() {
let key = process.env.PLUS_KEY
if (!key || typeof key !== 'string' || key.length < 20) return
async function getLicenseInfo(key = '') {
const { key: plusKey } = await plusDB.findOneAsync({}) || {}
// console.log('plusKey: ', plusKey)
// 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 = ''
if (global.serverIp && (Date.now() - global.getServerIpLastTime) / 1000 / 60 < 60) {
ip = global.serverIp
@ -20,7 +25,7 @@ async function getLicenseInfo() {
if (!ip) {
consola.error('activate plus failed: get public ip failed')
global.serverIp = ''
return
return { success: false, msg: 'get public ip failed' }
}
try {
let response
@ -64,9 +69,7 @@ async function getLicenseInfo() {
let { decryptKey, expiryDate, usedIPCount, maxIPs, usedIPs } = data
decryptKey = await AESEncryptAsync(decryptKey)
consola.success('activate plus success')
const { PlusDB } = require('./db-class')
const plusData = { key, decryptKey, expiryDate, usedIPCount, maxIPs, usedIPs }
const plusDB = new PlusDB().getInstance()
let count = await plusDB.countAsync({})
if (count === 0) {
await plusDB.insertAsync(plusData)
@ -74,21 +77,17 @@ async function getLicenseInfo() {
await plusDB.removeAsync({}, { multi: true })
await plusDB.insertAsync(plusData)
}
return { success: true, msg: '激活成功' }
}
consola.error('activate plus failed: ', data)
return { success: false, msg: '激活失败' }
} catch (error) {
consola.error(`activate plus failed: ${ error.message || error.errMsg?.message }`)
if (error.clear) {
const { PlusDB } = require('./db-class')
const plusDB = new PlusDB().getInstance()
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

View File

@ -1,3 +1,5 @@
const path = require('path')
const decryptAndExecuteAsync = require('./decrypt-file')
const nodemailer = require('nodemailer')
const axios = require('axios')
const commonTemp = require('../template/commonTemp')
@ -6,7 +8,6 @@ const notifyConfigDB = new NotifyConfigDB().getInstance()
const notifyDB = new NotifyDB().getInstance()
function sendServerChan(sendKey, title, content) {
if (!sendKey) return consola.error('发送server酱通知失败, sendKey 为空')
return new Promise((async (resolve, reject) => {
try {
consola.info('server酱通知预发送: ', title)
@ -28,7 +29,6 @@ function sendServerChan(sendKey, 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) => {
try {
consola.info('邮箱通知预发送: ', title)
@ -60,8 +60,8 @@ async function sendNoticeAsync(noticeAction, title, content) {
try {
let notifyList = await notifyDB.findAsync({})
let { sw } = notifyList.find((item) => item.type === noticeAction) // 获取对应动作的通知开关
console.log('notify swtich: ', noticeAction, sw)
if (!sw) return
// console.log('notify swtich: ', noticeAction, sw)
if (!sw) return consola.info('通知开关关闭, 不发送通知: ', noticeAction)
let notifyConfig = await notifyConfigDB.findOneAsync({})
let { type } = notifyConfig
if (!type) return consola.error('通知类型不存在: ', type)
@ -78,7 +78,15 @@ async function sendNoticeAsync(noticeAction, title, content) {
if (!service || !user || !pass) return consola.info('未发送邮件通知通知, 未配置邮箱: ', { service, user, pass })
await sendEmail({ service, user, pass }, title, content)
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:
consola.info('未配置通知类型: ', type)
break
}
} catch (error) {

1
server/app/utils/plus.js Normal file
View File

@ -0,0 +1 @@
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=

View File

@ -1,6 +1,6 @@
{
"name": "server",
"version": "3.0.1",
"version": "3.0.3",
"description": "easynode-server",
"bin": "./bin/www",
"scripts": {
@ -43,6 +43,7 @@
"node-os-utils": "^1.3.7",
"node-rsa": "^1.1.1",
"node-schedule": "^2.1.1",
"node-telegram-bot-api": "^0.66.0",
"nodemailer": "^6.9.14",
"qrcode": "^1.5.4",
"socket.io": "^4.7.5",

View File

@ -1,6 +1,6 @@
{
"name": "web",
"version": "3.0.1",
"version": "3.0.3",
"description": "easynode-web",
"private": true,
"scripts": {

View File

@ -126,5 +126,11 @@ export default {
},
getEasynodeVersion() {
return axios({ url: '/version', method: 'get' })
},
getPlusConf() {
return axios({ url: '/plus-conf', method: 'get' })
},
updatePlusKey(data) {
return axios({ url: '/plus-conf', method: 'post', data })
}
}

View File

@ -10,7 +10,7 @@
size="small"
type="primary"
link
@click="handlePlusSupport"
@click="gotoPlusPage"
>
去激活
</el-button>
@ -22,13 +22,15 @@
<script setup>
import { computed, getCurrentInstance } from 'vue'
import { useRouter } from 'vue-router'
const { proxy: { $store } } = getCurrentInstance()
const router = useRouter()
const isPlusActive = computed(() => $store.isPlusActive)
const handlePlusSupport = () => {
window.open('https://en.221022.xyz/buy-plus', '_blank')
const gotoPlusPage = () => {
router.push('/setting?tabKey=plus')
}
</script>

View File

@ -0,0 +1,206 @@
<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> -->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<strong>EasyNode</strong>最初是一个简单的Web终端工具随着用户群的不断扩大功能需求也日益增长为了实现大家的功能需求我投入了大量的业余时间进行开发和维护
一直在为爱发电渐渐的也没了开发的动力
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;为了项目的可持续发展<strong>后续</strong>版本开始推出<strong>PLUS</strong>版本后续特性功能开发也会优先在<strong>PLUS</strong>版本中实现但即使不升级到<strong>PLUS</strong>也不会影响到<strong>EasyNode</strong>的基础功能使用注意:
暂不支持纯内网用户激活PLUS功能
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>

View File

@ -37,107 +37,26 @@
</el-dropdown-menu>
</template>
</el-dropdown>
<el-popover placement="left" :width="320" trigger="hover">
<template #reference>
<div class="plus_icon_wrapper">
<img
class="plus_icon"
src="@/assets/plus.png"
alt="PLUS"
:style="{ filter: isPlusActive ? 'grayscale(0%)' : 'grayscale(100%)' }"
>
<img
v-if="!isPlusActive && discount"
class="discount_badge"
src="@/assets/discount.png"
alt="Discount"
>
</div>
</template>
<template #default>
<div class="plus_content_wrap">
<!-- Plus 激活状态信息 -->
<div v-if="isPlusActive" class="plus_status">
<div class="status_header">
<el-icon>
<CircleCheckFilled />
</el-icon>
<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>
<div class="plus_benefits" :class="{ active: isPlusActive }" @click="handlePlus">
<span v-if="!isPlusActive" class="support_btn" @click="handlePlusSupport">激活PLUS</span>
<div v-if="!isPlusActive && discount" class="discount_content" @click="handlePlusSupport">
<el-tag type="warning" effect="dark">
<el-icon><Promotion /></el-icon>
<span>{{ discountContent || '限时优惠活动' }}</span>
</el-tag>
</div>
<div class="benefits_header">
<el-icon>
<StarFilled />
</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 class="plus_icon_wrapper" @click="gotoPlusPage">
<img
class="plus_icon"
src="@/assets/plus.png"
alt="PLUS"
:style="{ filter: isPlusActive ? 'grayscale(0%)' : 'grayscale(100%)' }"
>
<img
v-if="!isPlusActive && discount"
class="discount_badge"
src="@/assets/discount.png"
alt="Discount"
>
</div>
</div>
<el-dialog
v-model="visible"
title="版本更新"
top="10vh"
top="20vh"
width="30%"
:append-to-body="false"
:close-on-click-modal="false"
@ -163,20 +82,8 @@
<p>
TG更新通知频道<a class="link" href="https://t.me/easynode_notify" target="_blank">https://t.me/easynode_notify</a>
</p>
<p style="line-height: 2;letter-spacing: 1px;">
<strong style="color: #F56C6C;font-weight: 600;">PLUS说明:</strong><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<strong>EasyNode</strong>最初是一个简单的Web终端工具随着用户群的不断扩大功能需求也日益增长为了实现大家的功能需求我投入了大量的业余时间进行开发和维护
一直在为爱发电渐渐的也没了开发的动力
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;为了项目的可持续发展<strong>3.0.0</strong>版本开始推出了<strong>PLUS</strong>版本具体特性鼠标悬浮右上角PLUS图标查看后续特性功能开发也会优先在<strong>PLUS</strong>版本中实现但即使不升级到<strong>PLUS</strong>也不会影响到<strong>EasyNode</strong>的基础功能使用注意: 暂不支持纯内网用户激活PLUS功能
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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="visible = false">关闭</el-button>
<el-button v-if="!isPlusActive" type="primary" @click="handlePlusSupport">激活PLUS</el-button>
</div>
</div>
</el-dialog>
@ -198,38 +105,23 @@
<script setup>
import { ref, getCurrentInstance, computed, onMounted, onBeforeUnmount } from 'vue'
import { User, Sunny, Moon, Fold, CircleCheckFilled, Star, StarFilled, Promotion } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import { User, Sunny, Moon, Fold } from '@element-plus/icons-vue'
import packageJson from '../../package.json'
import MenuList from './menuList.vue'
const { proxy: { $router, $store, $api, $message } } = getCurrentInstance()
const router = useRouter()
const visible = ref(false)
const checkVersionErr = ref(false)
const currentVersion = ref(`v${ packageJson.version }`)
const latestVersion = ref(null)
const menuCollapse = ref(false)
const discount = ref(false)
const discountContent = ref('')
const plusFeatures = [
'跳板机功能,拯救被墙实例与龟速终端输入',
'本地socket断开自动重连,无需手动重新连接',
'批量修改实例配置(优化版)',
'脚本库批量导出导入',
'凭据管理支持解密带密码保护的密钥',
'提出的功能需求享有更高的开发优先级',
]
// const soonFeatures = [
// '',
// '',
// '',
// ]
const isNew = computed(() => latestVersion.value && latestVersion.value !== currentVersion.value)
const user = computed(() => $store.user)
const title = computed(() => $store.title)
const plusInfo = computed(() => $store.plusInfo)
const isPlusActive = computed(() => $store.isPlusActive)
const isDark = computed({
@ -249,8 +141,8 @@ const handleLogout = () => {
$router.push('/login')
}
const handlePlusSupport = () => {
window.open('https://en.221022.xyz/buy-plus', '_blank')
const gotoPlusPage = () => {
router.push('/setting?tabKey=plus')
}
async function checkLatestVersion() {
@ -301,7 +193,6 @@ const getPlusDiscount = async () => {
const { data } = await $api.getPlusDiscount()
if (data?.discount) {
discount.value = data.discount
discountContent.value = data.content
}
}

View File

@ -101,7 +101,7 @@ const useStore = defineStore({
this.$patch({ localScriptList })
},
async getPlusInfo() {
const { data: plusInfo } = await $api.getPlusInfo()
const { data: plusInfo = {} } = await $api.getPlusInfo()
if (plusInfo?.expiryDate) {
const isPlusActive = new Date(plusInfo.expiryDate) > new Date()
this.$patch({ isPlusActive })
@ -111,6 +111,9 @@ const useStore = defineStore({
}
plusInfo.expiryDate = dayjs(plusInfo.expiryDate).format('YYYY-MM-DD')
plusInfo.expiryDate?.startsWith('9999') && (plusInfo.expiryDate = '永久授权')
this.$patch({ plusInfo })
} else {
this.$patch({ isPlusActive: false })
}
this.$patch({ plusInfo })
},

View File

@ -149,3 +149,7 @@ export const isMobile = () => {
let userAgent = navigator.userAgent || navigator.vendor || window.opera
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')
}

View File

@ -46,7 +46,7 @@
</el-form-item>
<el-form-item prop="mfa2Token" label="MFA2验证码">
<el-input
v-model.trim.number="loginForm.mfa2Token"
v-model="loginForm.mfa2Token"
type="text"
placeholder="MFA2应用上的6位数字(未设置可忽略)"
autocomplete="off"
@ -103,7 +103,7 @@ const loginForm = reactive({
const rules = reactive({
loginName: { required: true, message: '需输入用户名', trigger: 'change' },
pwd: { required: true, message: '需输入密码', trigger: 'change' },
mfa2Token: { required: false, message: '需输入码', trigger: 'change' }
mfa2Token: { required: false, message: '需输入MFA2验证码', trigger: 'change' }
})
const handleLogin = () => {
@ -124,7 +124,7 @@ const handleLogin = () => {
if (ciphertext === -1) return $message.error({ message: '公钥加载失败', center: true })
loading.value = true
try {
let { data, msg } = await $api.login({ loginName, ciphertext, jwtExpires, mfa2Token })
let { data, msg } = await $api.login({ loginName, ciphertext, jwtExpires, mfa2Token: Number(mfa2Token) })
const { token } = data
$store.setJwtToken(token, expireEnum.ONE_SESSION === expireTime.value)
$store.setUser(loginName)

View File

@ -426,7 +426,7 @@ onActivated(async () => {
const { hostIds, execClientInstallScript } = route.query
if (!hostIds) return
if (execClientInstallScript === 'true') {
let clientInstallScript = 'curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash\n'
let clientInstallScript = 'curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash\n'
console.log(hostIds.split(','))
createExecShell(hostIds.split(','), clientInstallScript, 300)
// $messageBox.confirm(`${ host }`, 'Warning', {
@ -435,7 +435,7 @@ onActivated(async () => {
// type: 'warning'
// })
// .then(async () => {
// let clientInstallScript = 'curl -o- https://ghp.ci/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash\n'
// let clientInstallScript = 'curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash\n'
// createExecShell([host,], clientInstallScript, 300)
// })
}

View File

@ -69,20 +69,45 @@
/>
</el-form-item>
</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-button type="primary" :loading="loading" @click="handleSave">
<el-button
type="primary"
:loading="loading"
@click="handleSave"
>
测试并保存
</el-button>
<!-- <el-tooltip effect="dark" content="重复添加的邮箱将会被覆盖" placement="right">
</el-tooltip> -->
</el-form-item>
</el-form>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
import { ref, reactive, onMounted, getCurrentInstance, computed } from 'vue'
// import PlusSupportTip from '@/components/common/PlusSupportTip.vue'
const { proxy: { $api, $notification } } = getCurrentInstance()
const { proxy: { $api, $notification, $store } } = getCurrentInstance()
const loading = ref(false)
const noticeConfig = ref({})
@ -95,14 +120,28 @@ const noticeTypeList = ref([
type: 'sct',
desc: 'Server酱'
},
{
type: 'tg',
desc: 'Telegram'
},
])
const formRef = ref(null)
const isPlusActive = computed(() => $store.isPlusActive)
const rules = reactive({
'sct.sendKey': { required: true, message: '需输入sendKey', trigger: 'change' },
'email.service': { required: true, 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 = () => {

View File

@ -0,0 +1,234 @@
<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>

View File

@ -9,15 +9,16 @@
label-width="86px"
:show-message="false"
>
<el-form-item label="原用户名" prop="oldLoginName">
<el-form-item label="原用户名" prop="oldLoginName" class="form_item">
<el-input
v-model.trim="formData.oldLoginName"
clearable
placeholder=""
autocomplete="off"
class="input"
/>
</el-form-item>
<el-form-item label="原密码" prop="oldPwd">
<el-form-item label="原密码" prop="oldPwd" class="form_item">
<el-input
v-model.trim="formData.oldPwd"
type="password"
@ -25,17 +26,19 @@
show-password
placeholder=""
autocomplete="off"
class="input"
/>
</el-form-item>
<el-form-item label="新用户名" prop="newLoginName">
<el-form-item label="新用户名" prop="newLoginName" class="form_item">
<el-input
v-model.trim="formData.newLoginName"
clearable
placeholder=""
autocomplete="off"
class="input"
/>
</el-form-item>
<el-form-item label="新密码" prop="newPwd">
<el-form-item label="新密码" prop="newPwd" class="form_item">
<el-input
v-model.trim="formData.newPwd"
type="password"
@ -43,6 +46,7 @@
clearable
placeholder=""
autocomplete="off"
class="input"
@keyup.enter="handleUpdate"
/>
</el-form-item>
@ -159,7 +163,11 @@ onMounted(() => {
<style lang="scss" scoped>
.password-form {
width: 500px;
.form_item {
.input {
width: 450px;
}
}
}
.mfa2_title {
font-size: 18px;

View File

@ -1,33 +1,56 @@
<template>
<div class="setting_container">
<el-tabs tab-position="top">
<el-tab-pane label="修改密码" lazy>
<el-tabs v-model="tabKey" tab-position="top">
<el-tab-pane label="修改密码" name="user">
<User />
</el-tab-pane>
<el-tab-pane label="登录日志">
<el-tab-pane label="登录日志" name="record" lazy>
<Record />
</el-tab-pane>
<el-tab-pane label="全局通知" lazy>
<el-tab-pane label="全局通知" name="notify">
<GlobalNotify />
</el-tab-pane>
<el-tab-pane label="通知配置" lazy>
<el-tab-pane label="通知配置" name="notify-config">
<NotifyConfig />
</el-tab-pane>
<el-tab-pane label="Plus激活" name="plus">
<UserPlus />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { watch, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import GlobalNotify from './components/global-notify.vue'
// import EmailList from './components/email-list.vue'
import Record from './components/record.vue'
import User from './components/user.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>
<style lang="scss" scoped>
.setting_container {
height: 100%;
padding: 20px;
overflow: auto;
}
</style>

View File

@ -211,7 +211,7 @@ const connectIO = () => {
curStatus.value = CONNECT_FAIL
term.value.write('\r\n\x1b[91mError: 连接失败,请检查EasyNode服务端是否正常, 回车重新发起连接\x1b[0m \r\n')
$notification({
title: '连接失败',
title: '服务端连接失败',
message: '请检查EasyNode服务端是否正常',
type: 'error'
})
@ -222,7 +222,6 @@ const reconnectTerminal = (isCommonTips = false, tips) => {
socket.value.removeAllListeners()
socket.value.close()
socket.value = null
curStatus.value = CONNECT_FAIL
socketConnected.value = false
if (isCommonTips) {
if (isPlusActive.value && autoReconnect.value) {
@ -354,12 +353,11 @@ function extractLastCdPath(text) {
const onData = () => {
term.value.onData((key) => {
if (!socket.value || !socketConnected.value) return
if ('\r' === key && curStatus.value === CONNECT_FAIL) {
reconnectTerminal(false, '重新连接中...')
return
}
if (!socket.value || !socketConnected.value) return
if (isLongPressCtrl.value || isLongPressAlt.value) {
const keyCode = key.toUpperCase().charCodeAt(0)

1029
yarn.lock

File diff suppressed because it is too large Load Diff