mirror of https://github.com/1Panel-dev/1Panel
feat: 两步验证增加手动输入选项 (#822)
parent
292dca6419
commit
2c8b19bff2
|
@ -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',
|
||||
|
|
|
@ -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: '保存天数',
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue