Merge pull request #3450 from jumpserver/dev

v3.8.0
This commit is contained in:
Bryan
2023-10-19 03:33:53 -05:00
committed by GitHub
50 changed files with 661 additions and 412 deletions

View File

@@ -1,12 +1,5 @@
<template>
<div>
<div v-if="mfaDialogVisible">
<UserConfirmDialog
:url="url"
@UserConfirmCancel="exit"
@UserConfirmDone="getAuthInfo"
/>
</div>
<Dialog
:destroy-on-close="true"
:show-cancel="false"
@@ -67,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'
@@ -76,7 +68,6 @@ export default {
components: {
Dialog,
PasswordHistoryDialog,
UserConfirmDialog,
ShowKeyCopyFormatter
},
props: {
@@ -128,7 +119,10 @@ export default {
const url = `/api/v1/accounts/account-secrets/${this.account.id}/histories/?limit=1`
this.$axios.get(url, { disableFlashErrorMsg: true }).then(resp => {
this.versions = resp.count
this.showSecretDialog()
})
} else {
this.showSecretDialog()
}
},
methods: {
@@ -146,10 +140,10 @@ export default {
this.$message.success(this.$tc('common.updateSuccessMsg'))
})
},
getAuthInfo() {
this.$axios.get(this.url, { disableFlashErrorMsg: true }).then(resp => {
this.secretInfo = resp
this.sshKeyFingerprint = resp?.spec_info?.ssh_key_fingerprint || '-'
showSecretDialog() {
return this.$axios.get(this.url, { disableFlashErrorMsg: true }).then((res) => {
this.secretInfo = res
this.sshKeyFingerprint = res?.spec_info?.ssh_key_fingerprint || '-'
this.showSecret = true
})
},

View File

@@ -120,7 +120,6 @@ export default {
},
async mounted() {
await this.getUrlMeta()
await this.handleFieldChange()
},
methods: {
async getUrlMeta() {

View File

@@ -1,35 +1,31 @@
<template>
<Dialog
:destroy-on-close="true"
:close-on-click-modal="false"
:destory-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;"
type="info"
type="error"
/>
</el-col>
</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 +35,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 +51,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 +86,116 @@ 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: '',
visible: false
subTypeSelected: '',
inputPlaceholder: '',
smsBtnText: this.$t('common.SendVerificationCode'),
smsBtnDisabled: false,
confirmTypeRequired: '',
subTypeChoices: [],
secretValue: '',
visible: false,
callback: null,
cancel: null,
processing: false
}
},
computed: {
showPassword() {
if (this.ConfirmType === 'password') {
return true
}
return false
}
},
watch: {
visible(val) {
if (!val) {
this.$emit('UserConfirmCancel', true)
}
return this.confirmTypeRequired === 'password'
}
},
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(() => {
// 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({ 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('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.subTypeChoices = data.content
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)
}).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
})
})
},
methods: {
helpText(val) {
this.HelpText = this.Content.filter(item => item.name === val)[0]?.placeholder
if (val === 'sms') {
this.smsWidth = 6
} else {
this.smsWidth = 0
}
},
logOut() {
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'))
handleConfirm() {
if (this.confirmTypeRequired === 'relogin') {
return this.logout()
}
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)
if (this.subTypeSelected === 'otp' && this.secretValue.length !== 6) {
return this.$message.error(this.$tc('common.MFAErrorMsg'))
}
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.callback()
this.visible = false
})
}
}
@@ -228,5 +209,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

@@ -16,9 +16,8 @@
>
<i :class="item.icon" style="margin-right: 4px;" />{{ item.name }}
</el-button>
<el-autocomplete
v-if="item.type === 'input' && item.el.autoComplete"
v-if="item.type === 'input' &&item.el && item.el.autoComplete"
v-model="item.value"
:placeholder="item.placeholder"
:fetch-suggestions="item.el.query"
@@ -27,7 +26,14 @@
@select="item.callback(item.value)"
@change="item.callback(item.value)"
/>
<el-input
v-else-if="item.type==='input'"
v-model="item.value"
:placeholder="item.placeholder"
class="inline-input"
size="mini"
@change="item.callback(item.value)"
/>
<div v-if="item.type==='select' && item.el && item.el.create" class="select-content">
<span class="filter-label">
{{ item.name }}:

View File

@@ -1,6 +1,6 @@
<template>
<div class="input-text">
{{ value.toString() }}
<div :class="bolder ? 'bolder' : ''" class="input-text">
{{ value.toString() || text }}
</div>
</template>
@@ -9,7 +9,15 @@ export default {
props: {
value: {
type: [String, Boolean],
default: () => false
default: ''
},
text: {
type: String,
default: ''
},
bolder: {
type: Boolean,
default: true
}
},
data() {
@@ -20,12 +28,14 @@ export default {
<style lang='scss' scoped>
.input-text {
border: solid 1px #dcdfe6;
line-height: 32px;
padding-left: 5px;
padding-left: 8px;
height: 32px;
margin-top: 4px;
font-size: 13px;
}
.bolder {
border: solid 1px #dcdfe6;
}
</style>

View File

@@ -1,13 +1,5 @@
<template>
<div>
<div v-if="mfaDialogShow">
<UserConfirmDialog
:url="url"
@AuthMFAError="handleAuthMFAError"
@UserConfirmCancel="handleExportCancel"
@UserConfirmDone="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,12 +99,12 @@ export default {
},
data() {
return {
meta: {},
exportDialogShow: false,
exportOption: 'all',
exportTypeOption: 'csv',
meta: {},
mfaVerified: false,
mfaDialogShow: false
mfaDialogShow: false,
confirmUrl: '/api/v1/accounts/account-secrets/?limit=1'
}
},
computed: {
@@ -136,7 +128,6 @@ export default {
delete query[key]
}
}
return query
},
tableHasQuery() {
@@ -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

@@ -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

@@ -36,6 +36,7 @@
@focus="focus = true"
@change="handleConfirm"
@keyup.enter.native="handleConfirm"
@keyup.delete.native="handleDelete"
/>
</div>
@@ -67,6 +68,7 @@ export default {
filterKey: '',
filterValue: '',
valueLabel: '',
emptyCount: 0,
filterTags: this.default || {},
focus: false
}
@@ -105,28 +107,32 @@ export default {
},
watch: {
options: {
handler(val) {
if (val && val.length > 0) {
const routeFilter = this.checkInTableColumns()
handler(newVal, oldVal) {
if (newVal && newVal.length > 0) {
const routeFilter = this.checkInTableColumns(newVal)
if (oldVal.length > 0 && newVal.length !== oldVal.length) {
const beforeRouteFilter = this.checkInTableColumns(oldVal)
// 如果2次过滤的参数相同就不在重复请求
if (_.isEqual(routeFilter, beforeRouteFilter)) {
return
}
}
this.filterTagSearch(routeFilter)
}
},
deep: true
}
},
mounted() {
setTimeout(() => {
if (Object.keys(this.filterMaps).length > 0) {
return this.$emit('tagSearch', this.filterMaps)
},
filterValue(newValue, oldValue) {
if (newValue === '' && oldValue !== '') {
this.emptyCount = 1
}
}, 400)
// this.$nextTick(() => this.$emit('tagSearch', this.filterMaps))
}
},
methods: {
// 获取url中的查询条件判断是不是包含在当前查询条件里
checkInTableColumns() {
checkInTableColumns(options) {
const searchFieldOptions = {}
const queryInfoValues = this.options.map((i) => i.value)
const queryInfoValues = options.map((i) => i.value)
const routeQuery = this.getUrlQuery ? this.$route?.query : {}
const routeQueryKeysLength = Object.keys(routeQuery).length
if (routeQueryKeysLength < 1) return searchFieldOptions
@@ -204,10 +210,10 @@ export default {
...asFilterTags,
...routeFilter
}
if (Object.keys(routeFilter).length > 0) {
if (Object.keys(this.filterTags).length > 0) {
setTimeout(() => {
return this.$emit('tagSearch', this.filterMaps)
}, 490)
}, 400)
}
},
getValueLabel(key, value) {
@@ -243,10 +249,22 @@ export default {
},
handleTagClose(evt) {
this.$delete(this.filterTags, evt)
this.checkUrlFilds(evt)
if (this.getUrlQuery) {
this.checkUrlFields(evt)
}
this.$emit('tagSearch', this.filterMaps)
return true
},
handleDelete() {
const filterTags = Object.keys(this.filterTags)
if (this.filterValue === '' && filterTags.length > 0) {
if (this.emptyCount === 2) {
this.handleTagClose(filterTags[filterTags.length - 1])
} else {
this.emptyCount++
}
}
},
handleConfirm() {
if (this.filterValue === '') {
this.handleTagClose(this.filterKey)
@@ -256,6 +274,9 @@ export default {
if (this.filterValue && !this.filterKey) {
this.filterKey = 'search' + '_' + this.filterValue
}
setTimeout(() => {
this.emptyCount = 2
}, 10)
const tag = {
key: this.filterKey,
label: this.keyLabel,
@@ -304,7 +325,7 @@ export default {
this.$refs.SearchInput.focus()
},
// 删除查询条件时改变url
checkUrlFilds(evt) {
checkUrlFields(evt) {
let newQuery = _.omit(this.$route.query, evt)
if (this.getUrlQuery && evt.startsWith('search')) {
if (newQuery.search) delete newQuery.search

View File

@@ -116,6 +116,8 @@
"AccountExportTips": "The exported information contains account secret, which involves sensitive information. The exported format is an encrypted zip file (if no encryption password is set, please go to personal information to set the file encryption password).",
"TaskID": "Task ID",
"AccountTemplate": "Account template",
"Sync": "Sync",
"SyncUpdateAccountInfo": "Sync update account info",
"AddAccountResult": "Add account result",
"AutoPush": "Auto Push",
"GeneralAccounts": "General Accounts",
@@ -228,6 +230,7 @@
"NoSQLProtocol": "NoSQL Protocol"
},
"assets": {
"SyncProtocolToAsset": "Sync protocol to asset",
"CommentHelpText": "Note: Note information will be hovered and displayed in the user authorization asset tree of Luna page, which can be viewed by ordinary users. Please do not fill in sensitive information.",
"BulkUpdatePlatformHelpText": "Only when the original platform type of the asset is the same as the selected platform type will it be updated, if the platform type before and after the update is different, it will not be updated.",
"ImportMessage": "Please go to the page of mapping type to import data",
@@ -983,10 +986,12 @@
},
"ops": {
"EnterRunUser": "Enter run user",
"EnterRunningPath": "Enter running path",
"Save": "Save",
"Reset": "Reset",
"SystemError": "System Error",
"RunasHelpText": "Fill in the user name to run the script",
"RunningPathHelpText": "Fill in the running path of the script. This setting only takes effect for shell scripts.",
"RunasPolicyHelpText": "Indicates the account selection strategy when there is no running user on the current asset",
"StatusGreen": "Recently in good condition",
"StatusRed": "The last task execution failed",
@@ -1021,6 +1026,7 @@
"ratio": "Ratio",
"run": "Run",
"runAs": "Run as",
"runningPath": "Running path",
"runTimes": "Run times",
"selectAssetsMessage": "Select the left asset, select the running system user, execute command in batch",
"selectedAssets": "Selected assets:",
@@ -1726,7 +1732,9 @@
"securityPasswordUpperCase": "After opening, the user password changes and resets must contain uppercase letters",
"securityServiceAccountRegistration": "Allow using bootstrap token register service account, when terminal setup, can disable it",
"SecurityInsecureCommand": "After it is enabled, when a dangerous command is executed on the asset, an email alarm notification will be sent",
"IP": "192.168.1.1,192.168.1.2 | 192.168.1.1-12 | 192.168.1.1-192.168.1.12 | 192.168.1.0/30 | 192.168.1.1"
"IP": "192.168.1.1,192.168.1.2 | 192.168.1.1-12 | 192.168.1.1-192.168.1.12 | 192.168.1.0/30 | 192.168.1.1",
"CustomParams": "On the left are the parameters received by the SMS platform, and on the right are the JumpServer parameters to be formatted, as follows:<br/>{\"phone_numbers\": \"123,134\", \"content\": \"The verification code is: 666666\"}",
"CustomFile": "Save the customized file to the specified directory (data/sms/main.py) and enable the configuration item SMS_CUSTOM_FILE_MD5=<file md5> in config.txt"
},
"validatorMessage": {
"EnsureThisValueIsGreaterThanOrEqualTo3": "Ensure this value is greater than or equal to 3",
@@ -1925,8 +1933,7 @@
"UserName": "Name",
"Account": "Account",
"Existing": "Existing",
"AccountInformation": "Account information",
"PersonalSetting": "Personal setting",
"UserInformation": "User information",
"Authentication": "Account",
"Comment": "Comment",
"ConfirmPassword": "Confirm password",
@@ -1955,7 +1962,7 @@
"OrgAuditor": "Org Auditor",
"HelpText": {
"MFAOfUserFirstLoginPersonalInformationImprovementPage": "Enable multi-factor authentication to make the account more secure <br/> After is enabled, you will enter the multi-factor authentication binding process on your next login <br/> You can also bind directly in (personal information -> fast modifier -> modifier multiple factor Settings)",
"MFAOfUserFirstLoginUserGuidePage": "To protect the security of you and the company <br/> please properly keep your account, password, key and other important and sensitive information <br/> (e.g., set a complex password and enable multi-factor authentication)",
"MFAOfUserFirstLoginUserGuidePage": "To protect the security of you and the company <br/> please properly keep your account, password, key and other important and sensitive information <br/> (e.g., set a complex password and enable multi-factor authentication) <br> Personal information such as email, phone number, WeChat, etc. is only used for user authentication and platform internal message notifications.",
"SSHKeyOfProfileSSHUpdatePage": "Copy your public key here",
"OrgRoleHelpText": "Organizational roles are the user's role in the current organization",
"addRolePermissions": "After add or update success, set permissions in detail page"
@@ -1973,6 +1980,7 @@
"OldSSHKey": "Old SSH key",
"Profile": "Profile",
"ProfileSetting": "Profile setting",
"AuthSetting": "Auth setting",
"Remove": "Remove",
"ResetAndDownloadSSHKey": "Reset and download SSH Key",
"ResetPublicKeyAndDownload": "Reset public key and download",
@@ -2306,5 +2314,10 @@
"applets": {
"PublishStatus": "Publish status",
"NoPublished": "Unpublished"
},
"profile": {
"CreateAccessKey": "Create Access key",
"ApiKeyWarning": "To reduce the risk of AccessKey exposure, Secret is provided only during creation and cannot be queried again later. Please keep it safe.",
"PasskeyAddDisableInfo": "Your authentication source is {source}, and Passkey addition is not supported."
}
}

View File

@@ -116,6 +116,8 @@
"AccountExportTips": "エクスポート情報には機密情報を含むアカウント暗号文が含まれており、エクスポートされたフォーマットは暗号化されたzipファイルです暗号化パスワードが設定されていない場合は、個人情報にファイル暗号化パスワードを設定してください。",
"TaskID": "タスク ID",
"AccountTemplate": "账号模版",
"Sync": "同期",
"SyncUpdateAccountInfo": "アカウント情報の同期更新",
"AddAccountResult": "账号批量添加结果",
"AutoPush": "自動プッシュ",
"GeneralAccounts": "一般アカウント",
@@ -228,6 +230,7 @@
"NoSQLProtocol": "非リレーショナルデータベース"
},
"assets": {
"SyncProtocolToAsset": "プロトコルをアセットに同期",
"CommentHelpText": "注意コメント情報はLunaページのユーザー認可資産ツリーに表示されます。一般ユーザーは表示できますので、機密情報を記入しないでください。",
"BulkUpdatePlatformHelpText": "アセットの元のプラットフォーム タイプが選択したプラットフォーム タイプと同じ場合にのみ更新され、更新の前後のプラットフォーム タイプが異なる場合は更新されません。",
"ImportMessage": "ミラータイプのページにデータをインポートしてください",
@@ -981,11 +984,13 @@
},
"ops": {
"EnterRunUser": "実行ユーザーの入力",
"EnterRunningPath": "実行パスの入力",
"Save": "保存#ホゾン#",
"Reset": "リストア",
"SystemError": "システムエラー",
"RunasHelpText": "スクリプトを実行するユーザー名を入力",
"RunasPolicyHelpText": "現在の資産にこの実行ユーザーが存在しない場合にアカウント選択ポリシーを実行することを示します",
"RunningPathHelpText": "スクリプトの実行パスを入力します。この設定はシェル スクリプトにのみ有効です",
"StatusGreen": "最近調子がいい",
"StatusRed": "最後のタスクの実行に失敗しました",
"StatusYellow": "最近の存在の実行に失敗しました",
@@ -1019,6 +1024,7 @@
"ratio": "スケール",
"run": "実行",
"runAs": "実行ユーザー",
"runningPath": "実行パス",
"runTimes": "実行回数",
"selectAssetsMessage": "左側の資産を選択し、実行するシステムユーザーを選択し、コマンドを一括実行します",
"selectedAssets": "選択済アセット:",
@@ -1731,7 +1737,9 @@
"securityPasswordUpperCase": "オンにすると、ユーザーパスワードの変更、リセットに大文字を含める必要があります",
"securityServiceAccountRegistration": "Bootstrap tokenを使用して端末を登録できるようにし、端末の登録が成功すると禁止できるようにします",
"SecurityInsecureCommand": "オンにすると、資産に危険なコマンドが実行されたときに、メールの警告通知が送信されます",
"IP": "192.168.1.1,192.168.1.2 | 192.168.1.1-12 | 192.168.1.1-192.168.1.12 | 192.168.1.0/30 | 192.168.1.1"
"IP": "192.168.1.1,192.168.1.2 | 192.168.1.1-12 | 192.168.1.1-192.168.1.12 | 192.168.1.0/30 | 192.168.1.1",
"CustomParams": "左側はSMSプラットフォームで受信されるパラメータ、右側はJumpServerフォーマットされるパラメータで、最終的には次のようになります。<br/>{\"phone_numbers\": \"123,134\", \"content\": \"認証コードは: 666666\"}",
"CustomFile": "カスタムファイルを指定のディレクトリ(data/sms/main.py)に置き、config.txtで設定項目SMS_CUSTOM_FILE_MD5=<ファイルmd5値>を有効にします"
},
"validatorMessage": {
"EnsureThisValueIsGreaterThanOrEqualTo3": "この値が3以上であることを確認してください",
@@ -1914,8 +1922,7 @@
"AuthSettings": "認証設定",
"UserName": "氏名",
"Account": "アカウント",
"AccountInformation": "アカウント情報",
"PersonalSetting": "個人設定",
"UserInformation": "アカウント情報",
"Authentication": "認証",
"Comment": "備考",
"ConfirmPassword": "パスワードの確認",
@@ -1944,7 +1951,7 @@
"setFeiShu": "飛書認証を設定する",
"HelpText": {
"MFAOfUserFirstLoginPersonalInformationImprovementPage": "アカウントをより安全にするために、マルチファクタ认证を有効にします。 <Br/> 有効にすると、次回のログイン時に多因子認証バインディングプロセスに入ります。また、 (個人情報-> クイック修正-> 多因子設定の変更) もできます。で直接紐付けます",
"MFAOfUserFirstLoginUserGuidePage": "あなたと会社の安全を守るために、あなたのアカウント、パスワード、鍵などの重要な機密情報を大切に保管してください (例: 複雑なパスワードを設定し、多因子認証を有効にする)",
"MFAOfUserFirstLoginUserGuidePage": "あなたと会社の安全を守るために、あなたのアカウント、パスワード、鍵などの重要な機密情報を大切に保管してください (例: 複雑なパスワードを設定し、多因子認証を有効にする) <br> メールボックス、携帯電話番号、微信などの個人情報は、ユーザー認証やプラットフォーム内部のメッセージ通知としてのみ使用されています。",
"SSHKeyOfProfileSSHUpdatePage": "公開鍵をここにコピーします",
"OrgRoleHelpText": "組織ロールは、現在の組織におけるユーザーのロールです",
"addRolePermissions": "作成/更新に成功したら、詳細に権限を追加します"
@@ -1963,6 +1970,7 @@
"OldSSHKey": "元のSSH公開鍵",
"Profile": "個人情報",
"ProfileSetting": "個人情報設定",
"AuthSetting": "資格認定の設定",
"Remove": "削除",
"ResetAndDownloadSSHKey": "キーをリセットしてダウンロードします",
"ResetPublicKeyAndDownload": "SSHキーをリセットしてダウンロードします",
@@ -2297,5 +2305,10 @@
"applets": {
"PublishStatus": "投稿ステータス",
"NoPublished": "未発表"
},
"profile": {
"CreateAccessKey": "Create Access key",
"ApiKeyWarning": "AccessKeyの漏洩リスクを低減するため、Secretは作成時にのみ提供され、後で再度クエリできません。安全に保管してください。",
"PasskeyAddDisableInfo": "あなたの認証元は {source} であり、Passkeyの追加はサポートされていません。"
}
}

View File

@@ -1,5 +1,4 @@
{
"": "",
"accounts": {
"AutoPush": "自动推送",
"GeneralAccounts": "普通账号",
@@ -11,6 +10,8 @@
"GenerateSuccessMsg": "账号生成成功",
"GenerateAccounts": "重新生成账号",
"UpdateSecret": "更新密文",
"Sync": "同步",
"SyncUpdateAccountInfo": "同步更新账号信息",
"AddAccountResult": "账号批量添加结果",
"AccountPolicy": "账号策略",
"BulkCreateStrategy": "创建时对于不符合要求的账号,如:密钥类型不合规,唯一键约束,可选择以上策略。",
@@ -229,6 +230,7 @@
},
"assets": {
"CustomType": "自定义类型",
"SyncProtocolToAsset": "同步协议到资产",
"CustomHelpMessage": "自定义类型资产,依赖于远程应用,请前往系统设置在远程应用中配置",
"CustomFields": "自定义属性",
"CommentHelpText": "注意:备注信息会在 Luna 页面的用户授权资产树中进行悬停显示,普通用户可以查看,请不要填写敏感信息。",
@@ -960,10 +962,12 @@
},
"ops": {
"EnterRunUser": "输入运行用户",
"EnterRunningPath": "输入运行路径",
"Save": "保存",
"Reset": "还原",
"SystemError": "系统错误",
"RunasHelpText": "填写运行脚本的用户名",
"RunningPathHelpText": "填写脚本的运行路径,此设置仅 shell 脚本生效",
"RunasPolicyHelpText": "当前资产上没此运行用户时,采取什么账号选择策略。跳过:不执行。优先特权账号:如果有特权账号先选特权账号,如果没有就选普通账号。仅特权账号:只从特权账号中选择,如果没有则不执行",
"StatusGreen": "近期状态良好",
"StatusRed": "上一次任务执行失败",
@@ -1057,6 +1061,7 @@
"ratio": "比例",
"run": "执行",
"runAs": "运行用户",
"runningPath": "运行路径",
"RunasPolicy": "账号策略",
"runTimes": "执行次数",
"selectAssetsMessage": "选择左侧资产, 选择运行的系统用户,批量执行命令",
@@ -1676,7 +1681,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",
@@ -1692,7 +1697,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|成功",
@@ -1710,7 +1715,9 @@
"securityPasswordUpperCase": "开启后,用户密码修改、重置必须包含大写字母",
"securityServiceAccountRegistration": "允许使用bootstrap token注册终端, 当组件注册成功后可以禁止",
"SecurityInsecureCommand": "开启后,当资产上有危险命令执行时,会发送邮件告警通知",
"IP": "192.168.1.1,192.168.1.2 | 192.168.1.1-12 | 192.168.1.1-192.168.1.12 | 192.168.1.0/30 | 192.168.1.1"
"IP": "192.168.1.1,192.168.1.2 | 192.168.1.1-12 | 192.168.1.1-192.168.1.12 | 192.168.1.0/30 | 192.168.1.1",
"CustomParams": "左侧为短信平台接收的参数右侧为JumpServer待格式化参数最终如下<br/>{\"phone_numbers\": \"123,134\", \"content\": \"验证码为: 666666\"}",
"CustomFile": "请将自定义的文件放到指定目录下(data/sms/main.py),并在 config.txt 中启用配置项 SMS_CUSTOM_FILE_MD5=<文件md5值>"
},
"validatorMessage": {
"EnsureThisValueIsGreaterThanOrEqualTo3": "请确保该值大于或者等于 3",
@@ -1870,10 +1877,15 @@
"TestNodeAssetConnectivity": "测试资产节点可连接性",
"UpdateNodeAssetHardwareInfo": "更新节点资产硬件信息"
},
"profile": {
"CreateAccessKey": "创建访问密钥",
"ApiKeyWarning": "为降低 AccessKey 泄露的风险,只在创建时提供 Secret后续不可再进行查询请妥善保存。",
"PasskeyAddDisableInfo": "你的认证来源是 {source}, 不支持添加 Passkey"
},
"users": {
"LunaSettingUpdate": "Luna 配置设置",
"KokoSettingUpdate": "Koko 配置设置",
"UserSetting": "个人设置",
"UserSetting": "偏好设置",
"AllMembers": "全部成员",
"UnbindHelpText": "本地用户为此认证来源用户,无法解绑",
"SetStatus": "设置状态",
@@ -1890,8 +1902,7 @@
"AuthSettings": "认证配置",
"UserName": "姓名",
"Account": "账户",
"AccountInformation": "账号信息",
"PersonalSetting": "个人设置",
"UserInformation": "用户信息",
"Authentication": "认证",
"Comment": "备注",
"ConfirmPassword": "确认密码",
@@ -1920,7 +1931,7 @@
"setFeiShu": "设置飞书认证",
"HelpText": {
"MFAOfUserFirstLoginPersonalInformationImprovementPage": "启用多因子认证,使账号更加安全。<br/> 启用之后您将会在下次登录时进入多因子认证绑定流程;您也可以在(个人信息->快速修改->更改多因子设置)中直接绑定!",
"MFAOfUserFirstLoginUserGuidePage": "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:设置复杂密码,并启用多因子认证)",
"MFAOfUserFirstLoginUserGuidePage": "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:设置复杂密码,并启用多因子认证)<br/> 邮箱、手机号、微信等个人信息,仅作为用户认证和平台内部消息通知使用。",
"SSHKeyOfProfileSSHUpdatePage": "复制你的公钥到这里",
"OrgRoleHelpText": "组织角色是用户在当前组织中的角色",
"addRolePermissions": "创建/更新成功后,详情中添加权限"
@@ -1939,6 +1950,7 @@
"OldSSHKey": "原来SSH公钥",
"Profile": "个人信息",
"ProfileSetting": "个人信息设置",
"AuthSetting": "认证设置",
"Remove": "移除",
"ResetAndDownloadSSHKey": "重置并下载密钥",
"ResetPublicKeyAndDownload": "重置并下载SSH密钥",

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1697615639399" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7535" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M122.88 232.106667l-2.56-2.986667H42.666667v-106.666667l81.066666 2.56v-4.266666a175.36 175.36 0 0 1 331.946667 0l2.56 2.56h532.053333v104.533333H454.826667v4.266667a174.933333 174.933333 0 0 1-331.946667 0z m212.437333-122.112A80.64 80.64 0 0 0 290.986667 95.573333h-1.706667a80.64 80.64 0 1 0 46.037333 14.421334zM515.413333 564.906667l2.56 2.986666a175.36 175.36 0 0 0 331.946667 0v-4.266666h140.373333v-104.533334H853.333333l-2.56-2.986666a175.36 175.36 0 0 0-331.946666 0v4.266666L42.666667 457.386667v107.52h472.746666z m170.666667-133.546667a80.597333 80.597333 0 0 1 54.698667 138.24A80.64 80.64 0 1 1 682.666667 431.36h3.413333z m-467.2 469.333333l2.56 2.56a175.36 175.36 0 0 0 331.946667 0v-4.266666l436.48 2.133333v-106.24H556.8l-2.133333-2.986667a174.933333 174.933333 0 0 0-331.946667 0v4.266667L42.666667 793.173333v107.52h176.213333z m170.666667-133.546666a80.64 80.64 0 1 1-82.346667 80.64A80.64 80.64 0 0 1 387.84 768l1.706667-0.853333z" fill="#515151" p-id="7536"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -8,9 +8,13 @@
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="profile">
<svg-icon class="icon" icon-class="personal" />
<svg-icon class="icon" icon-class="attestation" />
{{ $t('common.nav.Profile') }}
</el-dropdown-item>
<el-dropdown-item command="UserSetting">
<svg-icon class="icon" icon-class="preference" />
{{ $t('users.UserSetting') }}
</el-dropdown-item>
<el-dropdown-item v-if="$hasPerm('authentication.view_accesskey')" command="apiKey">
<svg-icon class="icon" icon-class="key" />
{{ $t('common.nav.APIKey') }}
@@ -22,10 +26,6 @@
<svg-icon class="icon" icon-class="unlock-one" />
{{ $t('common.nav.TempPassword') }}
</el-dropdown-item>
<el-dropdown-item v-if="$hasPerm('authentication.view_connectiontoken')" command="connectionToken">
<svg-icon class="icon" icon-class="token" />
{{ $t('common.nav.ConnectionToken') }}
</el-dropdown-item>
<el-dropdown-item command="logout" divided>
<svg-icon class="icon" icon-class="logout" />
{{ $t('common.nav.Logout') }}
@@ -64,13 +64,13 @@ 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')
break
case 'connectionToken':
this.$router.push('/profile/connection-token')
case 'UserSetting':
this.$router.push('/profile/user/setting')
}
},
logout() {

View File

@@ -42,10 +42,10 @@ export default {
},
methods: {
getIntervalDays(date) {
date = new Date(date)
const dateExpired = this.$moment(date, 'YYYY-MM-DD').format('YYYY-MM-DD')
const dateNow = this.$moment(new Date()).format('YYYY-MM-DD')
const intervalTime = this.$moment(dateNow).diff(this.$moment(dateExpired), 'days')
return intervalTime
return this.$moment(dateNow).diff(this.$moment(dateExpired), 'days')
}
}
}

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

@@ -28,7 +28,7 @@
</template>
</el-tabs>
<transition appear mode="out-in" name="fade-transform">
<transition v-if="loading" appear mode="out-in" name="fade-transform">
<slot>
<keep-alive>
<component :is="computeActiveComponent" />
@@ -60,6 +60,11 @@ export default {
required: true
}
},
data() {
return {
loading: true
}
},
computed: {
iActiveMenu: {
get() {
@@ -90,6 +95,18 @@ export default {
return needActiveComponent
}
},
watch: {
$route(to, from) {
const activeTab = to.query?.activeTab
if (activeTab && this.iActiveMenu !== activeTab) {
this.iActiveMenu = activeTab
this.loading = false
setTimeout(() => {
this.loading = true
})
}
}
},
created() {
this.iActiveMenu = this.getPropActiveTab()
},

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

@@ -20,7 +20,7 @@ export default {
name: 'ProfileInfo',
component: () => import('@/views/profile/ProfileInfo'),
meta: {
title: i18n.t('users.AccountInformation'),
title: i18n.t('users.UserInformation'),
icon: 'attestation',
permissions: []
}
@@ -30,11 +30,21 @@ export default {
name: 'ProfileSetting',
component: () => import('@/views/profile/ProfileUpdate/index'),
meta: {
title: i18n.t('users.Profile'),
title: i18n.t('users.AuthSetting'),
icon: 'personal',
permissions: []
}
},
{
path: '/profile/user/setting',
name: 'UserSetting',
component: () => import('@/views/profile/UserSettingUpdate/index'),
meta: {
title: i18n.t('users.UserSetting'),
icon: 'preference',
permissions: []
}
},
{
path: '/profile/improvement',
component: () => import('@/views/profile/ProfileImprovement'),
@@ -43,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: {
@@ -86,16 +96,6 @@ export default {
hidden: ({ settings }) => !settings['AUTH_PASSKEY'],
permissions: ['authentication.view_connectiontoken']
}
},
{
path: '/profile/user/setting',
name: 'UserSetting',
component: () => import('@/views/profile/UserSettingUpdate/index'),
meta: {
title: i18n.t('users.UserSetting'),
icon: 'setting',
permissions: []
}
}
]
}

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)
}
}

BIN
src/styles/icons/db2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

View File

@@ -88,6 +88,9 @@
background: url('./icons/unix.png') no-repeat center left transparent;
}
&.db2_ico_docu {
background: url('./icons/db2.png') no-repeat center left transparent;
}
&.private_ico_docu {
background: url('./icons/private.png') no-repeat center left transparent;

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

@@ -60,7 +60,7 @@ export default {
{ assets: [row.id] }
).then(res => {
this.$message.success(this.$tc('common.deleteSuccessMsg'))
reload()
this.$store.commit('common/reload')
}).catch(error => {
this.$message.error(this.$tc('common.deleteErrorMsg') + ' ' + error)
})
@@ -92,7 +92,7 @@ export default {
title: this.$t('accounts.AccountChangeSecret.AddAsset'),
disabled: this.$store.getters.currentOrgIsRoot,
canSelect: (row, index) => {
return this.object.assets.indexOf(row.id) === -1
return (this.object.assets?.map(i => i.id) || []).indexOf(row.id) === -1
},
performAdd: (items, that) => {
const relationUrl = `/api/v1/accounts/change-secret/${this.object.id}/asset/add/`
@@ -104,7 +104,7 @@ export default {
onAddSuccess: (items, that) => {
this.$log.debug('AssetSelect value', that.assets)
this.$message.success(this.$tc('common.updateSuccessMsg'))
window.location.reload()
this.$store.commit('common/reload')
}
},
nodeRelationConfig: {

View File

@@ -14,6 +14,14 @@ export default {
},
data() {
const vm = this
const isUpdate = vm.$route.path.indexOf('/update') > -1 && vm.$route.params?.id
const formFields = templateFields(vm)
for (const [key, value] of formFields) {
if (key === vm.$t('assets.Secret')) {
isUpdate && value.push('is_sync_account')
}
}
return {
initial: {
secret_type: 'password',
@@ -21,11 +29,28 @@ export default {
},
url: '/api/v1/accounts/account-templates/',
hasDetailInMsg: false,
fields: [
...templateFields(vm)
],
fields: formFields,
fieldsMeta: {
...templateFieldsMeta(vm)
...templateFieldsMeta(vm),
is_sync_account: {
label: this.$t('accounts.SyncUpdateAccountInfo'),
el: {
icon: 'fa fa-external-link',
type: 'primary',
size: 'mini'
},
component: 'el-button',
on: {
click: () => {
vm.$router.push({
name: 'AccountTemplateDetail',
query: {
activeTab: 'Account'
}
})
}
}
}
},
cleanFormValue(value) {
Object.keys(value).forEach((item, index, arr) => {
@@ -35,6 +60,7 @@ export default {
}
})
value['secret'] = encryptPassword(value['secret'])
delete value.is_sync_account
return value
},
createSuccessNextRoute: { name: 'AccountTemplateList' },

View File

@@ -20,10 +20,6 @@
:url="secretUrl"
:visible.sync="showViewSecretDialog"
/>
<AccountTemplateChangeSecretDialog
:object="object"
:visible.sync="visible"
/>
</el-row>
</div>
</template>
@@ -31,17 +27,16 @@
<script>
import GenericListTable from '@/layout/components/GenericListTable'
import QuickActions from '@/components/QuickActions'
import AccountTemplateChangeSecretDialog from './AccountTemplateChangeSecretDialog'
import { ActionsFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
import ViewSecret from '@/components/Apps/AccountListTable/ViewSecret'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'AccountTemplateChangeSecret',
components: {
ViewSecret,
QuickActions,
GenericListTable,
AccountTemplateChangeSecretDialog
GenericListTable
},
props: {
object: {
@@ -58,14 +53,18 @@ export default {
showViewSecretDialog: false,
quickActions: [
{
title: this.$t('accounts.UpdateSecret'),
title: this.$t('accounts.SyncUpdateAccountInfo'),
attrs: {
type: 'primary',
label: this.$t('common.Update')
label: this.$t('accounts.Sync')
},
callbacks: Object.freeze({
click: () => {
vm.visible = true
this.$axios.patch(
`/api/v1/accounts/account-templates/${this.object.id}/sync-related-accounts/`
).then(res => {
openTaskPage(res['task'])
})
}
})
}

View File

@@ -1,93 +0,0 @@
<template>
<Dialog
v-if="iVisible"
:destroy-on-close="true"
:show-cancel="false"
:show-confirm="false"
:title="$tc('accounts.UpdateSecret')"
:visible.sync="iVisible"
width="50%"
>
<AutoDataForm
:form="form"
v-bind="config"
@submit="onSubmit"
/>
</Dialog>
</template>
<script>
import { AutoDataForm, Dialog } from '@/components'
import { templateFieldsMeta } from '../const.js'
import { encryptPassword } from '@/utils/crypto'
export default {
name: 'AccountTemplateChangeSecretDialog',
components: {
Dialog,
AutoDataForm
},
props: {
visible: {
type: Boolean,
default: false
},
object: {
type: Object,
default: null
}
},
data() {
return {
form: this.object,
secretType: this.object?.secret_type?.value,
config: {
hasSaveContinue: false,
url: '/api/v1/accounts/account-templates/',
fields: [
['', [
'secret_type', 'secret', 'ssh_key', 'token',
'access_key', 'passphrase', 'api_key'
]]
],
fieldsMeta: {
...templateFieldsMeta(this),
secret_type: {
disabled: true
}
}
}
}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
methods: {
onSubmit(form) {
const { object, secretType } = this
const currentSecretType = secretType === 'password' ? form?.['secret'] : form?.[secretType]
const params = {
secret: currentSecretType ? encryptPassword(currentSecretType) : '',
is_sync_account: true,
...(secretType === 'ssh_key' && { passphrase: encryptPassword(form?.['passphrase']) })
}
this.$axios.patch(
`/api/v1/accounts/account-templates/${object.id}/`,
params
).then(() => {
this.$message.success(this.$tc('common.updateSuccessMsg'))
}).finally(() => {
this.iVisible = false
})
}
}
}
</script>

View File

@@ -67,7 +67,9 @@ export default {
}
},
reviewers: {
hidden: (item) => item.action !== 'review',
hidden: (formValue) => {
return !['review', 'notice'].includes(formValue.action)
},
rules: [rules.RequiredChange],
el: {
value: [],
@@ -80,7 +82,16 @@ export default {
}
}
},
url: '/api/v1/acls/login-asset-acls/'
url: '/api/v1/acls/login-asset-acls/',
cleanFormValue(value) {
if (!Array.isArray(value.rules.ip_group)) {
value.rules.ip_group = value.rules.ip_group ? value.rules.ip_group.split(',') : []
}
if (!['review', 'notice'].includes(value.action)) {
value.reviewers = []
}
return value
}
}
},
methods: {}

View File

@@ -44,8 +44,15 @@ export default {
width: '160px',
formatter: AmountFormatter,
formatterArgs: {
routeQuery: {
activeTab: 'GroupUser'
route: 'AccountGatherList',
getRoute({ row }) {
return {
name: 'CommandFilterAclList',
query: {
activeTab: 'CommandGroup',
command_filters: row.id
}
}
}
}
}

View File

@@ -11,9 +11,11 @@ export default {
ListTable
},
data() {
const _id = this.$route.query.command_filters
const url = `/api/v1/acls/command-groups/${_id ? `?command_filters=${_id}` : ''}`
return {
tableConfig: {
url: '/api/v1/acls/command-groups/',
url: url,
permissions: {
app: 'acls',
resource: 'commandgroup'

View File

@@ -51,7 +51,7 @@ export default {
},
rules: [Required],
hidden: (formValue) => {
return formValue.action !== 'review'
return !['review', 'notice'].includes(formValue.action)
}
},
rules: {
@@ -86,7 +86,7 @@ export default {
if (!Array.isArray(value.rules.ip_group)) {
value.rules.ip_group = value.rules.ip_group ? value.rules.ip_group.split(',') : []
}
if (value.action !== 'review') {
if (!['review', 'notice'].includes(value.action)) {
value.reviewers = []
}
return value

View File

@@ -17,6 +17,9 @@ export default {
addFieldsMeta: this.getAddFieldsMeta()
}
},
mounted() {
this.url = `${this.url}?platform=${this.$route.query.platform}`
},
methods: {
getAddFields() {
const platform = this.$route.query.type

View File

@@ -131,6 +131,22 @@ export default {
})
}
})
},
{
title: this.$t(`assets.SyncProtocolToAsset`),
attrs: {
type: 'primary',
label: this.$t('accounts.Sync')
},
callbacks: Object.freeze({
click: () => {
const data = { platform_id: this.object.id }
this.$axios.post(
'/api/v1/assets/assets/sync-platform-protocols/', data).then(res => {
this.$message.success(this.$tc('common.updateSuccessMsg'))
})
}
})
}
]
this.quickActions = quickActions

View File

@@ -3,7 +3,7 @@
<div class="head">
<Title :config="config" />
</div>
<LineChart v-bind="lineChartConfig" />
<LineChart v-if="loading" v-bind="lineChartConfig" />
</div>
</template>
@@ -20,6 +20,7 @@ export default {
},
data() {
return {
loading: false,
config: {
title: this.$t('dashboard.UserAssetActivity'),
tip: this.$t('dashboard.UserAssetActivity')
@@ -34,7 +35,11 @@ export default {
}
},
mounted() {
this.getMetricData()
try {
this.getMetricData()
} finally {
this.loading = true
}
},
methods: {
async getMetricData() {

View File

@@ -81,6 +81,7 @@ export default {
DataZTree: 0,
runas: '',
runasPolicy: 'skip',
chdir: '',
command: '',
module: 'shell',
timeout: -1,
@@ -200,6 +201,17 @@ export default {
callback: (option) => {
this.timeout = option
}
},
chdir: {
type: 'input',
name: this.$t('ops.runningPath'),
align: 'left',
value: '',
placeholder: this.$tc('ops.EnterRunningPath'),
tip: this.$tc('ops.RunningPathHelpText'),
callback: (val) => {
this.chdir = val
}
}
},
right: {
@@ -401,6 +413,9 @@ export default {
is_periodic: false,
timeout: this.timeout
}
if (this.chdir) {
data.chdir = this.chdir
}
createJob(data).then(res => {
this.executionInfo.timeCost = 0
this.executionInfo.status = 'running'

View File

@@ -58,7 +58,15 @@ export default {
width: 150,
objects: this.object.assets,
formatter: DeleteActionFormatter,
deleteUrl: `/api/v1/perms/asset-permissions-assets-relations/?assetpermission=${this.object.id}&asset=`
onDelete: function(col, row, cellValue, reload) {
const url = `/api/v1/perms/asset-permissions-assets-relations/?assetpermission=${this.object.id}&asset=${cellValue}`
this.$axios.delete(url).then(res => {
this.$message.success(this.$tc('common.deleteSuccessMsg'))
this.$store.commit('common/reload')
}).catch(error => {
this.$message.error(this.$tc('common.deleteErrorMsg') + ' ' + error)
})
}.bind(this)
}
},
tableAttrs: {
@@ -81,7 +89,7 @@ export default {
hasObjectsId: this.object.assets?.map(i => i.id) || [],
disabled: this.$store.getters.currentOrgIsRoot,
canSelect: (row, index) => {
return this.object.assets.indexOf(row.id) === -1
return (this.object.assets?.map(i => i.id) || []).indexOf(row.id) === -1
},
performAdd: (items, that) => {
const relationUrl = `/api/v1/perms/asset-permissions-assets-relations/`

View File

@@ -1,38 +1,70 @@
<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"
/>
<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'
export default {
components: {
Dialog,
GenericListPage
},
data() {
const ajaxUrl = '/api/v1/authentication/access-keys/'
return {
mfaUrl: '',
mfaDialogVisible: false,
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 +97,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 +107,7 @@ export default {
}
},
headerActions: {
hasSearch: true,
hasRightActions: true,
hasRefresh: true,
hasMoreActions: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
@@ -90,10 +120,8 @@ export default {
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.key = res
this.visible = true
})
}.bind(this)
}
@@ -105,9 +133,29 @@ export default {
getRefsListTable() {
return this.$refs.GenericListTable.$refs.ListTable.$refs.ListTable || {}
}
},
methods: {
}
}
</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

@@ -52,7 +52,7 @@ export default {
name: 'Expired',
title: this.$t('setting.Expire'),
type: 'info',
can: ({ row }) => !row['is_expired'] && this.$hasPerm('authentication.change_connectiontoken'),
can: ({ row }) => !row['is_expired'] && this.$hasPerm('authentication.expire_connectiontoken'),
callback: function({ row }) {
this.$axios.patch(`${ajaxUrl}${row.id}/expire/`,
).then(res => {

View File

@@ -9,8 +9,12 @@
:show-buttons="false"
:title="$tc('auth.AddPassKey')"
:visible.sync="dialogVisible"
width="600px"
>
<AutoDataForm v-bind="form" @submit="onAddConfirm" />
<el-alert v-if="!isLocalUser" :closable="false" class="source-alert" type="error">
{{ $t('profile.PasskeyAddDisableInfo', {source: source.label}) }}
</el-alert>
<AutoDataForm v-else v-bind="form" @submit="onAddConfirm" />
</Dialog>
</div>
</template>
@@ -115,6 +119,12 @@ export default {
computed: {
getRefsListTable() {
return this.$refs.GenericListTable.$refs.ListTable.$refs.ListTable || {}
},
isLocalUser() {
return this.source?.value === 'local'
},
source() {
return this.$store.getters.currentUser?.source
}
},
methods: {
@@ -133,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)
})
},
@@ -149,5 +162,8 @@ export default {
}
</script>
<style scoped>
<style lang='scss' scoped>
.source-alert >>> .el-alert__content {
text-align: center;
}
</style>

View File

@@ -1,12 +1,5 @@
<template>
<Page v-bind="$attrs">
<UserConfirmDialog
v-if="showPasswordDialog"
:url="confirmUrl"
:visible.sync="showPasswordDialog"
@UserConfirmCancel="exit"
@UserConfirmDone="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.location.href = url
} 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)

View File

@@ -7,6 +7,7 @@
:initial="object"
:submit-method="submitMethod"
:url="url"
:clean-form-value="cleanFormValue"
class="password-update"
/>
</IBox>
@@ -61,7 +62,9 @@ export default {
for (const k in this.remoteMeta) {
if (this.remoteMeta.hasOwnProperty(k)) {
fields.push([this.remoteMeta[k].label, [k]])
fieldsMeta[k] = { 'fields': Object.keys(this.remoteMeta[k].children) }
fieldsMeta[k] = { 'fields': Object.keys(this.remoteMeta[k].children).filter(
// todo: remove this when we have a better solution
(key) => key !== 'terminal_theme_name') }
}
}
@@ -83,6 +86,13 @@ export default {
},
submitMethod() {
return 'patch'
},
cleanFormValue(value) {
if (this.category === 'koko') {
// todo: remove this when we have a better solution
delete value['basic']['terminal_theme_name']
}
return value
}
}
}

View File

@@ -26,7 +26,8 @@ export default {
},
{
title: this.$t('setting.Ticket'),
name: 'Ticket'
name: 'Ticket',
hidden: !this.$store.getters.hasValidLicense
},
{
title: this.$t('setting.AppOps'),

View File

@@ -16,11 +16,13 @@
:title="$tc('setting.ImportLicense')"
:visible.sync="dialogLicenseImport"
top="20vh"
width="600px"
@cancel="dialogLicenseImport = false"
@confirm="importLicense"
>
{{ this.$t('setting.LicenseFile') }}
<br>
<div style="padding-bottom: 10px">
{{ this.$t('setting.LicenseFile') }}
</div>
<input type="file" @change="fileChange">
</Dialog>
</div>

View File

@@ -99,18 +99,14 @@ export default {
}
],
cleanFormValue(data) {
const submitValue = {}
submitValue['EMAIL_RECIPIENT'] = data['EMAIL_RECIPIENT']
submitValue['EMAIL_FROM'] = data['EMAIL_FROM']
submitValue['EMAIL_SUBJECT_PREFIX'] = data['EMAIL_SUBJECT_PREFIX']
Object.keys(submitValue).forEach(
Object.keys(data).forEach(
function(key) {
if (submitValue[key] === null) {
delete submitValue[key]
if (data[key] === null) {
delete data[key]
}
}
)
return submitValue
return data
},
submitMethod() {
return 'patch'

View File

@@ -52,7 +52,8 @@ export default {
fieldsMeta: {
'CUSTOM_SMS_API_PARAMS': {
label: this.$t('common.Params'),
component: JsonEditor
component: JsonEditor,
helpText: this.$t('setting.helpTip.CustomParams')
},
SMS_TEST_PHONE: {
component: PhoneInput

View File

@@ -0,0 +1,67 @@
<template>
<div>
<el-alert :title="helpMessage" type="success" :closable="false" />
<BaseSMS ref="baseSms" :config="$data" :title="$tc('setting.Custom')" />
</div>
</template>
<script>
import BaseSMS from './Base.vue'
import { PhoneInput } from '@/components/Form/FormFields'
export default {
name: 'SMSFileCustom',
components: {
BaseSMS
},
data() {
const vm = this
return {
helpMessage: this.$t('setting.helpTip.CustomFile'),
url: `/api/v1/settings/setting/?category=custom_file`,
hasDetailInMsg: false,
visible: false,
moreButtons: [
{
title: this.$t('common.Test'),
loading: false,
callback: function(value, form, btn) {
btn.loading = true
vm.$axios.post(
`/api/v1/settings/sms/custom_file/testing/`,
value
).then(res => {
vm.$message.success(res['msg'])
}).catch((error) => {
vm.$log.error('err occur')
vm.$refs.baseSms.testPerformError(error)
}).finally(() => { btn.loading = false })
}
}
],
fields: [
[
this.$t('common.Other'),
[
'SMS_TEST_PHONE'
]
]
],
fieldsMeta: {
SMS_TEST_PHONE: {
component: PhoneInput
}
},
submitMethod() {
return 'patch'
}
}
},
computed: {},
methods: {}
}
</script>
<style scoped>
</style>

View File

@@ -10,6 +10,7 @@ import SMSAlibaba from './SMSAlibaba.vue'
import SMSTencent from './SMSTencent.vue'
import SMSHuawei from './SMSHuawei.vue'
import SMSCustom from './SMSCustom.vue'
import SMSFileCustom from './SMSFileCustom.vue'
import CMPP2 from './CMPP2.vue'
import { IBox } from '@/components'
@@ -25,12 +26,12 @@ export default {
fields: [
[
this.$t('setting.Basic'), [
'SMS_ENABLED', 'SMS_BACKEND'
'SMS_ENABLED', 'SMS_BACKEND', 'SMS_CODE_LENGTH'
]
],
[
this.$t('setting.SMSProvider'), [
'ALIYUN', 'QCLOUD', 'HUAWEICLOUD', 'CMPP2', 'SMSCustom'
'ALIYUN', 'QCLOUD', 'HUAWEICLOUD', 'CMPP2', 'SMSCustom', 'SMSFileCustom'
]
]
],
@@ -69,6 +70,13 @@ export default {
hidden: (form) => {
return form['SMS_BACKEND'] !== 'custom'
}
},
SMSFileCustom: {
label: this.$t('setting.Custom'),
component: SMSFileCustom,
hidden: (form) => {
return form['SMS_BACKEND'] !== 'custom_file'
}
}
},
submitMethod() {

View File

@@ -4,7 +4,7 @@
<script>
import { GenericCreateUpdatePage } from '@/layout/components'
import { Text } from '@/components/Form/FormFields'
import { TextReadonly } from '@/components/Form/FormFields'
export default {
components: {
@@ -33,9 +33,10 @@ export default {
readonly: true
},
permissions: {
component: Text,
component: TextReadonly,
el: {
text: this.$t('users.HelpText.addRolePermissions')
text: this.$t('users.HelpText.addRolePermissions'),
bolder: false
}
}
}

View File

@@ -181,7 +181,7 @@ export default {
key: this.$t('users.Phone'),
formatter: () => {
const phoneObj = this.object.phone
return <div>{phoneObj?.code}{phoneObj?.phone}</div>
return <div>{phoneObj?.code} {phoneObj?.phone}</div>
}
},
'wecom_id', 'dingtalk_id', 'feishu_id',