mirror of
https://github.com/jumpserver/lina.git
synced 2025-04-27 19:15:13 +00:00
299 lines
8.7 KiB
Vue
299 lines
8.7 KiB
Vue
<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>
|