perf: 优化确认的 dialog

This commit is contained in:
ibuler
2023-10-09 18:36:31 +08:00
committed by 老广
parent 8bc617c4a7
commit 50af6fe017
7 changed files with 217 additions and 154 deletions

View File

@@ -1,12 +1,6 @@
<template>
<div>
<div v-if="mfaDialogVisible">
<UserConfirmDialog
:url="url"
@UserConfirmCancel="exit"
@UserConfirmDone="getAuthInfo"
/>
</div>
<UserConfirmDialog :handler="getAuthInfo" @onConfirmDone="showSecretDialog" />
<Dialog
:destroy-on-close="true"
:show-cancel="false"
@@ -147,11 +141,12 @@ export default {
})
},
getAuthInfo() {
this.$axios.get(this.url, { disableFlashErrorMsg: true }).then(resp => {
this.secretInfo = resp
this.sshKeyFingerprint = resp?.spec_info?.ssh_key_fingerprint || '-'
this.showSecret = true
})
return this.$axios.get(this.url, { disableFlashErrorMsg: true })
},
showSecretDialog(res) {
this.secretInfo = res
this.sshKeyFingerprint = res?.spec_info?.ssh_key_fingerprint || '-'
this.showSecret = true
},
exit() {
this.$emit('update:visible', false)

View File

@@ -1,20 +1,19 @@
<template>
<Dialog
:destroy-on-close="true"
:show-cancel="false"
:show-confirm="false"
:title="title"
:visible.sync="visible"
:width="'36%'"
class="dialog-content"
v-bind="$attrs"
width="600px"
@confirm="visible = false"
v-on="$listeners"
>
<div v-if="ConfirmType === 'relogin'">
<div v-if="confirmTypeRequired === 'relogin'">
<el-row :gutter="24" style="margin: 0 auto;">
<el-col :md="24" :sm="24">
<el-alert
:closable="false"
:title="$tc('auth.ReLoginTitle')"
center
style="margin-bottom: 20px;"
@@ -24,12 +23,7 @@
</el-row>
<el-row :gutter="24" style="margin: 0 auto;">
<el-col :md="24" :sm="24">
<el-button
size="mini"
style="width: 100%; line-height:20px;"
type="primary"
@click="logOut"
>
<el-button class="confirm-btn" size="mini" type="primary" @click="logout">
{{ this.$t('auth.ReLogin') }}
</el-button>
</el-col>
@@ -39,14 +33,13 @@
<el-row :gutter="24" style="margin: 0 auto;">
<el-col :md="24" :sm="24" :span="24" class="add">
<el-select
v-model="Select"
:disabled="ConfirmType === 'password'"
v-model="subTypeSelected"
style="width: 100%; margin-bottom: 20px;"
@change="helpText(Select)"
@change="handleSubTypeChange"
>
<el-option
v-for="(item, i) of Content"
:key="i"
v-for="item of subTypeChoices"
:key="item.name"
:disabled="item.disabled"
:label="item.display_name"
:value="item.name"
@@ -56,28 +49,23 @@
</el-row>
<el-row :gutter="24" style="margin: 0 auto;">
<el-col :md="24" :sm="24" style="display: flex; margin-bottom: 20px;">
<el-input v-model="SecretKey" :placeholder="HelpText" :show-password="showPassword" />
<span v-if="Select === 'sms'" style="margin: -1px 0 0 20px;">
<el-input v-model="secretValue" :placeholder="inputPlaceholder" :show-password="showPassword" />
<span v-if="subTypeSelected === 'sms'" style="margin: -1px 0 0 20px;">
<el-button
:disabled="smsBtndisabled"
:disabled="smsBtnDisabled"
size="mini"
style="line-height:20px; float: right;"
type="primary"
@click="sendChallengeCode"
@click="sendSMSCode"
>
{{ smsBtnText }}
</el-button>
</span>
</el-col>
</el-row>
<el-row :gutter="24" style="margin: 0 auto;">
<el-row :gutter="24" style="margin: 10px auto;">
<el-col :md="24" :sm="24">
<el-button
size="mini"
style="width: 100%; line-height:20px;"
type="primary"
@click="userConfirm"
>
<el-button class="confirm-btn" size="mini" type="primary" @click="handleConfirm">
{{ this.$t('common.Confirm') }}
</el-button>
</el-col>
@@ -96,125 +84,125 @@ export default {
props: {
url: {
type: String,
default: () => ''
default: ''
},
handler: {
type: Function,
default: null
}
},
data() {
return {
title: '',
title: this.$t('common.CurrentUserVerify'),
smsWidth: 0,
Select: '',
Level: null,
HelpText: '',
smsBtnText: '',
smsBtndisabled: false,
ConfirmType: '',
Content: null,
SecretKey: '',
subTypeSelected: '',
inputPlaceholder: '',
smsBtnText: this.$t('common.SendVerificationCode'),
smsBtnDisabled: false,
confirmTypeRequired: '',
subTypeChoices: [],
secretValue: '',
visible: false
}
},
computed: {
showPassword() {
if (this.ConfirmType === 'password') {
return true
return this.confirmTypeRequired === 'password'
},
iHandler() {
if (this.handler === null) {
return () => this.$axios.get(this.url, { disableFlashErrorMsg: true })
}
return false
return this.handler
}
},
watch: {
visible(val) {
if (!val) {
this.$emit('UserConfirmCancel', true)
this.$emit('onConfirmCancel', true)
}
}
},
mounted() {
this.smsBtnText = this.$t('common.SendVerificationCode')
this.$axios.get(`${this.url}`, { disableFlashErrorMsg: true }).then(
() => { this.$emit('UserConfirmDone', true) }).catch((err) => {
const confirm_type = err.response.data.code
this.$axios.get('/api/v1/authentication/confirm/', { params: { confirm_type: confirm_type }}).then((data) => {
this.ConfirmType = data.confirm_type
this.Content = data.content
if (this.ConfirmType === 'relogin') {
this.$axios.post(
`/api/v1/authentication/confirm/`,
{
confirm_type: this.ConfirmType,
secret_key: ''
},
{ disableFlashErrorMsg: true },
).then(() => { this.$emit('UserConfirmDone', true) }).catch(() => {
this.title = this.$t('auth.NeedReLogin')
this.visible = true
})
return
}
if (this.ConfirmType === 'mfa') {
this.Select = this.Content.filter(item => !item.disabled)[0].name
if (this.Select === 'sms') {
this.smsWidth = 6
}
this.HelpText = this.Content.filter(item => !item.disabled)[0].placeholder
} else if (this.ConfirmType === 'password') {
this.Select = this.$t('setting.password')
this.HelpText = this.$t('common.PasswordRequireForSecurity')
this.Content = [{ 'name': 'password' }]
}
this.title = this.$t('common.CurrentUserVerify')
this.visible = true
}).catch(() => {
this.$emit('AuthMFAError', true)
})
})
this.performConfirm()
},
methods: {
helpText(val) {
this.HelpText = this.Content.filter(item => item.name === val)[0]?.placeholder
if (val === 'sms') {
this.smsWidth = 6
} else {
this.smsWidth = 0
}
handleSubTypeChange(val) {
this.inputPlaceholder = this.subTypeChoices.filter(item => item.name === val)[0]?.placeholder
this.smsWidth = val === 'sms' ? 6 : 0
},
logOut() {
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
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)
})
})
},
logout() {
window.location.href = `${process.env.VUE_APP_LOGOUT_PATH}?next=${this.$route.fullPath}`
},
sendChallengeCode() {
this.$axios.post(
`/api/v1/authentication/mfa/select/`, {
type: 'sms'
}
).then(res => {
this.$message.success(this.$t('common.VerificationCodeSent'))
sendSMSCode() {
this.$axios.post(`/api/v1/authentication/mfa/select/`, { type: 'sms' }).then(res => {
this.$message.success(this.$tc('common.VerificationCodeSent'))
let time = 60
const interval = setInterval(() => {
const originText = this.smsBtnText
this.smsBtnText = this.$t('common.Pending') + `: ${time}`
this.smsBtndisabled = true
this.smsBtnDisabled = true
time -= 1
if (time === 0) {
this.smsBtnText = this.$t('common.SendVerificationCode')
this.smsBtndisabled = false
this.smsBtnText = originText
this.smsBtnDisabled = false
clearInterval(interval)
}
}, 1000)
})
},
userConfirm() {
if (this.Select === 'otp' && this.SecretKey.length !== 6) {
return this.$message.error(this.$t('common.MFAErrorMsg'))
afterConfirm() {
this.iHandler().then(res => {
this.$emit('onConfirmDone', res)
}).catch((e) => {
this.$emit('onHandlerError', e)
}).finally(() => {
this.visible = false
})
},
handleConfirm() {
if (this.subTypeSelected === 'otp' && this.secretValue.length !== 6) {
return this.$message.error(this.$tc('common.MFAErrorMsg'))
}
this.$axios.post(
`/api/v1/authentication/confirm/`, {
confirm_type: this.ConfirmType,
mfa_type: this.ConfirmType === 'password' ? undefined : this.Select,
secret_key: this.SecretKey
}
).then(res => {
this.$emit('UserConfirmDone', true)
const data = {
confirm_type: this.confirmTypeRequired,
mfa_type: this.confirmTypeRequired === 'mfa' ? this.subTypeSelected : '',
secret_key: this.secretValue
}
this.$axios.post(`/api/v1/authentication/confirm/`, data).then(res => {
this.afterConfirm()
})
}
}
@@ -228,5 +216,16 @@ export default {
.dialog-content >>> .el-dialog {
padding: 8px;
.el-dialog__body {
padding-top: 30px;
padding-bottom: 30px;
}
}
.confirm-btn {
width: 100%;
line-height: 20px;
}
</style>

View File

@@ -2,10 +2,10 @@
<div>
<div v-if="mfaDialogShow">
<UserConfirmDialog
:url="url"
:url="confirmUrl"
@AuthMFAError="handleAuthMFAError"
@UserConfirmCancel="handleExportCancel"
@UserConfirmDone="showExportDialog"
@onConfirmCancel="handleExportCancel"
@onConfirmDone="showExportDialog"
/>
</div>
<Dialog
@@ -112,7 +112,8 @@ export default {
exportTypeOption: 'csv',
meta: {},
mfaVerified: false,
mfaDialogShow: false
mfaDialogShow: false,
confirmUrl: '/api/v1/accounts/account-secrets/?limit=1'
}
},
computed: {
@@ -136,7 +137,6 @@ export default {
delete query[key]
}
}
return query
},
tableHasQuery() {

View File

@@ -5,6 +5,7 @@
<script>
import BaseFormatter from './base.vue'
import { toSafeLocalDateStr } from '@/utils/common'
export default {
name: 'DateFormatter',
extends: BaseFormatter,
@@ -13,7 +14,7 @@ export default {
if (this.cellValue) {
value = toSafeLocalDateStr(this.cellValue)
} else {
value = ''
value = '-'
}
// const locale = this.$i18n.locale
// const value = dt.toLocaleString(locale, { hourCycle: 'h23' })

View File

@@ -1,5 +1,4 @@
{
"": "",
"accounts": {
"AutoPush": "自动推送",
"GeneralAccounts": "普通账号",
@@ -1679,7 +1678,7 @@
"LDAPUser": "LDAP 用户",
"helpText": {
"TempPassword": "临时密码有效期为 300 秒,使用后立刻失效",
"ApiKeyList": "使用 Api key 签名请求头进行认证,每个请求的头部是不一样的, 相对于 Token 方式,更加安全,请查阅文档使用",
"ApiKeyList": "使用 Api key 签名请求头进行认证,每个请求的头部是不一样的, 相对于 Token 方式,更加安全,请查阅文档使用;<br>为降低泄露风险Secret 仅在生成时可以查看, 每个用户最多支持创建 10 个",
"ConnectionTokenList": "连接令牌是将身份验证和连接资产结合起来使用的一种认证信息支持用户一键登录到资产目前支持的组件包括KoKo、Lion、Magnus、Razor 等",
"authLdapSearchFilter": "可能的选项是(cn或uid或sAMAccountName=%(user)s)",
"authLdapSearchOu": "使用|分隔各OU",
@@ -1695,7 +1694,7 @@
"securityLoginLimitTime": "提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录",
"securityMaxIdleTime": "提示:如果超过该配置没有操作,连接会被断开 (单位:分)",
"securityPasswordExpirationTime": "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期提醒邮件将在密码过期前5天内由系统每天自动发送给用户",
"siteUrl": "eg: http://jumpserver.abc.com:8080",
"siteUrl": "eg: https://jumpserver.example.com:8080",
"terminalHeartbeatInterval": "单位: 秒",
"terminalSessionKeepDuration": "单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不受影响)",
"terminalTelnetRegex": "登录telnet服务器成功后的提示正则表达式如: Last\\s*login|success|成功",
@@ -1873,6 +1872,10 @@
"TestNodeAssetConnectivity": "测试资产节点可连接性",
"UpdateNodeAssetHardwareInfo": "更新节点资产硬件信息"
},
"profile": {
"CreateAccessKey": "创建访问密钥",
"ApiKeyWarning": "为降低 AccessKey 泄露的风险,只在创建时提供 Secret后续不可再进行查询请妥善保存。"
},
"users": {
"LunaSettingUpdate": "Luna 配置设置",
"KokoSettingUpdate": "Koko 配置设置",

View File

@@ -1,38 +1,80 @@
<template>
<GenericListPage
ref="GenericListTable"
:header-actions="headerActions"
:help-message="helpMessage"
:table-config="tableConfig"
/>
<div>
<GenericListPage
ref="GenericListTable"
:header-actions="headerActions"
: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"
@confirm="visible = false"
>
<el-alert type="warning">
{{ warningText }}
<div class="secret">
<div class="row">
<span class="col">ID:</span>
<span class="value">{{ key.id }}</span>
</div>
<div class="row">
<span class="col">Secret:</span>
<span class="value">{{ key.secret }}</span>
</div>
</div>
</el-alert>
</Dialog>
</div>
</template>
<script>
import { GenericListPage } from '@/layout/components'
import { DateFormatter, ShowKeyCopyFormatter } from '@/components/Table/TableFormatters'
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,
key: { id: '', secret: '' },
tableConfig: {
hasSelection: true,
url: ajaxUrl,
columns: ['id', 'secret', 'is_active', 'date_created', 'date_last_used', 'actions'],
columnsShow: {
min: ['id', 'actions'],
default: ['id', 'secret', 'is_active', 'date_created', 'actions']
min: ['id', 'actions']
},
columnsMeta: {
id: {
label: 'Access Key'
label: 'ID'
},
secret: {
label: 'Secret Key',
formatter: ShowKeyCopyFormatter
label: 'Secret',
formatter: () => {
return '********'
}
},
date_created: {
label: this.$t('common.DateCreated'),
@@ -65,7 +107,7 @@ export default {
this.getRefsListTable.reloadTable()
this.$message.success(this.$tc('common.updateSuccessMsg'))
}).catch(error => {
this.$message.error(this.$tc('common.updateErrorMsg' + ' ' + error))
this.$message.error(this.$t('common.updateErrorMsg') + ' ' + error)
})
}.bind(this)
}
@@ -75,9 +117,7 @@ export default {
}
},
headerActions: {
hasSearch: true,
hasRightActions: true,
hasRefresh: true,
hasMoreActions: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
@@ -89,12 +129,10 @@ export default {
type: 'primary',
can: () => this.$hasPerm('authentication.add_accesskey'),
callback: function() {
this.$axios.post(ajaxUrl).then(res => {
this.getRefsListTable.reloadTable()
this.$message.success(this.$tc('common.updateSuccessMsg'))
}).catch(error => {
this.$message.error(this.$tc('common.updateErrorMsg' + ' ' + error))
})
this.mfaDialogVisible = false
setTimeout(() => {
this.mfaDialogVisible = true
}, 100)
}.bind(this)
}
]
@@ -105,9 +143,36 @@ export default {
getRefsListTable() {
return this.$refs.GenericListTable.$refs.ListTable.$refs.ListTable || {}
}
},
methods: {
showAccessKeyDialog(res) {
this.key = res
this.visible = true
setTimeout(() => {
this.mfaDialogVisible = false
})
}
}
}
</script>
<style scoped>
.secret {
color: #2b2f3a;
margin-top: 20px;
}
.row {
margin-bottom: 10px;
}
.col {
width: 100px;
text-align: left;
display: inline-block;
}
.value {
font-weight: 600;
}
</style>

View File

@@ -4,8 +4,8 @@
v-if="showPasswordDialog"
:url="confirmUrl"
:visible.sync="showPasswordDialog"
@UserConfirmCancel="exit"
@UserConfirmDone="verifyDone"
@onConfirmCancel="exit"
@onConfirmDone="verifyDone"
/>
<div>
<el-row :gutter="20">
@@ -346,7 +346,7 @@ export default {
verifyDone() {
const url = this.bindOrUNBindUrl
if (!this.object[`${this.currentEdit}_id`]) {
window.location.href = url
window.open(url, 'Bind', 'width=800,height=600')
} else {
this.$axios.post(url).then(res => {
this.$message.success(this.$tc('common.updateSuccessMsg'))