Merge pull request #3433 from jumpserver/pr@dev@refactor_confirm

perf: 优化确认操作
This commit is contained in:
老广
2023-10-13 17:27:27 +08:00
committed by GitHub
14 changed files with 137 additions and 151 deletions

View File

@@ -1,6 +1,5 @@
<template>
<div>
<UserConfirmDialog :handler="getAuthInfo" @onConfirmDone="showSecretDialog" />
<Dialog
:destroy-on-close="true"
:show-cancel="false"
@@ -61,7 +60,6 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import PasswordHistoryDialog from './PasswordHistoryDialog.vue'
import UserConfirmDialog from '@/components/Apps/UserConfirmDialog/index.vue'
import { ShowKeyCopyFormatter } from '@/components/Table/TableFormatters'
import { encryptPassword } from '@/utils/crypto'
@@ -70,7 +68,6 @@ export default {
components: {
Dialog,
PasswordHistoryDialog,
UserConfirmDialog,
ShowKeyCopyFormatter
},
props: {
@@ -124,6 +121,7 @@ export default {
this.versions = resp.count
})
}
this.showSecretDialog()
},
methods: {
accountConfirmHandle() {
@@ -140,13 +138,12 @@ export default {
this.$message.success(this.$tc('common.updateSuccessMsg'))
})
},
getAuthInfo() {
return this.$axios.get(this.url, { disableFlashErrorMsg: true })
},
showSecretDialog(res) {
this.secretInfo = res
this.sshKeyFingerprint = res?.spec_info?.ssh_key_fingerprint || '-'
this.showSecret = true
return this.$axios.get(this.url, { disableFlashErrorMsg: true }).then(() => {
this.secretInfo = res
this.sshKeyFingerprint = res?.spec_info?.ssh_key_fingerprint || '-'
this.showSecret = true
})
},
exit() {
this.$emit('update:visible', false)

View File

@@ -1,5 +1,7 @@
<template>
<Dialog
:close-on-click-modal="false"
:destory-on-close="true"
:show-cancel="false"
:show-confirm="false"
:title="title"
@@ -17,7 +19,7 @@
:title="$tc('auth.ReLoginTitle')"
center
style="margin-bottom: 20px;"
type="info"
type="error"
/>
</el-col>
</el-row>
@@ -94,72 +96,68 @@ export default {
data() {
return {
title: this.$t('common.CurrentUserVerify'),
smsBtnText: this.$t('common.SendVerificationCode'),
smsWidth: 0,
subTypeSelected: '',
inputPlaceholder: '',
smsBtnText: this.$t('common.SendVerificationCode'),
smsBtnDisabled: false,
confirmTypeRequired: '',
subTypeChoices: [],
secretValue: '',
visible: false
visible: false,
callback: null,
cancel: null,
processing: false
}
},
computed: {
showPassword() {
return this.confirmTypeRequired === 'password'
},
iHandler() {
if (this.handler === null) {
return () => this.$axios.get(this.url, { disableFlashErrorMsg: true })
}
return this.handler
}
},
watch: {
visible(val) {
if (!val) {
this.$emit('onConfirmFinal', true)
}
}
},
mounted() {
this.performConfirm()
// const onRecvCallback = _.debounce(this.performConfirm, 500)
this.$eventBus.$on('showConfirmDialog', this.performConfirm)
},
methods: {
handleSubTypeChange(val) {
this.inputPlaceholder = this.subTypeChoices.filter(item => item.name === val)[0]?.placeholder
this.smsWidth = val === 'sms' ? 6 : 0
},
performConfirm() {
this.iHandler().then((res) => {
this.$emit('onConfirmDone', res)
}).catch((err) => {
const confirmType = err.response.data?.code
const confirmUrl = '/api/v1/authentication/confirm/'
this.$axios.get(confirmUrl, { params: { confirm_type: confirmType }}).then((data) => {
this.subTypeChoices = data.content
this.confirmTypeRequired = data.confirm_type
performConfirm({ 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') {
const data = {
confirm_type: this.confirmTypeRequired,
secret_key: ''
}
this.$axios.post(confirmUrl, data, { disableFlashErrorMsg: true }).then(() => {
this.afterConfirm()
}).catch(() => {
this.title = this.$t('auth.NeedReLogin')
})
return
}
const defaultSubType = this.subTypeChoices.filter(item => !item.disabled)[0]
this.subTypeSelected = defaultSubType.name
this.inputPlaceholder = defaultSubType.placeholder
this.visible = true
}).catch(() => {
this.$emit('AuthMFAError', true)
})
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('auth.NeedReLogin')
this.visible = true
})
}
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('common.GetConfirmTypeFailed')
this.$message.error(msg)
this.cancel(err)
}).finally(() => {
this.processing = false
})
},
logout() {
@@ -183,17 +181,10 @@ export default {
}, 1000)
})
},
afterConfirm() {
this.iHandler().then(res => {
this.$emit('onConfirmDone', res)
}).catch((e) => {
this.$emit('onHandlerError', e)
}).finally(() => {
this.$emit('onConfirmFinal')
this.visible = false
})
},
handleConfirm() {
if (this.confirmTypeRequired === 'relogin') {
return this.logout()
}
if (this.subTypeSelected === 'otp' && this.secretValue.length !== 6) {
return this.$message.error(this.$tc('common.MFAErrorMsg'))
}
@@ -203,7 +194,8 @@ export default {
secret_key: this.secretValue
}
this.$axios.post(`/api/v1/authentication/confirm/`, data).then(res => {
this.afterConfirm()
this.callback()
this.visible = false
})
}
}

View File

@@ -1,13 +1,5 @@
<template>
<div>
<div v-if="mfaDialogShow">
<UserConfirmDialog
:url="confirmUrl"
@AuthMFAError="handleAuthMFAError"
@onConfirmCancel="handleExportCancel"
@onConfirmDone="showExportDialog"
/>
</div>
<Dialog
v-if="exportDialogShow"
:destroy-on-close="true"
@@ -40,7 +32,9 @@
:disabled="!option.can"
:label="option.value"
class="export-item"
>{{ option.label }}</el-radio>
>
{{ option.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
@@ -50,15 +44,13 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import UserConfirmDialog from '@/components/Apps/UserConfirmDialog/index.vue'
import { createSourceIdCache } from '@/api/common'
import * as queryUtil from '@/components/Table/DataTable/compenents/el-data-table/utils/query'
export default {
name: 'ExportDialog',
components: {
Dialog,
UserConfirmDialog
Dialog
},
props: {
selectedRows: {
@@ -107,11 +99,10 @@ export default {
},
data() {
return {
meta: {},
exportDialogShow: false,
exportOption: 'all',
exportTypeOption: 'csv',
meta: {},
mfaVerified: false,
mfaDialogShow: false,
confirmUrl: '/api/v1/accounts/account-secrets/?limit=1'
}
@@ -189,12 +180,9 @@ export default {
this.exportDialogShow = true
return
}
// 这是需要校验 MFA 的
if (!this.mfaDialogShow) {
this.mfaDialogShow = true
} else {
this.$axios.get('/api/v1/authentication/confirm/check/?confirm_type=mfa').then(() => {
this.exportDialogShow = true
}
})
},
downloadCsv(url) {
const a = document.createElement('a')
@@ -231,13 +219,11 @@ export default {
async handleExportConfirm() {
await this.handleExport()
this.exportDialogShow = false
this.mfaDialogShow = false
},
handleExportCancel() {
const vm = this
setTimeout(() => {
vm.exportDialogShow = false
vm.mfaDialogShow = false
}, 100)
},
handleAuthMFAError() {

View File

@@ -64,7 +64,7 @@ export default {
window.location.href = `${process.env.VUE_APP_LOGOUT_PATH}?next=${this.$route.fullPath}`
break
case 'apiKey':
this.$router.push('/profile/key')
this.$router.push('/profile/api-keys')
break
case 'tempPassword':
this.$router.push('/profile/temp-password')

View File

@@ -15,16 +15,19 @@
</el-alert>
<slot />
</PageContent>
<UserConfirmDialog />
</div>
</template>
<script>
import PageHeading from './PageHeading'
import PageContent from './PageContent'
import UserConfirmDialog from '@/components/Apps/UserConfirmDialog/index.vue'
export default {
name: 'Page',
components: {
UserConfirmDialog,
PageHeading,
PageContent
},

View File

@@ -8,6 +8,7 @@ import App from './App'
import store from './store'
import router from './router'
import i18n from './i18n/i18n'
import { eventBus } from './utils/const'
import '@/icons' // icon
import '@/guards' // permission control
@@ -65,7 +66,7 @@ Vue.prototype.$message = message
Vue.prototype.$xss = xss
// 注册全局事件总线
Vue.prototype.$eventBus = new Vue()
Vue.prototype.$eventBus = eventBus
new Vue({
el: '#app',
i18n,

View File

@@ -53,7 +53,7 @@ export default {
meta: { title: i18n.t('route.PersonalInformationImprovement'), permissions: [] }
},
{
path: '/profile/key',
path: '/profile/api-keys',
component: () => import('@/views/profile/ApiKey'),
name: 'ApiKey',
meta: {

View File

@@ -5,7 +5,8 @@ const getDefaultState = () => {
metaMap: {},
metaPromiseMap: {},
isRouterAlive: true,
sqlQueryCounter: []
sqlQueryCounter: [],
confirmDialogVisible: false
}
}
@@ -27,6 +28,9 @@ const mutations = {
if (state.sqlQueryCounter.length > 10) {
state.sqlQueryCounter.shift()
}
},
setConfirmDialogVisible: (state, show) => {
state.confirmDialogVisible = show
}
}
@@ -74,6 +78,9 @@ const actions = {
return
}
commit('addSQLQueryCounter', { url, count: sqlCount })
},
showConfirmDialog({ commit, state }, show) {
commit('setConfirmDialogVisible', show)
}
}

6
src/utils/const.js Normal file
View File

@@ -0,0 +1,6 @@
import Vue from 'vue'
export const eventBus = new Vue()
export default {
eventBus
}

View File

@@ -1,5 +1,6 @@
import axios from 'axios'
import i18n from '@/i18n/i18n'
import { eventBus } from '@/utils/const'
import { getTokenFromCookie } from '@/utils/auth'
import { getErrorResponseMsg } from '@/utils/common'
import { refreshSessionIdAge } from '@/api/users'
@@ -119,6 +120,17 @@ function refreshSessionAgeDelay(response) {
}, 30 * 1000)
}
function ifConfirmRequired({ response, error }) {
if (response.status !== 412) {
return null
}
return new Promise((resolve, reject) => {
const callback = () => resolve()
const cancel = () => reject()
eventBus.$emit('showConfirmDialog', { response, callback, cancel })
})
}
// response interceptor
service.interceptors.response.use(
/**
@@ -142,16 +154,27 @@ service.interceptors.response.use(
}
return res
},
error => {
async error => {
// NProgress.done()
if (!error.response) {
return Promise.reject(error)
}
const response = error.response
ifUnauthorized({ response, error })
ifBadRequest({ response, error })
flashErrorMsg({ response, error })
const confirming = ifConfirmRequired({ response, error })
if (confirming) {
return new Promise((resolve, reject) => {
confirming.then(() => {
resolve(service(error.config))
}).catch(() => {
reject(error)
})
})
}
await ifUnauthorized({ response, error })
await ifBadRequest({ response, error })
await flashErrorMsg({ response, error })
return Promise.reject(error)
}
)

View File

@@ -47,11 +47,11 @@ export default {
cursor: pointer;
}
& > > > .table-content {
& >>> .table-content {
margin-left: 21px;
}
& ::v-deep .el-table__row {
& >>> .el-table__row {
height: 40px;
& > td {

View File

@@ -6,19 +6,12 @@
:help-message="helpMessage"
:table-config="tableConfig"
/>
<UserConfirmDialog
v-if="mfaDialogVisible"
:handler="createAccessKey"
@close="mfaDialogVisible = false"
@onConfirmDone="showAccessKeyDialog"
/>
<Dialog
:show-cancel="false"
:title="$tc('profile.CreateAccessKey')"
:visible.sync="visible"
width="700px"
@close="onClose"
@confirm="onConfirm"
@confirm="visible = false"
>
<el-alert type="warning">
{{ warningText }}
@@ -41,21 +34,17 @@
import { GenericListPage } from '@/layout/components'
import { DateFormatter } from '@/components/Table/TableFormatters'
import Dialog from '@/components/Dialog/index.vue'
import UserConfirmDialog from '@/components/Apps/UserConfirmDialog/index.vue'
export default {
components: {
UserConfirmDialog,
Dialog,
GenericListPage
},
data() {
const ajaxUrl = '/api/v1/authentication/access-keys/'
const vm = this
return {
mfaUrl: '',
mfaDialogVisible: false,
createAccessKey: () => vm.$axios.post(ajaxUrl),
helpMessage: this.$t('setting.helpText.ApiKeyList'),
warningText: this.$t('profile.ApiKeyWarning'),
visible: false,
@@ -130,10 +119,10 @@ export default {
type: 'primary',
can: () => this.$hasPerm('authentication.add_accesskey'),
callback: function() {
this.mfaDialogVisible = false
setTimeout(() => {
this.mfaDialogVisible = true
}, 100)
this.$axios.post(ajaxUrl).then(res => {
this.key = res
this.visible = true
})
}.bind(this)
}
]
@@ -146,20 +135,6 @@ export default {
}
},
methods: {
showAccessKeyDialog(res) {
this.key = res
this.visible = true
setTimeout(() => {
this.mfaDialogVisible = false
})
},
onClose() {
this.getRefsListTable.reloadTable()
},
onConfirm() {
this.visible = false
this.getRefsListTable.reloadTable()
}
}
}
</script>

View File

@@ -9,6 +9,7 @@
:show-buttons="false"
:title="$tc('auth.AddPassKey')"
:visible.sync="dialogVisible"
width="600px"
>
<el-alert v-if="!isLocalUser" :closable="false" class="source-alert" type="error">
{{ $t('profile.PasskeyAddDisableInfo', {source: source.label}) }}
@@ -142,6 +143,9 @@ export default {
this.getRefsListTable.reloadTable()
this.$message.success(this.$tc('common.createSuccessMsg'))
}).catch((error) => {
if (error.response.status === 412) {
return
}
alert(error)
})
},

View File

@@ -1,12 +1,5 @@
<template>
<Page v-bind="$attrs">
<UserConfirmDialog
v-if="showPasswordDialog"
:url="confirmUrl"
:visible.sync="showPasswordDialog"
@onConfirmCancel="exit"
@onConfirmDone="verifyDone"
/>
<div>
<el-row :gutter="20">
<el-col :md="14" :sm="24">
@@ -35,7 +28,6 @@
import Page from '@/layout/components/Page'
import DetailCard from '@/components/Cards/DetailCard'
import QuickActions from '@/components/QuickActions'
import UserConfirmDialog from '@/components/Apps/UserConfirmDialog'
import { toSafeLocalDateStr } from '@/utils/common'
import store from '@/store'
@@ -44,8 +36,7 @@ export default {
components: {
Page,
DetailCard,
QuickActions,
UserConfirmDialog
QuickActions
},
props: {
object: {
@@ -72,7 +63,7 @@ export default {
callbacks: {
click: function() {
this.currentEdit = 'wecom'
this.showPasswordDialog = true
this.verifyDone()
}.bind(this)
}
},
@@ -89,7 +80,7 @@ export default {
callbacks: {
click: function() {
this.currentEdit = 'dingtalk'
this.showPasswordDialog = true
this.verifyDone()
}.bind(this)
}
},
@@ -106,7 +97,7 @@ export default {
callbacks: {
click: function() {
this.currentEdit = 'feishu'
this.showPasswordDialog = true
this.verifyDone()
}.bind(this)
}
},
@@ -298,7 +289,7 @@ export default {
confirmUrl() {
return '/api/v1/authentication/confirm-oauth/'
},
bindOrUNBindUrl() {
bindOrUnbindUrl() {
let url = ''
if (!this.object[`${this.currentEdit}_id`]) {
url = `/core/auth/${this.currentEdit}/qr/bind/?redirect_url=${this.$route.fullPath}`
@@ -344,16 +335,17 @@ export default {
return backendList
},
verifyDone() {
const url = this.bindOrUNBindUrl
if (!this.object[`${this.currentEdit}_id`]) {
window.open(url, 'Bind', 'width=800,height=600')
} else {
this.$axios.post(url).then(res => {
this.$message.success(this.$tc('common.updateSuccessMsg'))
this.$store.dispatch('users/getProfile')
})
}
this.showPasswordDialog = false
this.$axios.get(this.confirmUrl).then(() => {
const url = this.bindOrUnbindUrl
if (!this.object[`${this.currentEdit}_id`]) {
window.open(url, 'Bind', 'width=800,height=600')
} else {
this.$axios.post(url).then(res => {
this.$message.success(this.$tc('common.updateSuccessMsg'))
this.$store.dispatch('users/getProfile')
})
}
})
},
exit() {
this.$emit('update:visible', false)