feat: 两步验证增加手动输入选项 (#822)

This commit is contained in:
ssongliu 2023-04-28 11:42:16 +08:00 committed by GitHub
parent 292dca6419
commit 2c8b19bff2
4 changed files with 149 additions and 91 deletions

View file

@ -891,6 +891,14 @@ const message = {
mfaHelper1: 'Download a MFA verification mobile app e.g.:',
mfaHelper2: 'Scan the following QR code using the mobile app to obtain the 6-digit verification code',
mfaHelper3: 'Enter six digits from the app',
mfaSecret: 'Secret',
mfaTypeOption: 'Select the method of obtaining the secret',
qrCode: 'QR code',
manualInput: 'Manual input',
mfaCode: 'Code',
sslDisable: 'Disable',
sslDisableHelper:
'If the https service is disabled, you need to restart the panel for it to take effect. Do you want to continue?',
https: 'Setting up HTTPS protocol access for the panel can enhance the security of panel access.',
selfSigned: 'Self signed',

View file

@ -873,17 +873,6 @@ const message = {
password: '密码',
path: '路径',
https: '为面板设置 https 协议访问提升面板访问安全性',
selfSigned: '自签名',
selfSignedHelper: '自签证书不被浏览器信任显示不安全是正常现象',
import: '导入',
select: '选择已有',
domainOrIP: '域名/IP',
timeOut: '过期时间',
rootCrtDownload: '根证书下载',
primaryKey: '密钥',
certificate: '证书',
snapshot: '快照',
thirdPartySupport: '仅支持第三方账号',
recoverDetail: '恢复详情',
@ -939,9 +928,25 @@ const message = {
mfaHelper1: '下载两步验证手机应用 :',
mfaHelper2: '使用手机应用扫描以下二维码获取 6 位验证码',
mfaHelper3: '输入手机应用上的 6 位数字',
mfaSecret: '验证密钥',
mfaTypeOption: '选择获取密钥方式',
qrCode: '二维码',
manualInput: '手动输入',
mfaCode: '验证码',
sslDisable: '禁用',
sslDisableHelper: '禁用 https 服务需要重启面板才能生效是否继续',
https: '为面板设置 https 协议访问提升面板访问安全性',
selfSigned: '自签名',
selfSignedHelper: '自签证书不被浏览器信任显示不安全是正常现象',
import: '导入',
select: '选择已有',
domainOrIP: '域名/IP',
timeOut: '过期时间',
rootCrtDownload: '根证书下载',
primaryKey: '密钥',
certificate: '证书',
monitor: '监控',
enableMonitor: '监控状态',
storeDays: '保存天数',

View file

@ -73,7 +73,8 @@
{{ $t('setting.complexityHelper') }}
</span>
</el-form-item>
<el-form-item :label="$t('setting.mfa')" prop="securityEntrance">
<el-form-item :label="$t('setting.mfa')">
<el-switch
@change="handleMFA"
v-model="form.mfaStatus"
@ -84,37 +85,6 @@
{{ $t('setting.mfaHelper') }}
</span>
</el-form-item>
<el-form-item v-if="isMFAShow">
<el-card style="width: 100%">
<ul style="line-height: 24px">
<li>
{{ $t('setting.mfaHelper1') }}
<ul>
<li>Google Authenticator</li>
<li>Microsoft Authenticator</li>
<li>1Password</li>
<li>LastPass</li>
<li>Authenticator</li>
</ul>
</li>
<li>{{ $t('setting.mfaHelper2') }}</li>
<el-image
style="margin-left: 15px; width: 100px; height: 100px"
:src="otp.qrImage"
/>
<li>{{ $t('setting.mfaHelper3') }}</li>
<el-input v-model="mfaCode"></el-input>
<div style="margin-top: 10px; margin-bottom: 10px; float: right">
<el-button @click="onCancelMfaBind">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button type="primary" @click="onBind">
{{ $t('commons.button.saveAndEnable') }}
</el-button>
</div>
</ul>
</el-card>
</el-form-item>
<el-form-item label="https" prop="ssl">
<el-switch
@ -137,6 +107,7 @@
</template>
</LayoutContent>
<MfaSetting ref="mfaRef" @search="search" />
<EntranceSetting ref="entranceRef" @search="search" />
<TimeoutSetting ref="timeoutref" @search="search" />
</div>
@ -145,27 +116,20 @@
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue';
import { ElForm, ElMessageBox } from 'element-plus';
import { Setting } from '@/api/interface/setting';
import LayoutContent from '@/layout/layout-content.vue';
import SSLSetting from '@/views/setting/safe/ssl/index.vue';
import MfaSetting from '@/views/setting/safe/mfa/index.vue';
import TimeoutSetting from '@/views/setting/safe/timeout/index.vue';
import EntranceSetting from '@/views/setting/safe/entrance/index.vue';
import {
updateSetting,
getMFA,
bindMFA,
getSettingInfo,
updatePort,
getSystemAvailable,
updateSSL,
} from '@/api/modules/setting';
import { updateSetting, getSettingInfo, updatePort, getSystemAvailable, updateSSL } from '@/api/modules/setting';
import i18n from '@/lang';
import { Rules } from '@/global/form-rules';
import { MsgError, MsgSuccess } from '@/utils/message';
import { MsgSuccess } from '@/utils/message';
const loading = ref(false);
const entranceRef = ref();
const timeoutref = ref();
const mfaRef = ref();
const form = reactive({
serverPort: 9999,
@ -176,7 +140,6 @@ const form = reactive({
expirationTime: '',
complexityVerification: '',
mfaStatus: 'disable',
mfaSecret: 'disable',
});
type FormInstance = InstanceType<typeof ElForm>;
@ -199,15 +162,7 @@ const search = async () => {
form.expirationTime = res.data.expirationTime;
form.complexityVerification = res.data.complexityVerification;
form.mfaStatus = res.data.mfaStatus;
form.mfaSecret = res.data.mfaSecret;
};
const isMFAShow = ref<boolean>(false);
const otp = reactive<Setting.MFAInfo>({
secret: '',
qrImage: '',
});
const mfaCode = ref();
const panelFormRef = ref<FormInstance>();
const onSave = async (formEl: FormInstance | undefined, key: string, val: any) => {
@ -271,12 +226,8 @@ const onSavePort = async (formEl: FormInstance | undefined, key: string, val: an
};
const handleMFA = async () => {
if (form.mfaStatus === 'enable') {
const res = await getMFA();
otp.secret = res.data.secret;
otp.qrImage = res.data.qrImage;
isMFAShow.value = true;
mfaRef.value.acceptParams();
} else {
isMFAShow.value = false;
loading.value = true;
await updateSetting({ key: 'MFAStatus', value: 'disable' })
.then(() => {
@ -321,29 +272,6 @@ const handleSSL = async () => {
});
};
const onBind = async () => {
if (!mfaCode.value) {
MsgError(i18n.global.t('commons.msg.comfimNoNull', ['code']));
return;
}
loading.value = true;
await bindMFA({ code: mfaCode.value, secret: otp.secret })
.then(() => {
loading.value = false;
search();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
isMFAShow.value = false;
})
.catch(() => {
loading.value = false;
});
};
const onCancelMfaBind = async () => {
form.mfaStatus = 'disable';
isMFAShow.value = false;
};
const onChangeExpirationTime = async () => {
timeoutref.value.acceptParams({ expirationDays: form.expirationDays });
};

View file

@ -0,0 +1,117 @@
<template>
<div>
<el-drawer
v-model="drawerVisiable"
:destroy-on-close="true"
:close-on-click-modal="false"
@close="handleClose"
size="30%"
>
<template #header>
<DrawerHeader :header="$t('setting.mfa')" :back="handleClose" />
</template>
<el-form :model="form" ref="formRef" v-loading="loading" label-position="top">
<el-form-item :label="$t('setting.mfaHelper1')">
<ul>
<li>Google Authenticator</li>
<li>Microsoft Authenticator</li>
<li>1Password</li>
<li>LastPass</li>
<li>Authenticator</li>
</ul>
</el-form-item>
<el-form-item :label="$t('setting.mfaTypeOption')">
<el-radio-group v-model="mode" @change="form.secret = ''">
<el-radio label="scan">{{ $t('setting.qrCode') }}</el-radio>
<el-radio label="input">{{ $t('setting.manualInput') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('setting.mfaHelper2')" v-if="mode === 'scan'">
<el-image style="width: 120px; height: 120px" :src="qrImage" />
</el-form-item>
<el-form-item
:label="$t('setting.mfaSecret')"
v-if="mode === 'input'"
prop="secret"
:rules="Rules.requiredInput"
>
<el-input v-model="form.secret"></el-input>
</el-form-item>
<el-form-item
:label="mode === 'scan' ? $t('setting.mfaHelper3') : $t('setting.mfaCode')"
prop="code"
:rules="Rules.requiredInput"
>
<el-input v-model="form.code"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onBind(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { bindMFA, getMFA } from '@/api/modules/setting';
import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
const loading = ref();
const qrImage = ref();
const mode = ref('scan');
const drawerVisiable = ref();
const formRef = ref();
const form = reactive({
code: '',
secret: '',
});
const emit = defineEmits<{ (e: 'search'): void }>();
const acceptParams = (): void => {
loadMfaCode();
drawerVisiable.value = true;
};
const loadMfaCode = async () => {
const res = await getMFA();
form.secret = res.data.secret;
qrImage.value = res.data.qrImage;
};
const onBind = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
await bindMFA(form)
.then(() => {
loading.value = false;
drawerVisiable.value = false;
emit('search');
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
};
const handleClose = () => {
emit('search');
drawerVisiable.value = false;
};
defineExpose({
acceptParams,
});
</script>