Skip to content

Commit

Permalink
✨ 支持MFA2二次验证
Browse files Browse the repository at this point in the history
  • Loading branch information
chaos-zhu committed Oct 23, 2024
1 parent 70bdaa5 commit f0b492d
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 25 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## [2.3.0](https://github.com/chaos-zhu/easynode/releases) (2024-10-xx)

* 重构本地数据库存储方式(性能提升一个level~)
* 支持MFA2二次验证
* 优化了一些页面在移动端的展示
* 修复偶现刷新页面需重新登录的bug


## [2.2.8](https://github.com/chaos-zhu/easynode/releases) (2024-10-20)

### Features
Expand Down
54 changes: 52 additions & 2 deletions server/app/controller/user.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const jwt = require('jsonwebtoken')
const axios = require('axios')
const speakeasy = require('speakeasy')
const QRCode = require('qrcode')
const { sendNoticeAsync } = require('../utils/notify')
const { RSADecryptAsync, AESEncryptAsync, SHA1Encrypt } = require('../utils/encrypt')
const { getNetIPInfo } = require('../utils/tools')
Expand Down Expand Up @@ -112,17 +114,65 @@ const getEasynodeVersion = async ({ res }) => {
try {
// const { data } = await axios.get('https://api.github.com/repos/chaos-zhu/easynode/releases/latest')
const { data } = await axios.get('https://get-easynode-latest-version.chaoszhu.workers.dev/version')
console.log(data)
res.success({ data, msg: 'success' })
} catch (error) {
consola.error('Failed to fetch Easynode latest version:', error)
res.fail({ msg: 'Failed to fetch Easynode latest version' })
}
}

let tempSecret = null
const getMFA2Status = async ({ res }) => {
const { enableMFA2 = false } = await keyDB.findOneAsync({})
res.success({ data: enableMFA2, msg: 'success' })
}
const getMFA2Code = async ({ res }) => {
const { user } = await keyDB.findOneAsync({})
let { otpauth_url, base32 } = speakeasy.generateSecret({ name: `EasyNode-${ user }`, length: 20 })
tempSecret = base32
const qrImage = await QRCode.toDataURL(otpauth_url)
const data = { qrImage, secret: tempSecret }
res.success({ data, msg: 'success' })
}

const enableMFA2 = async ({ res, request }) => {
const { body: { token } } = request
if (!token) return res.fail({ data: false, msg: '参数错误' })
try {
// const isValid = authenticator.verify({ token, secret: tempSecret })
const isValid = speakeasy.totp.verify({
secret: tempSecret,
encoding: 'base32',
token,
window: 1
})
if (!isValid) return res.fail({ msg: '验证失败' })
const keyConfig = await keyDB.findOneAsync({})
keyConfig.enableMFA2 = true
keyConfig.secret = tempSecret
tempSecret = null
await keyDB.updateAsync({}, keyConfig)
res.success({ msg: '验证成功' })
} catch (error) {
res.fail({ msg: `验证失败: ${ error.message }` })
}
}

const disableMFA2 = async ({ res }) => {
const keyConfig = await keyDB.findOneAsync({})
keyConfig.enableMFA2 = false
keyConfig.secret = null
await keyDB.updateAsync({}, keyConfig)
res.success({ msg: 'success' })
}

module.exports = {
login,
getpublicKey,
updatePwd,
getEasynodeVersion
getEasynodeVersion,
getMFA2Status,
getMFA2Code,
enableMFA2,
disableMFA2
}
22 changes: 21 additions & 1 deletion server/app/router/routes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand } = require('../controller/ssh')
const { getHostList, addHost, updateHost, removeHost, importHost } = require('../controller/host')
const { login, getpublicKey, updatePwd, getEasynodeVersion } = require('../controller/user')
const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMFA2Code, enableMFA2, disableMFA2 } = 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 } = require('../controller/scripts')
Expand Down Expand Up @@ -81,6 +81,26 @@ const user = [
method: 'get',
path: '/version',
controller: getEasynodeVersion
},
{
method: 'get',
path: '/mfa2-status',
controller: getMFA2Status
},
{
method: 'post',
path: '/mfa2-code',
controller: getMFA2Code
},
{
method: 'post',
path: '/mfa2-enable',
controller: enableMFA2
},
{
method: 'post',
path: '/mfa2-disable',
controller: disableMFA2
}
]
const notify = [
Expand Down
2 changes: 2 additions & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@
"node-rsa": "^1.1.1",
"node-schedule": "^2.1.1",
"nodemailer": "^6.9.14",
"qrcode": "^1.5.4",
"socket.io": "^4.7.5",
"socket.io-client": "^4.7.5",
"speakeasy": "^2.0.0",
"ssh2": "^1.15.0",
"ssh2-sftp-client": "^10.0.3"
},
Expand Down
30 changes: 12 additions & 18 deletions web/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,24 +52,18 @@ export default {
updatePwd(data) {
return axios({ url: '/pwd', method: 'put', data })
},
// updateHostSort(data) {
// return axios({ url: '/host-sort', method: 'put', data })
// },
// getUserEmailList() {
// return axios({ url: '/user-email', method: 'get' })
// },
// getSupportEmailList() {
// return axios({ url: '/support-email', method: 'get' })
// },
// updateUserEmailList(data) {
// return axios({ url: '/user-email', method: 'post', data })
// },
// deleteUserEmail(email) {
// return axios({ url: `/user-email/${ email }`, method: 'delete' })
// },
// pushTestEmail(data) {
// return axios({ url: '/push-email', method: 'post', data })
// },
getMFA2QR() {
return axios({ url: '/mfa2-code', method: 'post' })
},
getMFA2Status() {
return axios({ url: '/mfa2-status', method: 'get' })
},
enableMFA2(data) {
return axios({ url: '/mfa2-enable', method: 'post', data })
},
disableMFA2() {
return axios({ url: '/mfa2-disable', method: 'post' })
},
getNotifyConfig() {
return axios({ url: '/notify-config', method: 'get' })
},
Expand Down
101 changes: 98 additions & 3 deletions web/src/views/setting/components/user.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
:rules="rules"
:hide-required-asterisk="true"
label-suffix=""
label-width="90px"
label-width="86px"
:show-message="false"
>
<el-form-item label="原用户名" prop="oldLoginName">
Expand Down Expand Up @@ -50,13 +50,37 @@
<el-button type="primary" :loading="loading" @click="handleUpdate">确认</el-button>
</el-form-item>
</el-form>
<h2 class="mfa2_title">两步验证(MFA2)</h2>
<div v-if="isEnableMFA2">
<span class="enable_text">已启用</span>
<el-button class="disable_btn" type="danger" @click="handleDisableMFA2">禁用</el-button>
</div>
<template v-else>
<el-button v-if="startEnableMFA2" type="primary" @click="handleMFA2">启用</el-button>
<template v-else>
<div class="mfa2_container">
<!-- https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2 -->
<p>1. 使用MFA2应用(<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank" class="link">Google Authenticator</a> )扫描下面二维码,或者输入秘钥 <span class="secret">{{ MFA2Data.secret }}</span></p>
<img :src="MFA2Data.qrImage" :alt="MFA2Data.secret">
<p>2. 输入MFA2应用上的6位数字</p>
<el-input
v-model="mfa2Token"
class="mfa2_input"
clearable
placeholder=""
@keyup.enter="handleEnableMFA2"
/>
<el-button type="primary" @click="handleEnableMFA2">保存</el-button>
</div>
</template>
</template>
</template>

<script setup>
import { ref, reactive, getCurrentInstance } from 'vue'
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
import { RSAEncrypt } from '@utils/index.js'
const { proxy: { $api, $message, $store } } = getCurrentInstance()
const { proxy: { $api, $message, $messageBox, $store } } = getCurrentInstance()
const loading = ref(false)
const formRef = ref(null)
Expand All @@ -73,6 +97,14 @@ const rules = reactive({
newPwd: { required: true, message: '输入新密码', trigger: 'change' }
})
const startEnableMFA2 = ref(true)
const isEnableMFA2 = ref(false)
const MFA2Data = ref({
qrImage: '',
secret: ''
})
const mfa2Token = ref('')
const handleUpdate = () => {
formRef.value.validate()
.then(async () => {
Expand All @@ -89,10 +121,73 @@ const handleUpdate = () => {
formRef.value.resetFields()
})
}
const getMFA2Status = async () => {
let { data } = await $api.getMFA2Status()
isEnableMFA2.value = data
}
const handleMFA2 = async () => {
startEnableMFA2.value = false
let { data } = await $api.getMFA2QR()
MFA2Data.value = data
console.log(data)
}
const handleEnableMFA2 = async () => {
if (!mfa2Token.value) return $message({ type: 'error', center: true, message: '请输入MFA2应用上的6位数字' })
let { msg } = await $api.enableMFA2({ token: mfa2Token.value })
$message({ type: 'success', center: true, message: msg })
getMFA2Status()
}
const handleDisableMFA2 = async () => {
$messageBox.confirm('确认禁用MFA2', 'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
let { msg } = await $api.disableMFA2()
$message({ type: 'success', center: true, message: msg })
getMFA2Status()
})
}
onMounted(() => {
getMFA2Status()
})
</script>

<style lang="scss" scoped>
.password-form {
width: 500px;
}
.mfa2_title {
font-size: 18px;
margin-bottom: 20px;
}
.mfa2_container {
align-items: flex-start;
color: var(--el-text-color-regular);
font-size: var(--el-form-label-font-size);
line-height: 32px;
.secret {
color: var(--el-color-primary);
text-decoration: underline;
}
img {
width: 150px;
height: 150px;
border-radius: 5px;
}
.mfa2_input {
width: 150px;
margin-right: 15px;
}
}
.enable_text {
color: var(--el-color-primary);
}
.disable_btn {
margin: 0 15px;
}
</style>
Loading

0 comments on commit f0b492d

Please sign in to comment.