<template> <Dialog :close-on-click-modal="false" :destroy-on-close="true" :show-cancel="false" :show-confirm="false" :title="title" :visible.sync="visible" class="dialog-content" v-bind="$attrs" width="740px" @confirm="visible = false" v-on="$listeners" > <div v-if="confirmTypeRequired === 'relogin'"> <el-row :gutter="24" style="margin: 0 auto;"> <el-col :md="24" :sm="24"> <el-alert :title="$tc('ReLoginTitle')" center style="margin-bottom: 20px;" type="error" /> </el-col> </el-row> <el-row :gutter="24" style="margin: 0 auto;"> <el-col :md="24" :sm="24"> <el-button class="confirm-btn" size="mini" type="primary" @click="logout"> {{ this.$t('ReLogin') }} </el-button> </el-col> </el-row> </div> <div v-else> <el-row :gutter="24" style="margin: 0 auto;"> <el-col :md="24" :sm="24" :span="24" class="add"> <el-select v-model="subTypeSelected" style="width: 100%; margin-bottom: 20px;" @change="handleSubTypeChange" > <el-option v-for="item of subTypeChoices" :key="item.name" :disabled="item.disabled" :label="item.display_name" :value="item.name" /> </el-select> </el-col> </el-row> <el-row :gutter="24" style="margin: 0 auto;"> <el-col :md="24" :sm="24" style="display: flex; align-items: center; margin-bottom: 20px;"> <el-input v-if="subTypeSelected !== 'face'" v-model="secretValue" :placeholder="inputPlaceholder" :show-password="showPassword" @keyup.enter.native="handleConfirm" /> <iframe v-if="isFaceCaptureVisible && subTypeSelected ==='face' && faceCaptureUrl" :src="faceCaptureUrl" allow="camera" sandbox="allow-scripts allow-same-origin" style="width: 100%; height: 800px;border: none;" /> <span v-if="subTypeSelected === 'sms' || subTypeSelected === 'email'" style="margin: -1px 0 0 20px;"> <el-button :disabled="smsBtnDisabled" size="mini" style="line-height: 14px; float: right;" type="primary" @click="sendCode" > {{ smsBtnText }} </el-button> </span> </el-col> </el-row> <el-row :gutter="24" style="margin: 10px auto;"> <el-col :md="24" :sm="24"> <el-button v-if="subTypeSelected!=='face'" class="confirm-btn" size="mini" type="primary" @click="handleConfirm" > {{ this.$t('Confirm') }} </el-button> <el-button v-if="subTypeSelected==='face'&&!isFaceCaptureVisible" class="confirm-btn" size="mini" type="primary" @click="handleFaceCapture" > {{ this.$tc('VerifyFace') }} </el-button> </el-col> </el-row> </div> </Dialog> </template> <script> import Dialog from '@/components/Dialog/index.vue' import { encryptPassword } from '@/utils/crypto' export default { name: 'UserConfirmDialog', components: { Dialog }, props: { url: { type: String, default: '' }, handler: { type: Function, default: null } }, data() { return { title: this.$t('CurrentUserVerify'), smsWidth: 0, subTypeSelected: '', inputPlaceholder: '', smsBtnText: this.$t('SendVerificationCode'), smsBtnDisabled: false, confirmTypeRequired: '', subTypeChoices: [], secretValue: '', visible: false, callback: null, cancel: null, processing: false, isFaceCaptureVisible: false, faceToken: null, faceCaptureUrl: null } }, computed: { showPassword() { return this.confirmTypeRequired === 'password' } }, mounted() { this.$eventBus.$on('showConfirmDialog', this.performConfirm) }, beforeDestroy() { this.$eventBus.$off('showConfirmDialog', this.performConfirm) }, methods: { handleSubTypeChange(val) { if (val !== 'face') { this.isFaceCaptureVisible = false } this.inputPlaceholder = this.subTypeChoices.filter(item => item.name === val)[0]?.placeholder this.smsWidth = val === 'sms' ? 6 : 0 }, performConfirm: _.debounce(function({ response, callback, cancel }) { if (this.processing || this.visible) { return } this.processing = true this.callback = callback this.cancel = cancel this.$log.debug('perform confirm action') const confirmType = response.data?.code const confirmUrl = '/api/v1/authentication/confirm/' this.$axios.get(confirmUrl, { params: { confirm_type: confirmType }}).then((data) => { this.confirmTypeRequired = data.confirm_type if (this.confirmTypeRequired === 'relogin') { this.$axios.post(confirmUrl, { 'confirm_type': 'relogin', 'secret_key': 'x' }).then(() => { this.callback() this.visible = false }).catch(() => { this.title = this.$t('NeedReLogin') this.visible = true }) return } this.subTypeChoices = data.content const defaultSubType = this.subTypeChoices.filter(item => !item.disabled)[0] this.subTypeSelected = defaultSubType.name this.inputPlaceholder = defaultSubType.placeholder this.visible = true }).catch((err) => { const data = err.response?.data const msg = data?.error || data?.detail || data?.msg || this.$t('GetConfirmTypeFailed') this.$message.error(msg) this.cancel(err) }).finally(() => { this.processing = false }) }, 500), logout() { window.location.href = `${process.env.VUE_APP_LOGOUT_PATH}?next=${this.$route.fullPath}` }, sendCode() { this.$axios.post(`/api/v1/authentication/mfa/select/`, { type: this.subTypeSelected }).then(res => { this.$message.success(this.$tc('VerificationCodeSent')) let time = 60 this.smsBtnDisabled = true const interval = setInterval(() => { time -= 1 this.smsBtnText = `${this.$t('Pending')}: ${time}` if (time <= 0) { clearInterval(interval) this.smsBtnText = this.$t('SendVerificationCode') this.smsBtnDisabled = false } }, 1000) }).catch(() => { this.$message.error(this.$tc('FailedToSendVerificationCode')) }) }, startFaceCapture() { const url = '/api/v1/authentication/face/context/' this.$axios.post(url).then(data => { const token = data['token'] this.faceCaptureUrl = '/facelive/capture?token=' + token this.isFaceCaptureVisible = true const timer = setInterval(() => { this.$axios.get(url + `?token=${token}`).then(data => { if (data['is_finished']) { clearInterval(timer) this.isFaceCaptureVisible = false this.handleConfirm() } }) }, 1000) }).catch(() => { this.$message.error(this.$tc('FailedToStartFaceCapture')) }) }, handleFaceCapture() { this.startFaceCapture() }, handleConfirm() { if (this.confirmTypeRequired === 'relogin') { return this.logout() } if (this.subTypeSelected === 'otp' && this.secretValue.length !== 6) { return this.$message.error(this.$tc('MFAErrorMsg')) } const data = { confirm_type: this.confirmTypeRequired, mfa_type: this.confirmTypeRequired === 'mfa' ? this.subTypeSelected : '', secret_key: this.confirmTypeRequired === 'password' ? encryptPassword(this.secretValue) : this.secretValue } this.$axios.post(`/api/v1/authentication/confirm/`, data).then(() => { this.secretValue = '' this.visible = false this.$nextTick(() => { this.callback() }) }).catch((err) => { this.$message.error(err.message || this.$tc('ConfirmFailed')) this.faceCaptureUrl = null this.isFaceCaptureVisible = false }) } } } </script> <style lang="scss" scoped> .dialog-content ::v-deep .el-dialog__footer { padding: 0; } .dialog-content ::v-deep .el-dialog { padding: 8px; .el-dialog__body { padding-top: 30px; padding-bottom: 30px; } } .confirm-btn { width: 100%; line-height: 20px; } </style>