更新版本通知

This commit is contained in:
chaos-zhu 2024-11-10 10:24:09 +08:00
parent c04989b951
commit 079c62b838
15 changed files with 176 additions and 47 deletions

1
.gitignore vendored
View File

@ -14,3 +14,4 @@ plan.md
.env-encrypt-key .env-encrypt-key
*clear.js *clear.js
local-script local-script
版本发布.md

View File

@ -1,3 +1,9 @@
## [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) ## [3.0.0](https://github.com/chaos-zhu/easynode/releases) (2024-11-09)
* 新增跳板机功能,支持选择多台机器跳转 * 新增跳板机功能,支持选择多台机器跳转

View File

@ -66,11 +66,9 @@ _✨ 一个多功能Linux服务器WEB终端面板(webSSH&webSFTP) ✨_
docker run -d -p 8082:8082 --restart=always -v /root/easynode/db:/easynode/app/db chaoszhu/easynode docker run -d -p 8082:8082 --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`
## 监控服务安装 ## 监控服务安装
- 监控服务用于实时向服务端&web端推送**系统、公网IP、CPU、内存、硬盘、网卡**等基础信息 - 监控服务用于实时向服务端&web端推送**系统、公网IP、CPU、内存、硬盘、网卡**等基础信息

View File

@ -2,6 +2,8 @@ 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 { 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')
@ -86,7 +88,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) // 记录客户端登录IP(用于判断是否异地且只保留最近10<EFBFBD><EFBFBD>)
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 })
@ -172,6 +174,27 @@ const getPlusInfo = async ({ res }) => {
res.success({ data, msg: 'success' }) res.success({ data, msg: 'success' })
} }
const getPlusDiscount = async ({ res } = {}) => {
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
}
}
}
module.exports = { module.exports = {
login, login,
getpublicKey, getpublicKey,
@ -181,5 +204,6 @@ module.exports = {
getMFA2Code, getMFA2Code,
enableMFA2, enableMFA2,
disableMFA2, disableMFA2,
getPlusInfo getPlusInfo,
getPlusDiscount
} }

View File

@ -1,6 +1,6 @@
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand, decryptPrivateKey } = require('../controller/ssh') const { getSSHList, addSSH, updateSSH, removeSSH, getCommand, decryptPrivateKey } = require('../controller/ssh')
const { getHostList, addHost, updateHost, batchUpdateHost, removeHost, importHost } = require('../controller/host') const { getHostList, addHost, updateHost, batchUpdateHost, removeHost, importHost } = require('../controller/host')
const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMFA2Code, enableMFA2, disableMFA2, getPlusInfo } = require('../controller/user') const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMFA2Code, enableMFA2, disableMFA2, getPlusInfo, getPlusDiscount } = 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, batchRemoveScript, importScript } = require('../controller/scripts')
@ -116,6 +116,11 @@ const user = [
method: 'get', method: 'get',
path: '/plus-info', path: '/plus-info',
controller: getPlusInfo controller: getPlusInfo
},
{
method: 'get',
path: '/plus-discount',
controller: getPlusDiscount
} }
] ]
const notify = [ const notify = [

View File

@ -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 ({ host: ip, token }) => { socket.on('create', async ({ hostId, 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 })
const hostList = await hostListDB.findAsync({}) if (!targetHostInfo) throw new Error(`Host with ID ${ hostId } not found`)
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', `查找${ ip }】凭证信息失败`) if (!host) return socket.emit('create_fail', `查找id【${ hostId }】凭证信息失败`)
let authInfo = { host, port, username } let authInfo = { host, port, username }
// 解密放到try里面防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】 // 解密放到try里面防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】

View File

@ -2,6 +2,7 @@ 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')
async function getLicenseInfo() { async function getLicenseInfo() {
let key = process.env.PLUS_KEY let key = process.env.PLUS_KEY
@ -28,7 +29,7 @@ async function getLicenseInfo() {
let headers = { 'Content-Type': 'application/json' } let headers = { 'Content-Type': 'application/json' }
let timeout = 10000 let timeout = 10000
try { try {
response = await fetch('https://en1.221022.xyz/api/licenses/activate', { response = await fetch(plusServer1 + '/api/licenses/activate', {
method, method,
headers, headers,
body, body,
@ -41,7 +42,7 @@ async function getLicenseInfo() {
} 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('https://en2.221022.xyz/api/licenses/activate', { response = await fetch(plusServer2 + '/api/licenses/activate', {
method, method,
headers, headers,
body, body,

View File

@ -0,0 +1,4 @@
module.exports = {
plusServer1: 'https://en1.221022.xyz',
plusServer2: 'https://en2.221022.xyz'
}

View File

@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "3.0.0", "version": "3.0.1",
"description": "easynode-server", "description": "easynode-server",
"bin": "./bin/www", "bin": "./bin/www",
"scripts": { "scripts": {

View File

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

View File

@ -22,6 +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 } })
}, },

BIN
web/src/assets/discount.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -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">
@ -40,12 +40,20 @@
<el-popover placement="left" :width="320" trigger="hover"> <el-popover placement="left" :width="320" trigger="hover">
<template #reference> <template #reference>
<div class="plus_icon_wrapper">
<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
v-if="!isPlusActive && discount"
class="discount_badge"
src="@/assets/discount.png"
alt="Discount"
>
</div>
</template> </template>
<template #default> <template #default>
<div class="plus_content_wrap"> <div class="plus_content_wrap">
@ -87,10 +95,16 @@
</div> </div>
<div class="plus_benefits" :class="{ active: isPlusActive }" @click="handlePlus"> <div class="plus_benefits" :class="{ active: isPlusActive }" @click="handlePlus">
<span v-if="!isPlusActive" class="support_btn" @click="handlePlusSupport">去支持</span> <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"> <div class="benefits_header">
<el-icon> <el-icon>
<el-icon><StarFilled /></el-icon> <StarFilled />
</el-icon> </el-icon>
<span>Plus功能介绍</span> <span>Plus功能介绍</span>
</div> </div>
@ -103,7 +117,7 @@
</div> </div>
</div> </div>
<div class="coming_soon"> <!-- <div class="coming_soon">
<div class="soon_header">开发中的PLUS功能</div> <div class="soon_header">开发中的PLUS功能</div>
<div class="current_benefits"> <div class="current_benefits">
<div v-for="soonFeature in soonFeatures" :key="soonFeature" class="benefit_item"> <div v-for="soonFeature in soonFeatures" :key="soonFeature" class="benefit_item">
@ -113,7 +127,7 @@
<span>{{ soonFeature }}</span> <span>{{ soonFeature }}</span>
</div> </div>
</div> </div>
</div> </div> -->
</div> </div>
</div> </div>
</template> </template>
@ -122,15 +136,16 @@
<el-dialog <el-dialog
v-model="visible" v-model="visible"
title="关于" title="版本更新"
top="10vh" 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需要外网环境)</p> <p v-if="checkVersionErr" class="conspicuous">Error版本更新检测失败(版本检测API需要外网环境),请手动访问GitHub查看</p>
<p v-if="isNew" class="conspicuous"> <p v-if="isNew" class="conspicuous">
新版本可用: {{ latestVersion }} -> <a 新版本可用: {{ latestVersion }} -> <a
class="link" class="link"
@ -139,14 +154,14 @@
>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>
<p style="line-height: 2;letter-spacing: 1px;"> <p style="line-height: 2;letter-spacing: 1px;">
<strong style="color: #F56C6C;font-weight: 600;">PLUS说明:</strong><br> <strong style="color: #F56C6C;font-weight: 600;">PLUS说明:</strong><br>
@ -156,11 +171,12 @@
&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功能 &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> <br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="text-decoration: underline;"> &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>授权码 为了感谢前期赞赏过的用户, <strong>PLUS</strong>功能正式发布前所有进行过赞赏的用户无论金额大小均可联系作者TG: <a class="link" href="https://t.me/chaoszhu" target="_blank">@chaoszhu</a> 凭打赏记录免费获取永久<strong>PLUS</strong>授权码
</span> </span>
</p> </p>
<div v-if="!isPlusActive" class="about_footer"> <div class="about_footer">
<el-button type="primary" @click="handlePlusSupport">去支持</el-button> <el-button type="info" @click="visible = false">关闭</el-button>
<el-button v-if="!isPlusActive" type="primary" @click="handlePlusSupport">激活PLUS</el-button>
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
@ -181,18 +197,20 @@
</template> </template>
<script setup> <script setup>
import { ref, getCurrentInstance, computed } from 'vue' import { ref, getCurrentInstance, computed, onMounted, onBeforeUnmount } from 'vue'
import { User, Sunny, Moon, Fold, CircleCheckFilled, Star, StarFilled } from '@element-plus/icons-vue' import { User, Sunny, Moon, Fold, CircleCheckFilled, Star, StarFilled, Promotion } 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, $message } } = getCurrentInstance() const { proxy: { $router, $store, $api, $message } } = getCurrentInstance()
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 discountContent = ref('')
const plusFeatures = [ const plusFeatures = [
'跳板机功能,拯救被墙实例与龟速终端输入', '跳板机功能,拯救被墙实例与龟速终端输入',
@ -202,11 +220,11 @@ const plusFeatures = [
'凭据管理支持解密带密码保护的密钥', '凭据管理支持解密带密码保护的密钥',
'提出的功能需求享有更高的开发优先级', '提出的功能需求享有更高的开发优先级',
] ]
const soonFeatures = [ // const soonFeatures = [
'终端脚本变量及终端脚本输入优化', // '',
'终端分屏功能', // '',
'系统操作日志审计', // '',
] // ]
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)
@ -268,6 +286,33 @@ 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
discountContent.value = data.content
}
}
onMounted(() => {
checkFirstVisit()
getPlusDiscount()
})
onBeforeUnmount(() => {
clearTimeout(timer)
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -314,10 +359,26 @@ checkLatestVersion()
} }
} }
.plus_icon { .plus_icon_wrapper {
margin-left: 15px; margin-left: 15px;
width: 35px; display: flex;
align-items: center;
cursor: pointer; cursor: pointer;
.plus_icon {
width: 35px;
margin-right: 5px;
}
.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;
}
} }
} }
@ -354,6 +415,18 @@ checkLatestVersion()
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">
@ -460,5 +533,19 @@ checkLatestVersion()
} }
} }
} }
.discount_content {
margin: 8px 0;
.el-tag {
display: flex;
align-items: center;
padding: 6px 12px;
.el-icon {
margin-right: 4px;
}
}
}
} }
</style> </style>

View File

@ -168,7 +168,7 @@ import unknowIcon from '@/assets/image/system/unknow.png'
const { io } = socketIo const { io } = socketIo
const props = defineProps({ const props = defineProps({
host: { hostId: {
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', { host: props.host, token: token.value }) socket.value.emit('create', { hostId: props.hostId, 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' })

View File

@ -167,7 +167,7 @@
<Sftp <Sftp
v-if="showSftp" v-if="showSftp"
ref="sftpRefs" ref="sftpRefs"
:host="item.host" :host-id="item.id"
@resize="resizeTerminal" @resize="resizeTerminal"
/> />
</div> </div>