Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions frontend/src/lang/modules/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
27 changes: 16 additions & 11 deletions frontend/src/lang/modules/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -873,17 +873,6 @@ const message = {
password: '密码',
path: '路径',

https: '为面板设置 https 协议访问,提升面板访问安全性',
selfSigned: '自签名',
selfSignedHelper: '自签证书,不被浏览器信任,显示不安全是正常现象',
import: '导入',
select: '选择已有',
domainOrIP: '域名/IP:',
timeOut: '过期时间:',
rootCrtDownload: '根证书下载',
primaryKey: '密钥',
certificate: '证书',

snapshot: '快照',
thirdPartySupport: '仅支持第三方账号',
recoverDetail: '恢复详情',
Expand Down Expand Up @@ -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: '保存天数',
Expand Down
88 changes: 8 additions & 80 deletions frontend/src/views/setting/safe/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -137,6 +107,7 @@
</template>
</LayoutContent>

<MfaSetting ref="mfaRef" @search="search" />
<EntranceSetting ref="entranceRef" @search="search" />
<TimeoutSetting ref="timeoutref" @search="search" />
</div>
Expand All @@ -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,
Expand All @@ -176,7 +140,6 @@ const form = reactive({
expirationTime: '',
complexityVerification: '',
mfaStatus: 'disable',
mfaSecret: 'disable',
});
type FormInstance = InstanceType<typeof ElForm>;

Expand All @@ -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) => {
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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 });
};
Expand Down
117 changes: 117 additions & 0 deletions frontend/src/views/setting/safe/mfa/index.vue
Original file line number Diff line number Diff line change
@@ -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>