🐛 修复mfa2登录首字符为0时无法输入的bug&前端支持激活plus
This commit is contained in:
parent
9df142ccde
commit
cbc6fa02ac
@ -3,11 +3,13 @@ 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 version = require('../../package.json').version
|
||||||
|
const getLicenseInfo = require('../utils/get-plus')
|
||||||
const { plusServer1, plusServer2 } = require('../utils/plus-server')
|
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()
|
||||||
@ -175,6 +177,7 @@ const getPlusInfo = async ({ res }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getPlusDiscount = async ({ res } = {}) => {
|
const getPlusDiscount = async ({ res } = {}) => {
|
||||||
|
if (process.env.EXEC_ENV === 'local') return res.success({ discount: false })
|
||||||
const servers = [plusServer1, plusServer2]
|
const servers = [plusServer1, plusServer2]
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
try {
|
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 = {
|
module.exports = {
|
||||||
login,
|
login,
|
||||||
getpublicKey,
|
getpublicKey,
|
||||||
@ -205,5 +220,7 @@ module.exports = {
|
|||||||
enableMFA2,
|
enableMFA2,
|
||||||
disableMFA2,
|
disableMFA2,
|
||||||
getPlusInfo,
|
getPlusInfo,
|
||||||
getPlusDiscount
|
getPlusDiscount,
|
||||||
|
getPlusConf,
|
||||||
|
updatePlusKey
|
||||||
}
|
}
|
||||||
|
@ -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, 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 { 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')
|
||||||
@ -121,6 +121,16 @@ const user = [
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/plus-discount',
|
path: '/plus-discount',
|
||||||
controller: getPlusDiscount
|
controller: getPlusDiscount
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
path: '/plus-conf',
|
||||||
|
controller: getPlusConf
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
path: '/plus-conf',
|
||||||
|
controller: updatePlusKey
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
const notify = [
|
const notify = [
|
||||||
|
@ -3,10 +3,16 @@ 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 { plusServer1, plusServer2 } = require('./plus-server')
|
||||||
|
const { PlusDB } = require('./db-class')
|
||||||
|
const plusDB = new PlusDB().getInstance()
|
||||||
|
|
||||||
async function getLicenseInfo() {
|
async function getLicenseInfo(key = '') {
|
||||||
let key = process.env.PLUS_KEY
|
const { key: plusKey } = await plusDB.findOneAsync({}) || {}
|
||||||
if (!key || typeof key !== 'string' || key.length < 20) return
|
// 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 = ''
|
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
|
||||||
@ -20,7 +26,7 @@ async function getLicenseInfo() {
|
|||||||
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
|
return { success: false, msg: 'get public ip failed' }
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let response
|
let response
|
||||||
@ -64,9 +70,7 @@ async function getLicenseInfo() {
|
|||||||
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)
|
||||||
@ -74,14 +78,16 @@ async function getLicenseInfo() {
|
|||||||
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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,5 +126,11 @@ 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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
206
web/src/components/plus-table.vue
Normal file
206
web/src/components/plus-table.vue
Normal 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> -->
|
||||||
|
<strong>EasyNode</strong>最初是一个简单的Web终端工具,随着用户群的不断扩大,功能需求也日益增长,为了实现大家的功能需求,我投入了大量的业余时间进行开发和维护。
|
||||||
|
一直在为爱发电,渐渐的也没了开发的动力。
|
||||||
|
<br>
|
||||||
|
为了项目的可持续发展,<strong>后续</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 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>
|
@ -37,10 +37,7 @@
|
|||||||
</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>
|
|
||||||
<div class="plus_icon_wrapper">
|
|
||||||
<img
|
<img
|
||||||
class="plus_icon"
|
class="plus_icon"
|
||||||
src="@/assets/plus.png"
|
src="@/assets/plus.png"
|
||||||
@ -54,90 +51,12 @@
|
|||||||
alt="Discount"
|
alt="Discount"
|
||||||
>
|
>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="版本更新"
|
title="版本更新"
|
||||||
top="10vh"
|
top="20vh"
|
||||||
width="30%"
|
width="30%"
|
||||||
:append-to-body="false"
|
:append-to-body="false"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@ -163,20 +82,8 @@
|
|||||||
<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;">
|
|
||||||
<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 class="about_footer">
|
<div class="about_footer">
|
||||||
<el-button type="info" @click="visible = false">关闭</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>
|
||||||
@ -198,12 +105,14 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, getCurrentInstance, computed, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, getCurrentInstance, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { User, Sunny, Moon, Fold, CircleCheckFilled, Star, StarFilled, Promotion } 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'
|
||||||
|
import { handlePlusSupport } from '@/utils'
|
||||||
|
|
||||||
const { proxy: { $router, $store, $api, $message } } = getCurrentInstance()
|
const { proxy: { $router, $store, $api, $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 }`)
|
||||||
@ -212,20 +121,6 @@ const menuCollapse = ref(false)
|
|||||||
const discount = ref(false)
|
const discount = ref(false)
|
||||||
const discountContent = ref('')
|
const discountContent = ref('')
|
||||||
|
|
||||||
const plusFeatures = [
|
|
||||||
'跳板机功能,拯救被墙实例与龟速终端输入',
|
|
||||||
'本地socket断开自动重连,无需手动重新连接',
|
|
||||||
'批量修改实例配置(优化版)',
|
|
||||||
'脚本库批量导出导入',
|
|
||||||
'凭据管理支持解密带密码保护的密钥',
|
|
||||||
'提出的功能需求享有更高的开发优先级',
|
|
||||||
]
|
|
||||||
// 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)
|
||||||
const title = computed(() => $store.title)
|
const title = computed(() => $store.title)
|
||||||
@ -249,8 +144,8 @@ const handleLogout = () => {
|
|||||||
$router.push('/login')
|
$router.push('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePlusSupport = () => {
|
const gotoPlusPage = () => {
|
||||||
window.open('https://en.221022.xyz/buy-plus', '_blank')
|
router.push('/setting?tabKey=plus')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkLatestVersion() {
|
async function checkLatestVersion() {
|
||||||
|
@ -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,6 +111,9 @@ 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 })
|
||||||
},
|
},
|
||||||
|
@ -149,3 +149,7 @@ 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')
|
||||||
|
}
|
||||||
|
@ -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.trim.number="loginForm.mfa2Token"
|
v-model="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: '需输入密码', trigger: 'change' }
|
mfa2Token: { required: false, message: '需输入MFA2验证码', 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 })
|
let { data, msg } = await $api.login({ loginName, ciphertext, jwtExpires, mfa2Token: Number(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)
|
||||||
|
181
web/src/views/setting/components/user-plus.vue
Normal file
181
web/src/views/setting/components/user-plus.vue
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<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">
|
||||||
|
<el-input
|
||||||
|
v-model.trim="formData.key"
|
||||||
|
clearable
|
||||||
|
placeholder=""
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter.prevent="handleUpdate"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="loading" @click="handleUpdate">立即激活</el-button>
|
||||||
|
<el-button type="success" @click="handlePlusSupport">购买Plus</el-button>
|
||||||
|
</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 { 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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getPlusConf()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.plus-form {
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
|
|
@ -1,33 +1,61 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="setting_container">
|
<div class="setting_container">
|
||||||
<el-tabs tab-position="top">
|
<el-tabs v-model="tabKey" tab-position="top">
|
||||||
<el-tab-pane label="修改密码" lazy>
|
<el-tab-pane label="修改密码" name="user">
|
||||||
<User />
|
<User />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="登录日志">
|
<el-tab-pane label="登录日志" name="record" lazy>
|
||||||
<Record />
|
<Record />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="全局通知" lazy>
|
<el-tab-pane label="全局通知" name="notify">
|
||||||
<GlobalNotify />
|
<GlobalNotify />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="通知配置" lazy>
|
<el-tab-pane label="通知配置" name="notify-config">
|
||||||
<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 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'
|
||||||
|
|
||||||
|
// tabkey
|
||||||
|
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 } })
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleTabClick = (tab) => {
|
||||||
|
router.push({ query: { tabKey: tab.props.name } })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.setting_container {
|
.setting_container {
|
||||||
|
height: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user