mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-03-14 01:34:47 +08:00
feat: 两步验证增加手动输入选项 (#822)
This commit is contained in:
parent
292dca6419
commit
2c8b19bff2
@ -891,6 +891,14 @@ const message = {
|
|||||||
mfaHelper1: 'Download a MFA verification mobile app e.g.:',
|
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',
|
mfaHelper2: 'Scan the following QR code using the mobile app to obtain the 6-digit verification code',
|
||||||
mfaHelper3: 'Enter six digits from the app',
|
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.',
|
https: 'Setting up HTTPS protocol access for the panel can enhance the security of panel access.',
|
||||||
selfSigned: 'Self signed',
|
selfSigned: 'Self signed',
|
||||||
|
@ -873,17 +873,6 @@ const message = {
|
|||||||
password: '密码',
|
password: '密码',
|
||||||
path: '路径',
|
path: '路径',
|
||||||
|
|
||||||
https: '为面板设置 https 协议访问,提升面板访问安全性',
|
|
||||||
selfSigned: '自签名',
|
|
||||||
selfSignedHelper: '自签证书,不被浏览器信任,显示不安全是正常现象',
|
|
||||||
import: '导入',
|
|
||||||
select: '选择已有',
|
|
||||||
domainOrIP: '域名/IP:',
|
|
||||||
timeOut: '过期时间:',
|
|
||||||
rootCrtDownload: '根证书下载',
|
|
||||||
primaryKey: '密钥',
|
|
||||||
certificate: '证书',
|
|
||||||
|
|
||||||
snapshot: '快照',
|
snapshot: '快照',
|
||||||
thirdPartySupport: '仅支持第三方账号',
|
thirdPartySupport: '仅支持第三方账号',
|
||||||
recoverDetail: '恢复详情',
|
recoverDetail: '恢复详情',
|
||||||
@ -939,9 +928,25 @@ const message = {
|
|||||||
mfaHelper1: '下载两步验证手机应用 如:',
|
mfaHelper1: '下载两步验证手机应用 如:',
|
||||||
mfaHelper2: '使用手机应用扫描以下二维码,获取 6 位验证码',
|
mfaHelper2: '使用手机应用扫描以下二维码,获取 6 位验证码',
|
||||||
mfaHelper3: '输入手机应用上的 6 位数字',
|
mfaHelper3: '输入手机应用上的 6 位数字',
|
||||||
|
mfaSecret: '验证密钥',
|
||||||
|
mfaTypeOption: '选择获取密钥方式',
|
||||||
|
qrCode: '二维码',
|
||||||
|
manualInput: '手动输入',
|
||||||
|
mfaCode: '验证码',
|
||||||
sslDisable: '禁用',
|
sslDisable: '禁用',
|
||||||
sslDisableHelper: '禁用 https 服务,需要重启面板才能生效,是否继续?',
|
sslDisableHelper: '禁用 https 服务,需要重启面板才能生效,是否继续?',
|
||||||
|
|
||||||
|
https: '为面板设置 https 协议访问,提升面板访问安全性',
|
||||||
|
selfSigned: '自签名',
|
||||||
|
selfSignedHelper: '自签证书,不被浏览器信任,显示不安全是正常现象',
|
||||||
|
import: '导入',
|
||||||
|
select: '选择已有',
|
||||||
|
domainOrIP: '域名/IP:',
|
||||||
|
timeOut: '过期时间:',
|
||||||
|
rootCrtDownload: '根证书下载',
|
||||||
|
primaryKey: '密钥',
|
||||||
|
certificate: '证书',
|
||||||
|
|
||||||
monitor: '监控',
|
monitor: '监控',
|
||||||
enableMonitor: '监控状态',
|
enableMonitor: '监控状态',
|
||||||
storeDays: '保存天数',
|
storeDays: '保存天数',
|
||||||
|
@ -73,7 +73,8 @@
|
|||||||
{{ $t('setting.complexityHelper') }}
|
{{ $t('setting.complexityHelper') }}
|
||||||
</span>
|
</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('setting.mfa')" prop="securityEntrance">
|
|
||||||
|
<el-form-item :label="$t('setting.mfa')">
|
||||||
<el-switch
|
<el-switch
|
||||||
@change="handleMFA"
|
@change="handleMFA"
|
||||||
v-model="form.mfaStatus"
|
v-model="form.mfaStatus"
|
||||||
@ -84,37 +85,6 @@
|
|||||||
{{ $t('setting.mfaHelper') }}
|
{{ $t('setting.mfaHelper') }}
|
||||||
</span>
|
</span>
|
||||||
</el-form-item>
|
</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-form-item label="https" prop="ssl">
|
||||||
<el-switch
|
<el-switch
|
||||||
@ -137,6 +107,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
|
|
||||||
|
<MfaSetting ref="mfaRef" @search="search" />
|
||||||
<EntranceSetting ref="entranceRef" @search="search" />
|
<EntranceSetting ref="entranceRef" @search="search" />
|
||||||
<TimeoutSetting ref="timeoutref" @search="search" />
|
<TimeoutSetting ref="timeoutref" @search="search" />
|
||||||
</div>
|
</div>
|
||||||
@ -145,27 +116,20 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, reactive, onMounted } from 'vue';
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
import { ElForm, ElMessageBox } from 'element-plus';
|
import { ElForm, ElMessageBox } from 'element-plus';
|
||||||
import { Setting } from '@/api/interface/setting';
|
|
||||||
import LayoutContent from '@/layout/layout-content.vue';
|
import LayoutContent from '@/layout/layout-content.vue';
|
||||||
import SSLSetting from '@/views/setting/safe/ssl/index.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 TimeoutSetting from '@/views/setting/safe/timeout/index.vue';
|
||||||
import EntranceSetting from '@/views/setting/safe/entrance/index.vue';
|
import EntranceSetting from '@/views/setting/safe/entrance/index.vue';
|
||||||
import {
|
import { updateSetting, getSettingInfo, updatePort, getSystemAvailable, updateSSL } from '@/api/modules/setting';
|
||||||
updateSetting,
|
|
||||||
getMFA,
|
|
||||||
bindMFA,
|
|
||||||
getSettingInfo,
|
|
||||||
updatePort,
|
|
||||||
getSystemAvailable,
|
|
||||||
updateSSL,
|
|
||||||
} from '@/api/modules/setting';
|
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { Rules } from '@/global/form-rules';
|
import { Rules } from '@/global/form-rules';
|
||||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
import { MsgSuccess } from '@/utils/message';
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const entranceRef = ref();
|
const entranceRef = ref();
|
||||||
const timeoutref = ref();
|
const timeoutref = ref();
|
||||||
|
const mfaRef = ref();
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
serverPort: 9999,
|
serverPort: 9999,
|
||||||
@ -176,7 +140,6 @@ const form = reactive({
|
|||||||
expirationTime: '',
|
expirationTime: '',
|
||||||
complexityVerification: '',
|
complexityVerification: '',
|
||||||
mfaStatus: 'disable',
|
mfaStatus: 'disable',
|
||||||
mfaSecret: 'disable',
|
|
||||||
});
|
});
|
||||||
type FormInstance = InstanceType<typeof ElForm>;
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
|
|
||||||
@ -199,15 +162,7 @@ const search = async () => {
|
|||||||
form.expirationTime = res.data.expirationTime;
|
form.expirationTime = res.data.expirationTime;
|
||||||
form.complexityVerification = res.data.complexityVerification;
|
form.complexityVerification = res.data.complexityVerification;
|
||||||
form.mfaStatus = res.data.mfaStatus;
|
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 panelFormRef = ref<FormInstance>();
|
||||||
|
|
||||||
const onSave = async (formEl: FormInstance | undefined, key: string, val: any) => {
|
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 () => {
|
const handleMFA = async () => {
|
||||||
if (form.mfaStatus === 'enable') {
|
if (form.mfaStatus === 'enable') {
|
||||||
const res = await getMFA();
|
mfaRef.value.acceptParams();
|
||||||
otp.secret = res.data.secret;
|
|
||||||
otp.qrImage = res.data.qrImage;
|
|
||||||
isMFAShow.value = true;
|
|
||||||
} else {
|
} else {
|
||||||
isMFAShow.value = false;
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await updateSetting({ key: 'MFAStatus', value: 'disable' })
|
await updateSetting({ key: 'MFAStatus', value: 'disable' })
|
||||||
.then(() => {
|
.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 () => {
|
const onChangeExpirationTime = async () => {
|
||||||
timeoutref.value.acceptParams({ expirationDays: form.expirationDays });
|
timeoutref.value.acceptParams({ expirationDays: form.expirationDays });
|
||||||
};
|
};
|
||||||
|
117
frontend/src/views/setting/safe/mfa/index.vue
Normal file
117
frontend/src/views/setting/safe/mfa/index.vue
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user