feat(ui): add access control

This commit is contained in:
LH_R
2025-07-16 16:37:09 +08:00
parent 0a8855df75
commit d8387323dd
81 changed files with 5112 additions and 1408 deletions

View File

@@ -39,6 +39,7 @@
"lodash.pick": "^4.4.0",
"md5": "^2.2.1",
"moment": "^2.24.0",
"moment-timezone": "^0.6.0",
"nprogress": "^0.2.0",
"snabbdom": "^3.5.1",
"sortablejs": "1.9.0",

View File

@@ -1,32 +1,32 @@
import { axios } from '@/utils/request'
export function getAccountList(params) {
return axios({
url: '/oneterm/v1/account',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/account',
method: 'get',
params
})
}
export function postAccount(data) {
return axios({
url: '/oneterm/v1/account',
method: 'post',
data
})
return axios({
url: '/oneterm/v1/account',
method: 'post',
data
})
}
export function putAccountById(id, data) {
return axios({
url: `/oneterm/v1/account/${id}`,
method: 'put',
data
})
return axios({
url: `/oneterm/v1/account/${id}`,
method: 'put',
data
})
}
export function deleteAccountById(id) {
return axios({
url: `/oneterm/v1/account/${id}`,
method: 'delete',
})
return axios({
url: `/oneterm/v1/account/${id}`,
method: 'delete',
})
}

View File

@@ -1,32 +1,40 @@
import { axios } from '@/utils/request'
export function getAssetList(params) {
return axios({
url: '/oneterm/v1/asset',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/asset',
method: 'get',
params
})
}
export function postAsset(data) {
return axios({
url: '/oneterm/v1/asset',
method: 'post',
data
})
return axios({
url: '/oneterm/v1/asset',
method: 'post',
data
})
}
export function putAssetById(id, data) {
return axios({
url: `/oneterm/v1/asset/${id}`,
method: 'put',
data
})
return axios({
url: `/oneterm/v1/asset/${id}`,
method: 'put',
data
})
}
export function deleteAssetById(id) {
return axios({
url: `/oneterm/v1/asset/${id}`,
method: 'delete',
})
return axios({
url: `/oneterm/v1/asset/${id}`,
method: 'delete',
})
}
export function getAssetPermissions(id, params) {
return axios({
url: `/oneterm/v1/asset/${id}/permissions`,
method: 'get',
params
})
}

View File

@@ -1,17 +1,17 @@
import { axios } from '@/utils/request'
export function getAuth(params) {
return axios({
url: '/oneterm/v1/authorization',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/authorization',
method: 'get',
params
})
}
export function postAuth(data) {
return axios({
url: '/oneterm/v1/authorization',
method: 'post',
data
})
return axios({
url: '/oneterm/v1/authorization',
method: 'post',
data
})
}

View File

@@ -0,0 +1,32 @@
import { axios } from '@/utils/request'
export function getAuthList(params) {
return axios({
url: '/oneterm/v1/authorization_v2',
method: 'get',
params
})
}
export function postAuth(data) {
return axios({
url: '/oneterm/v1/authorization_v2',
method: 'post',
data
})
}
export function putAuthById(id, data) {
return axios({
url: `/oneterm/v1/authorization_v2/${id}`,
method: 'put',
data
})
}
export function deleteAuthById(id) {
return axios({
url: `/oneterm/v1/authorization_v2/${id}`,
method: 'delete',
})
}

View File

@@ -1,32 +1,32 @@
import { axios } from '@/utils/request'
export function getCommandList(params) {
return axios({
url: '/oneterm/v1/command',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/command',
method: 'get',
params
})
}
export function postCommand(data) {
return axios({
url: '/oneterm/v1/command',
method: 'post',
data
})
return axios({
url: '/oneterm/v1/command',
method: 'post',
data
})
}
export function putCommandById(id, data) {
return axios({
url: `/oneterm/v1/command/${id}`,
method: 'put',
data
})
return axios({
url: `/oneterm/v1/command/${id}`,
method: 'put',
data
})
}
export function deleteCommandById(id) {
return axios({
url: `/oneterm/v1/command/${id}`,
method: 'delete',
})
return axios({
url: `/oneterm/v1/command/${id}`,
method: 'delete',
})
}

View File

@@ -0,0 +1,32 @@
import { axios } from '@/utils/request'
export function getCommandTemplateList(params) {
return axios({
url: '/oneterm/v1/command_template',
method: 'get',
params
})
}
export function postCommandTemplate(data) {
return axios({
url: '/oneterm/v1/command_template',
method: 'post',
data
})
}
export function putCommandTemplateById(id, data) {
return axios({
url: `/oneterm/v1/command_template/${id}`,
method: 'put',
data
})
}
export function deleteCommandTemplateById(id) {
return axios({
url: `/oneterm/v1/command_template/${id}`,
method: 'delete',
})
}

View File

@@ -1,17 +1,17 @@
import { axios } from '@/utils/request'
export function getConfig(params) {
return axios({
url: '/oneterm/v1/config',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/config',
method: 'get',
params
})
}
export function postConfig(data) {
return axios({
url: '/oneterm/v1/config',
method: 'post',
data
})
return axios({
url: '/oneterm/v1/config',
method: 'post',
data
})
}

View File

@@ -1,21 +1,21 @@
import { axios } from '@/utils/request'
export function closeConnect(session_id) {
return axios({
url: `/oneterm/v1/connect/close/${session_id}`,
method: 'post',
})
return axios({
url: `/oneterm/v1/connect/close/${session_id}`,
method: 'post',
})
}
export function postConnectIsRight(asset_id, account_id, protocol, query = null) {
let url = `/oneterm/v1/connect/${asset_id}/${account_id}/${protocol}`
if (query) {
url = `${url}?${query}`
}
return axios({
url,
method: 'post',
})
let url = `/oneterm/v1/connect/${asset_id}/${account_id}/${protocol}`
if (query) {
url = `${url}?${query}`
}
return axios({
url,
method: 'post',
})
}
export function postShareLink(data) {

View File

@@ -1,32 +1,32 @@
import { axios } from '@/utils/request'
export function getGatewayList(params) {
return axios({
url: '/oneterm/v1/gateway',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/gateway',
method: 'get',
params
})
}
export function postGateway(data) {
return axios({
url: '/oneterm/v1/gateway',
method: 'post',
data
})
return axios({
url: '/oneterm/v1/gateway',
method: 'post',
data
})
}
export function putGatewayById(id, data) {
return axios({
url: `/oneterm/v1/gateway/${id}`,
method: 'put',
data
})
return axios({
url: `/oneterm/v1/gateway/${id}`,
method: 'put',
data
})
}
export function deleteGatewayById(id) {
return axios({
url: `/oneterm/v1/gateway/${id}`,
method: 'delete',
})
return axios({
url: `/oneterm/v1/gateway/${id}`,
method: 'delete',
})
}

View File

@@ -1,9 +1,9 @@
import { axios } from '@/utils/request'
export function getLoginLogList(params) {
return axios({
url: `/v1/acl/audit_log/login`,
method: 'GET',
params: params
})
return axios({
url: `/v1/acl/audit_log/login`,
method: 'GET',
params: params
})
}

View File

@@ -1,39 +1,39 @@
import { axios } from '@/utils/request'
export function getNodeList(params) {
return axios({
url: '/oneterm/v1/node',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/node',
method: 'get',
params
})
}
export function getNodeById(id) {
return axios({
url: `/oneterm/v1/node?id=${id}`,
method: 'get',
})
return axios({
url: `/oneterm/v1/node?id=${id}`,
method: 'get',
})
}
export function postNode(data) {
return axios({
url: '/oneterm/v1/node',
method: 'post',
data
})
return axios({
url: '/oneterm/v1/node',
method: 'post',
data
})
}
export function putNodeById(id, data) {
return axios({
url: `/oneterm/v1/node/${id}`,
method: 'put',
data
})
return axios({
url: `/oneterm/v1/node/${id}`,
method: 'put',
data
})
}
export function deleteNodeById(id) {
return axios({
url: `/oneterm/v1/node/${id}`,
method: 'delete',
})
return axios({
url: `/oneterm/v1/node/${id}`,
method: 'delete',
})
}

View File

@@ -1,17 +1,17 @@
import { axios } from '@/utils/request'
export function getOperationLogList(params) {
return axios({
url: `/oneterm/v1/history`,
method: 'GET',
params: params
})
return axios({
url: `/oneterm/v1/history`,
method: 'GET',
params: params
})
}
export function getResourceType(params) {
return axios({
url: `/oneterm/v1/history/type/mapping`,
method: 'get',
params: params
})
return axios({
url: `/oneterm/v1/history/type/mapping`,
method: 'get',
params: params
})
}

View File

@@ -1,17 +1,17 @@
import { axios } from '@/utils/request'
export function getCITypeGroups(params) {
return axios({
url: `/v0.1/ci_types/groups`,
method: 'GET',
params: params
})
return axios({
url: `/v0.1/ci_types/groups`,
method: 'GET',
params: params
})
}
export function getCITypeAttributesById(CITypeId, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'get',
params: parameter
})
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'get',
params: parameter
})
}

View File

@@ -1,32 +1,32 @@
import { axios } from '@/utils/request'
export function getPublicKeyList(params) {
return axios({
url: `/oneterm/v1/public_key`,
method: 'GET',
params: params
})
return axios({
url: `/oneterm/v1/public_key`,
method: 'GET',
params: params
})
}
export function addPublicKey(data) {
return axios({
url: `/oneterm/v1/public_key`,
method: 'POST',
data: data
})
return axios({
url: `/oneterm/v1/public_key`,
method: 'POST',
data: data
})
}
export function putPublicKeyById(id, data) {
return axios({
url: `/oneterm/v1/public_key/${id}`,
method: 'put',
data
})
return axios({
url: `/oneterm/v1/public_key/${id}`,
method: 'put',
data
})
}
export function deletePublicKeyById(id) {
return axios({
url: `/oneterm/v1/public_key/${id}`,
method: 'delete',
})
return axios({
url: `/oneterm/v1/public_key/${id}`,
method: 'delete',
})
}

View File

@@ -1,17 +1,17 @@
import { axios } from '@/utils/request'
export function getSessionList(params) {
return axios({
url: '/oneterm/v1/session',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/session',
method: 'get',
params
})
}
export function getSessionCmdList(session_id, params) {
return axios({
url: `/oneterm/v1/session/${session_id}/cmd`,
method: 'get',
params
})
return axios({
url: `/oneterm/v1/session/${session_id}/cmd`,
method: 'get',
params
})
}

View File

@@ -1,49 +1,49 @@
import { axios } from '@/utils/request'
export function getCountStat(params) {
return axios({
url: '/oneterm/v1/stat/count',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/stat/count',
method: 'get',
params
})
}
export function getAssetTypeStat(params) {
return axios({
url: '/oneterm/v1/stat/assettype',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/stat/assettype',
method: 'get',
params
})
}
export function getAssetStat(params) {
return axios({
url: '/oneterm/v1/stat/asset',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/stat/asset',
method: 'get',
params
})
}
export function getAccountStat(params) {
return axios({
url: '/oneterm/v1/stat/account',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/stat/account',
method: 'get',
params
})
}
export function getOfUserStat(params) {
return axios({
url: '/oneterm/v1/stat/count/ofuser',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/stat/count/ofuser',
method: 'get',
params
})
}
export function getRankOfUserStat(params) {
return axios({
url: '/oneterm/v1/stat/rank/ofuser',
method: 'get',
params
})
return axios({
url: '/oneterm/v1/stat/rank/ofuser',
method: 'get',
params
})
}

View File

@@ -0,0 +1,32 @@
import { axios } from '@/utils/request'
export function getTimeTemplateList(params) {
return axios({
url: '/oneterm/v1/time_template',
method: 'get',
params
})
}
export function postTimeTemplate(data) {
return axios({
url: '/oneterm/v1/time_template',
method: 'post',
data
})
}
export function putTimeTemplateById(id, data) {
return axios({
url: `/oneterm/v1/time_template/${id}`,
method: 'put',
data
})
}
export function deleteTimeTemplateById(id) {
return axios({
url: `/oneterm/v1/time_template/${id}`,
method: 'delete',
})
}

View File

@@ -6,7 +6,7 @@
<table :class="{ 'c-min-table': colspan < 2 }" class="c-weektime-table">
<thead class="c-weektime-head">
<tr>
<th rowspan="8" class="week-td">{{ $t('oneterm.assetList.weektime') }}</th>
<th rowspan="8" class="week-td">{{ $t('oneterm.timeTemplate.weektime') }}</th>
<th :colspan="12 * colspan">00:00 - 12:00</th>
<th :colspan="12 * colspan">12:00 - 24:00</th>
</tr>
@@ -16,7 +16,7 @@
</thead>
<tbody class="c-weektime-body">
<tr v-for="t in data" :key="t.row">
<td>{{ $t(t.week) }}</td>
<td>{{ $t(`oneterm.timeTemplate.day${t.day}`) }}</td>
<td
v-for="n in t.child"
:key="`${n.row}-${n.col}`"
@@ -32,15 +32,15 @@
<tr>
<td colspan="49" class="c-weektime-preview">
<div class="g-clearfix c-weektime-con">
<span class="g-pull-left">{{
selectState ? $t(`oneterm.assetList.selectedTime`) : $t(`oneterm.assetList.drag`)
}}</span>
<span class="g-pull-left">
{{ selectState ? $t('oneterm.timeTemplate.selectedTime') : $t('oneterm.timeTemplate.drag') }}
</span>
<a @click.prevent="$emit('onClear')" class="g-pull-right">{{ $t(`clear`) }}</a>
</div>
<div v-if="selectState" class="c-weektime-time">
<div v-for="t in selectValue" :key="t.id">
<p v-if="t.value && t.value.length">
<span class="g-tip-text">{{ $t(t.week) }}</span>
<span class="g-tip-text">{{ $t(`oneterm.timeTemplate.day${t.day}`) }}</span>
<span>{{ formatSelectValue(t.value) }}</span>
</p>
</div>
@@ -178,7 +178,7 @@ export default {
const _selectValue = this.data.map((item) => {
return {
id: item.row,
week: item.week,
day: item.day,
value: item.child.filter((c) => c.check).map((c) => c.value),
}
})

View File

@@ -1,70 +1,39 @@
const formatDate = (date, fmt) => {
const o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds(),
'q+': Math.floor((date.getMonth() + 3) / 3),
S: date.getMilliseconds()
}
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
(date.getFullYear() + '').substr(4 - RegExp.$1.length)
)
}
for (const k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
)
}
}
return fmt
// Format a Date object to 'HH:mm'
const formatTime = (date) => {
const h = String(date.getHours()).padStart(2, '0')
const m = String(date.getMinutes()).padStart(2, '0')
return `${h}:${m}`
}
const createArr = (len) => {
return Array.from(Array(len)).map((ret, id) => id)
// Generate half-hour intervals for one day, last end is 23:59
const generateDayIntervals = (day, row, total = 48) => {
const intervals = []
for (let i = 0; i < total; i++) {
const startMinutes = i * 30
const endMinutes = (i + 1) * 30
const begin = formatTime(new Date(2000, 0, 1, 0, startMinutes))
// Last interval ends at 23:59, others at next half hour
const end =
i === total - 1
? '23:59'
: formatTime(new Date(2000, 0, 1, 0, endMinutes))
intervals.push({
day,
value: `${begin}~${end}`,
begin,
end,
row,
col: i
})
}
return intervals
}
const formatWeektime = (col) => {
const timestamp = 1542384000000 // '2018-11-17 00:00:00'
const beginstamp = timestamp + col * 1800000 // col * 30 * 60 * 1000
const endstamp = beginstamp + 1800000
// Generate week data: 7 days, each with 48 half-hour intervals
const weekTimeData = Array.from({ length: 7 }, (_, i) => ({
day: i + 1,
row: i,
child: generateDayIntervals(i + 1, i, 48)
}))
const begin = formatDate(new Date(beginstamp), 'hh:mm')
const end = formatDate(new Date(endstamp), 'hh:mm')
return `${begin}~${end}`
}
const data = [
'星期一',
'星期二',
'星期三',
'星期四',
'星期五',
'星期六',
'星期日'
].map((ret, index) => {
const children = (ret, row, max) => {
return createArr(max).map((t, col) => {
return {
week: ret,
value: formatWeektime(col),
begin: formatWeektime(col).split('~')[0],
end: formatWeektime(col).split('~')[1],
row,
col
}
})
}
return {
week: ret,
row: index,
child: children(ret, index, 48)
}
})
export default data
export default weekTimeData

View File

@@ -0,0 +1,93 @@
<template>
<div class="enabled-status">
<div class="enabled-status-display">
<span
class="enabled-status-dot"
:style="{
backgroundColor: statusColor + '22',
'--innerBackgroundColor': statusColor
}"
></span>
<span>
{{ $t(statusText) }}
</span>
</div>
<a-popconfirm
:title="$t('oneterm.confirmEnable')"
@confirm="$emit('change', !status)"
>
<a-switch
:checked="status"
:checked-children="$t('oneterm.enabled')"
:un-checked-children="$t('oneterm.disabled')"
/>
</a-popconfirm>
</div>
</template>
<script>
export default {
name: 'EnabledStatus',
props: {
status: {
type: Boolean,
default: true
}
},
computed: {
statusColor() {
return this.status ? '#52c41a' : '#A5A9BC'
},
statusText() {
return this.status ? 'oneterm.enabled' : 'oneterm.disabled'
}
}
}
</script>
<style lang="less" scoped>
.enabled-status {
width: min-content;
cursor: pointer;
/deep/ .ant-switch {
display: none;
}
&-display {
display: flex;
align-items: center;
}
&-dot {
width: 14px;
height: 14px;
border-radius: 50%;
position: relative;
margin-right: 4px;
&::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 8px;
border-radius: 50%;
margin-top: -4px;
margin-left: -4px;
background-color: var(--innerBackgroundColor);
}
}
&:hover {
.enabled-status-display {
display: none;
}
/deep/ .ant-switch {
display: inline-block;
}
}
}
</style>

View File

@@ -2,42 +2,33 @@
<a-modal
:visible="visible"
:width="800"
:footer="null"
@cancel="handleCancel"
@ok="handleOk"
>
<template v-if="showACLConfig">
<div class="auth-node-title">
{{ $t(grantTitle) }}
</div>
<ACLTable
:tableData="aclTableData"
:resourceId="resourceId"
/>
<a-space>
<span class="grant-button" @click="openGrantUserModal('depart')">{{ $t('oneterm.assetList.grantUserOrDep') }}</span>
<span class="grant-button" @click="openGrantUserModal('role')">{{ $t('oneterm.assetList.grantRole') }}</span>
</a-space>
</template>
<div class="auth-node-title">
{{ $t('oneterm.assetList.grantLogin') }}
</div>
<EmployeeTreeSelect
v-model="rids"
multiple
:idType="2"
departmentKey="acl_rid"
employeeKey="acl_rid"
:placeholder="`${$t(`placeholder2`)}`"
class="custom-treeselect custom-treeselect-white"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-multiple-lineHeight': '18px',
}"
:limit="1"
:otherOptions="visualRoleList"
/>
<a-tabs v-model="tabKey">
<a-tab-pane key="accessPermission" :tab="$t('oneterm.assetList.accessPermission')">
<PermissionForm
ref="permissionFormRef"
:dataType="dataType"
:ids="ids"
@cancel="handleCancel"
/>
</a-tab-pane>
<a-tab-pane
v-if="showACLConfig"
key="operationPermissions"
:tab="$t(grantTitle)"
>
<ACLTable
:tableData="aclTableData"
:resourceId="resourceId"
/>
<a-space>
<span class="grant-button" @click="openGrantUserModal('depart')">{{ $t('oneterm.assetList.grantUserOrDep') }}</span>
<span class="grant-button" @click="openGrantUserModal('role')">{{ $t('oneterm.assetList.grantRole') }}</span>
</a-space>
</a-tab-pane>
</a-tabs>
<GrantUserModal
ref="grantUserModalRef"
@@ -49,30 +40,28 @@
<script>
import _ from 'lodash'
import { mapState } from 'vuex'
import { getAuth, postAuth } from '@/modules/oneterm/api/authorization.js'
import { getResourcePerms } from '@/modules/acl/api/permission'
import { searchRole } from '@/modules/acl/api/role'
import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue'
import PermissionForm from './permissionForm.vue'
import ACLTable from './aclTable.vue'
import GrantUserModal from './grantUserModal.vue'
export default {
name: 'GrantModal',
components: {
EmployeeTreeSelect,
PermissionForm,
ACLTable,
GrantUserModal
},
data() {
return {
tabKey: 'accessPermission',
visible: false,
rids: [], // 登录权限
resourceId: '', // acl resource id
aclTableData: [], // acl data
ids: [], // id 列表
ids: [], // id list
dataType: 'node',
showACLConfig: true, // 是否展示acl 配置
visualRoleList: [], // 虚拟角色
showACLConfig: true,
}
},
computed: {
@@ -83,11 +72,11 @@ export default {
grantTitle() {
switch (this.dataType) {
case 'node':
return 'oneterm.assetList.grantCatalog'
return 'oneterm.assetList.folderOperationPermissions'
case 'asset':
return 'oneterm.assetList.grantAsset'
return 'oneterm.assetList.assetOperationPermissions'
case 'account':
return 'oneterm.assetList.grantAccount'
return 'oneterm.assetList.accountOperationPermissions'
default:
return ''
}
@@ -99,12 +88,11 @@ export default {
ids,
resourceId,
}) {
this.tabKey = 'accessPermission'
this.showACLConfig = type === 'node' || (type !== 'node' && ids.length === 1)
this.ids = ids
this.dataType = type
this.resourceId = resourceId || ''
this.loadRoles()
this.resourceId = resourceId ?? ''
let aclTableData = []
if (this.showACLConfig) {
@@ -134,79 +122,15 @@ export default {
}
}
this.aclTableData = aclTableData
const getAuthParams = {
page_index: 1,
page_size: 9999,
}
switch (type) {
case 'node':
getAuthParams.node_id = ids[0]
break
case 'asset':
if (ids.length === 1) {
getAuthParams.asset_id = ids[0]
}
break
case 'account':
if (ids.length === 1) {
getAuthParams.account_id = ids[0]
}
break
default:
break
}
let rids = []
if (
getAuthParams.node_id ||
getAuthParams.asset_id ||
getAuthParams.account_id
) {
const res = await getAuth(getAuthParams)
let newRids = []
if (res?.data?.list?.length) {
res.data.list.forEach((item) => {
newRids.push(...(item?.rids || []))
})
}
newRids = _.uniq(newRids)
if (newRids?.length) {
rids = newRids.map((id) => `employee-${id}`)
}
}
this.rids = rids
this.visible = true
},
async loadRoles() {
const res = await searchRole({ page_size: 9999, page: 1, app_id: 'oneterm', user_role: 0, user_only: 0, is_all: true })
const visualRoleList = []
const roleList = (res?.roles || []).filter((item) => !/_virtual$/.test(item.name))
if (roleList.length) {
visualRoleList.push({
acl_rid: -100,
department_name: this.$t('acl.visualRole'),
sub_departments: [],
employees: roleList.map((item) => {
return {
nickname: item.name,
acl_rid: item.id
}
})
})
}
this.$set(this, 'visualRoleList', visualRoleList)
},
handleCancel() {
this.rids = []
this.visible = false
if (this.$refs.permissionFormRef) {
this.$refs.permissionFormRef.resetFields()
}
this.resourceId = ''
this.aclTableData = []
this.ids = []
@@ -214,52 +138,6 @@ export default {
this.showACLConfig = true
},
handleOk() {
const rids = this.rids.map((rid) => {
return Number(rid?.split?.('-')?.[1])
})
switch (this.dataType) {
case 'node':
postAuth({
rids,
node_id: this.ids[0]
}).then(() => {
this.$message.success(this.$t('updateSuccess'))
this.handleCancel()
})
break
case 'asset':
Promise.all(
this.ids.map((id) => {
return postAuth({
rids,
asset_id: id
})
})
).then(() => {
this.$message.success(this.$t('updateSuccess'))
this.handleCancel()
})
break
case 'account':
Promise.all(
this.ids.map((id) => {
return postAuth({
rids,
account_id: id
})
})
).then(() => {
this.$message.success(this.$t('updateSuccess'))
this.handleCancel()
})
break
default:
break
}
},
openGrantUserModal(type) {
this.$refs.grantUserModalRef.open(type)
},

View File

@@ -0,0 +1,180 @@
<template>
<a-form-model
ref="permissionFormRef"
:model="form"
:rules="rules"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 16 }"
>
<a-form-model-item :label="$t('oneterm.assetList.grantRole')" prop="rids">
<EmployeeTreeSelect
v-model="form.rids"
multiple
:idType="2"
departmentKey="acl_rid"
employeeKey="acl_rid"
:placeholder="$t('placeholder2')"
class="custom-treeselect custom-treeselect-white"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-multiple-lineHeight': '18px',
}"
:limit="1"
:otherOptions="visualRoleList"
/>
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.assetList.operationPermissions')" prop="permissions">
<PermissionCheckbox
:value="form.permissions"
@change="(key, checked) => form.permissions[key] = checked"
/>
</a-form-model-item>
<div class="permission-form-operation">
<a-button @click="handleCancel">
{{ $t('cancel') }}
</a-button>
<a-button
type="primary"
@click="handleOk"
>
{{ $t('confirm') }}
</a-button>
</div>
</a-form-model>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import { postAuth } from '@/modules/oneterm/api/authorizationV2.js'
import { searchRole } from '@/modules/acl/api/role'
import { getConfig } from '@/modules/oneterm/api/config'
import { PERMISSION_TYPE } from '@/modules/oneterm/views/systemSettings/accessControl/constants.js'
import { TARGET_SELECT_TYPE } from '@/modules/oneterm/views/access/auth/constants.js'
import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue'
import PermissionCheckbox from '@/modules/oneterm/views/systemSettings/accessControl/permissionCheckbox.vue'
const DEFAULT_PERMISSIONS = Object.values(PERMISSION_TYPE).reduce((config, key) => {
config[key] = false
return config
}, {})
export default {
name: 'PermissionForm',
components: {
EmployeeTreeSelect,
PermissionCheckbox
},
props: {
dataType: {
type: String,
default: 'node'
},
ids: {
type: Array,
default: () => []
}
},
data() {
return {
form: {
rids: [],
permissions: { ...DEFAULT_PERMISSIONS }
},
rules: {
rids: [{ required: true, message: this.$t('placeholder2') }],
},
visualRoleList: []
}
},
mounted() {
this.initDefaultPermissions()
this.loadRoles()
},
methods: {
initDefaultPermissions() {
getConfig({
info: true
}).then((res) => {
const default_permissions = res?.data?.default_permissions
Object.keys(DEFAULT_PERMISSIONS).forEach((key) => {
DEFAULT_PERMISSIONS[key] = default_permissions?.[key] ?? DEFAULT_PERMISSIONS[key]
})
this.form.permissions = { ...DEFAULT_PERMISSIONS }
})
},
async loadRoles() {
const res = await searchRole({ page_size: 9999, page: 1, app_id: 'oneterm', user_role: 0, user_only: 0, is_all: true })
const visualRoleList = []
const roleList = (res?.roles || []).filter((item) => !/_virtual$/.test(item.name))
if (roleList.length) {
visualRoleList.push({
acl_rid: -100,
department_name: this.$t('acl.visualRole'),
sub_departments: [],
employees: roleList.map((item) => {
return {
nickname: item.name,
acl_rid: item.id
}
})
})
}
this.visualRoleList = visualRoleList
},
handleCancel() {
this.resetFields()
this.$emit('cancel')
},
resetFields() {
this.form = {
rids: [],
permissions: { ...DEFAULT_PERMISSIONS }
}
this.$refs.permissionFormRef.resetFields()
},
handleOk() {
this.$refs.permissionFormRef.validate(async (valid) => {
if (!valid) return
const params = this.handleParams()
postAuth(params).then(() => {
this.$message.success(this.$t('updateSuccess'))
this.handleCancel()
})
})
},
handleParams() {
const rids = this.form.rids.map((rid) => {
return Number(rid?.split?.('-')?.[1])
})
const params = {
name: `${this.dataType}-${this.ids.join(',')}-${uuidv4()}`,
rids,
permissions: this.form.permissions
}
;['node', 'account', 'asset'].forEach((key) => {
params[`${key}_selector`] = {
type: TARGET_SELECT_TYPE.ID,
values: key === this.dataType ? this.ids.map((id) => String(id)) : []
}
})
return params
},
}
}
</script>
<style lang="less" scoped>
.permission-form-operation {
display: flex;
justify-content: flex-end;
column-gap: 8px;
}
</style>

View File

@@ -2,7 +2,7 @@ import genOnetermRoutes from './router/index'
import onetermStore from './store'
export default {
name: 'oneterm',
route: genOnetermRoutes,
store: onetermStore
name: 'oneterm',
route: genOnetermRoutes,
store: onetermStore
}

View File

@@ -16,7 +16,7 @@ const oneterm_en = {
accessRestrictions: 'Access Restrictions',
comment: 'Comment',
node: 'Node',
catalog: 'Catalog',
folder: 'Folder',
cmdbType: 'CMDB Type',
fieldMap: 'Field Map',
field: 'Field',
@@ -33,13 +33,25 @@ const oneterm_en = {
editPublicKey: 'Edit Public Key',
assetCount: 'Asset Count',
macAddress: 'Mac Address',
enabled: 'Enabled',
disabled: 'Disabled',
isEnable: 'Is Enable',
confirmEnable: 'Confirm switch enabled?',
switching: 'Switching',
switchingTip: 'Switching, total of {total}, {successNum} succeeded, {errorNum} failed',
permissions: 'Permissions',
menu: {
oneterm: 'OneTerm',
workStation: 'Work Station',
resourceControl: 'Resource Control',
basicResource: 'Basic Resource',
assetManagement: 'Asset Management',
assets: 'Assets',
gateways: 'Gateways',
accounts: 'Accounts',
gatewayManagement: 'Gateway Management',
accountManagement: 'Account Management',
accessControl: 'Access Control',
accessAuthorization: 'Access Authorization',
commandFilter: 'Command Filter',
accessTime: 'Access Time',
security: 'Security',
sessionAuditing: 'Session Auditing',
onlineSession: 'Online Session',
@@ -81,7 +93,8 @@ const oneterm_en = {
pageUnloadMessage: 'Are you sure you want to leave the page?',
batchExecution: 'Batch Execution',
chooseAssets: 'Choose Assets',
batchExecutionPlaceholder: 'Please input the command, press Enter to execute'
batchExecutionPlaceholder: 'Please input the command, press Enter to execute',
assetShare: 'Asset Share'
},
sessionTable: {
target: 'Target',
@@ -105,12 +118,12 @@ const oneterm_en = {
assetList: {
grantUser: 'Granted User',
assetTree: 'Asset Tree',
createCatalog: 'Create Catalog',
editCatalog: 'Edit Catalog',
deleteCatalog: 'Delete Catalog',
grantCatalog: 'Grant Catalog',
createFolder: 'Create Folder',
editFolder: 'Edit Folder',
deleteFolder: 'Delete Folder',
grantFolder: 'Grant Folder',
ip: 'IP',
catalogName: 'Catalog Name',
folderName: 'Folder Name',
connectable: 'Connectable',
connected: 'Connected',
error: 'Error',
@@ -125,9 +138,6 @@ const oneterm_en = {
commandIntercept: 'Command Intercept',
allowAccess: 'Allow Access',
prohibitAccess: 'Prohibit Access',
weektime: 'week/time',
selectedTime: 'Selected Time',
drag: 'Drag the mouse to select time',
assetList: 'Asset List',
createAsset: 'Create Asset',
editAsset: 'Edit Asset',
@@ -161,10 +171,15 @@ const oneterm_en = {
grantLogin: 'Grant Login',
grantUserOrDep: 'Grant User/Department',
grantRole: 'Grant Role',
createSubCatalog: 'Create Sub Catalog',
createSubFolder: 'Create Sub Folder',
assetSearchTip: 'Search in Asset',
basic: 'Basic',
database: 'Database'
database: 'Database',
folderOperationPermissions: 'Folder Operation Permissions',
assetOperationPermissions: 'Asset Operation Permissions',
accountOperationPermissions: 'Account Operation Permissions',
accessPermission: 'Access Permission',
operationPermissions: 'Operation Permissions'
},
log: {
time: 'Time',
@@ -193,26 +208,51 @@ const oneterm_en = {
clipboard: 'Clipboard'
},
systemSettings: {
commandIntercept: 'Command Intercept',
terminalControl: 'Terminal Control',
accessControl: 'Access Control',
quickCommand: 'Quick Command',
terminalDisplay: 'Terminal Display',
publicKey: 'Public Key',
publicKeyTip: 'Password-free authentication for command terminals to log into the SSH service of the bastion',
storageConfig: 'Storage Config'
},
commandIntercept: {
enable: 'Enable',
commandFilter: {
name: 'Command Filter',
commandManagement: 'Command Management',
commandTemplateManagement: 'Command Template Management',
regexp: 'RegExp',
createCommand: 'Create Command',
editCommand: 'Edit Command',
category: 'Category',
securityRelated: 'Security Related',
systemOperations: 'System Operations',
databaseOperations: 'Database Operations',
networkOperations: 'Network Operations',
fileOperations: 'File Operations',
developmentRelated: 'DevelopmentRelated',
riskLevel: 'Risk Level',
safe: 'Safe',
warning: 'Warning',
dangerous: 'Dangerous',
criticalDanger: 'Critical Danger',
commandCount: 'Command Count',
createCommandTemplate: 'Create Command Template',
editCommandTemplate: 'Edit Command Template',
selectCommand: 'Select Command',
unselectCommand: 'Unselect',
selectedCommand: 'Selected',
},
terminalControl: {
accessControl: {
timeout: 'Maximum session free time',
allowCopy: 'Allow Copy',
allowPaste: 'Allow Paste',
copyTip: 'Copy: The local device can get the contents of the clipboard text in the remote desktop in real time.',
pasteTip: 'Paste: Remote Desktop can get the content of the text currently copied by the local device via the clipboard.'
copyTip: 'The local device can get the contents of the clipboard text in the remote desktop in real time.',
pasteTip: 'Remote Desktop can get the content of the text currently copied by the local device via the clipboard.',
permissionConfig: 'Permission Config',
permissionConfigTip: 'Uploading and downloading are for SSH and RDP protocols, copying and pasting are for RDP protocols',
connect: 'Connect',
upload: 'Upload',
download: 'Download',
copy: 'Copy',
paste: 'Paste',
share: 'Share'
},
terminalDisplay: {
fontSetting: 'Font Setting',
@@ -268,7 +308,6 @@ const oneterm_en = {
createConfig: 'Create Config',
editConfig: 'Edit Config',
local: 'Local',
isEnable: 'Is Enable',
retentionDays: 'Retention Days',
archiveDays: 'Archive Days',
isCleanupEnabled: 'Is Cleanup Enabled',
@@ -302,8 +341,76 @@ const oneterm_en = {
basicConfig: 'Basic Config',
advancedConfig: 'Advanced Config',
advancedConfigTip: 'Configure the detailed parameters and strategies for storage',
confirmEnable: 'Confirm toggle enable?',
confirmPrimaryStorage: 'Confirm switching primary storage'
},
timeTemplate: {
createTimeTemplate: 'Create Time Template',
editTimeTemplate: 'Edit Time Template',
timeZone: 'Time Zone',
category: 'Category',
workTime: 'Work Time',
dutyTime: 'Duty Time',
maintenanceTime: 'Maintenance Time',
emergencyResponse: 'Emergency Response',
allTime: '24/7 Access',
timeRange: 'Time Range',
weektime: 'week/time',
selectedTime: 'Selected Time',
drag: 'Drag the mouse to select time',
day1: 'Monday',
day2: 'Tuesday',
day3: 'Wednesday',
day4: 'Thursday',
day5: 'Friday',
day6: 'Saturday',
day7: 'Sunday',
},
auth: {
createAuth: 'Create Authorization',
editAuth: 'Edit Authorization',
basicInfo: 'Basic Info',
priority: 'Priority',
emergency: 'Emergency',
high: 'High',
medium: 'Medium',
low: 'Low',
default: 'Default',
targetSelect: 'Target Select',
folderSelect: 'Folder Select',
assetSelect: 'Asset Select',
accountSelect: 'Account Select',
all: 'All',
selectItem: 'Select {name}',
selectItemTip: 'Please select the {name}',
regex: 'Regex',
regexTip: 'Please input the regular expression',
tag: 'Tag',
tags: 'Select Tag',
tagsTip: 'Please select the tag',
excludeItem: 'Exclude {name}',
excludeItemTip: 'Please select the {name} to be excluded',
authorizationRole: 'Authorization Role',
authorizationRoleTip: 'Please select an authorization role',
accessControl: 'Access Control',
accessTime: 'Access Time',
customTime: 'Custom Time',
timeTemplate: 'Time Template',
timeTemplateTip: 'Please select the time template',
editCustomTime: 'Edit Custom Time',
customTimeTip: 'No customized time set',
customTimeTip2: 'Please set a custom time',
timeZone: 'Time Zone',
time: 'Time',
commandFilter: 'Command Filter',
commandTip1: 'Please select the command',
commandTip2: 'Please select the command template',
ipWhiteList: 'IP White List',
ipWhiteListTip: 'Please select the IP White List',
validTime: 'Valid Time',
permanentValidity: 'Permanent Validity',
select: 'Select ({count} items)',
start: 'Start',
end: 'End'
}
}
export default oneterm_en

View File

@@ -16,7 +16,7 @@ const oneterm_zh = {
accessRestrictions: '接入限制',
comment: '备注',
node: '节点',
catalog: '目录',
folder: '目录',
cmdbType: '模型',
fieldMap: '字段映射',
field: '字段',
@@ -33,13 +33,24 @@ const oneterm_zh = {
editPublicKey: '编辑公钥',
assetCount: '资产数',
macAddress: 'Mac地址',
enabled: '启用',
disabled: '禁用',
isEnable: '是否启用',
confirmEnable: '确认切换启用状态?',
switching: '正在切换',
switchingTip: '正在切换,共{total}个,成功{successNum}个,失败{errorNum}个',
menu: {
oneterm: '堡垒机',
workStation: '工作台',
resourceControl: '资源管控',
basicResource: '基础资源',
assetManagement: '资产管理',
assets: '资产列表',
gateways: '网关列表',
accounts: '账号列表',
gatewayManagement: '网关管理',
accountManagement: '帐号管理',
accessControl: '访问控制',
accessAuthorization: '访问授权',
commandFilter: '命令拦截',
accessTime: '访问时段',
security: '安全设置',
sessionAuditing: '会话审计',
onlineSession: '在线会话',
@@ -82,6 +93,7 @@ const oneterm_zh = {
batchExecution: '批量执行',
chooseAssets: '选择资产',
batchExecutionPlaceholder: '请输入命令, 按 Enter 执行',
assetShare: '资产分享'
},
sessionTable: {
target: '目标',
@@ -105,12 +117,12 @@ const oneterm_zh = {
assetList: {
grantUser: '授权用户',
assetTree: '资产树',
createCatalog: '新建目录',
editCatalog: '编辑目录',
deleteCatalog: '删除目录',
grantCatalog: '授权目录',
createFolder: '新建目录',
editFolder: '编辑目录',
deleteFolder: '删除目录',
grantFolder: '授权目录',
ip: '地址',
catalogName: '目录名称',
folderName: '目录名称',
connectable: '可连接',
connected: '连接',
error: '错误',
@@ -125,9 +137,6 @@ const oneterm_zh = {
commandIntercept: '命令拦截',
allowAccess: '允许接入',
prohibitAccess: '禁止接入',
weektime: '星期/时间',
selectedTime: '已选择时间段',
drag: '可拖动鼠标选择时间段',
assetList: '资产列表',
createAsset: '创建资产',
editAsset: '编辑资产',
@@ -161,10 +170,15 @@ const oneterm_zh = {
grantLogin: '登录权限',
grantUserOrDep: '授权用户/部门',
grantRole: '授权角色',
createSubCatalog: '创建子目录',
createSubFolder: '创建子目录',
assetSearchTip: '在资产中搜索',
basic: '基础',
database: '数据库',
folderOperationPermissions: '目录操作权限',
assetOperationPermissions: '资产操作权限',
accountOperationPermissions: '帐号操作权限',
accessPermission: '访问权限',
operationPermissions: '操作权限'
},
log: {
time: '时间',
@@ -193,26 +207,51 @@ const oneterm_zh = {
clipboard: '剪贴板'
},
systemSettings: {
commandIntercept: '命令拦截',
terminalControl: '终端控制',
accessControl: '访问控制',
quickCommand: '快捷命令',
terminalDisplay: '终端显示',
publicKey: '我的公钥',
publicKeyTip: '用于终端登录堡垒机SSH服务的免密认证',
storageConfig: '存储配置'
},
commandIntercept: {
enable: '是否激活',
commandFilter: {
name: '命令拦截',
commandManagement: '命令管理',
commandTemplateManagement: '命令模板管理',
regexp: '是否正则',
createCommand: '创建命令',
editCommand: '编辑命令',
category: '分类',
securityRelated: '安全相关',
systemOperations: '系统操作',
databaseOperations: '数据库操作',
networkOperations: '网络操作',
fileOperations: '文件操作',
developmentRelated: '开发相关',
riskLevel: '风险等级',
safe: '安全',
warning: '警告',
dangerous: '危险',
criticalDanger: '严重危险',
commandCount: '命令数量',
createCommandTemplate: '新建命令模板',
editCommandTemplate: '编辑命令模板',
selectCommand: '选择命令',
unselectCommand: '未选命令',
selectedCommand: '已选命令',
},
terminalControl: {
accessControl: {
timeout: '会话最大空闲时间',
allowCopy: '允许复制',
allowPaste: '允许粘贴',
copyTip: '复制:本地设备可以实时获取远程桌面中的剪贴板文本内容。',
pasteTip: '粘贴:远程桌面可以通过剪贴板获取本地设备当前复制的文本内容。'
copyTip: '本地设备可以实时获取远程桌面中的剪贴板文本内容。',
pasteTip: '远程桌面可以通过剪贴板获取本地设备当前复制的文本内容。',
permissionConfig: '权限配置',
permissionConfigTip: '上传、下载是针对SSH、RDP协议复制、粘贴是针对RDP协议',
connect: '连接',
upload: '上传',
download: '下载',
copy: '复制',
paste: '粘贴',
share: '分享'
},
terminalDisplay: {
fontSetting: '字体设置',
@@ -269,7 +308,6 @@ const oneterm_zh = {
createConfig: '创建配置',
editConfig: '编辑配置',
local: '本地',
isEnable: '是否启用',
retentionDays: '保留天数',
archiveDays: '归档天数',
isCleanupEnabled: '是否启用清理',
@@ -303,8 +341,76 @@ const oneterm_zh = {
basicConfig: '基础配置',
advancedConfig: '高级配置',
advancedConfigTip: '配置存储的详细参数和策略',
confirmEnable: '确认切换启用状态?',
confirmPrimaryStorage: '确认切换主存储?'
},
timeTemplate: {
createTimeTemplate: '创建时间模板',
editTimeTemplate: '编辑时间模板',
timeZone: '时区',
category: '分类',
workTime: '工作时间',
dutyTime: '值班时间',
maintenanceTime: '维护时间',
emergencyResponse: '紧急响应',
allTime: '全天候响应',
timeRange: '时间段',
weektime: '星期/时间',
selectedTime: '已选择时间段',
drag: '可拖动鼠标选择时间段',
day1: '星期一',
day2: '星期二',
day3: '星期三',
day4: '星期四',
day5: '星期五',
day6: '星期六',
day7: '星期日',
},
auth: {
createAuth: '创建授权',
editAuth: '编辑授权',
basicInfo: '基础信息',
priority: '优先级',
emergency: '紧急',
high: '高优先级',
medium: '中优先级',
low: '低优先级',
default: '默认',
targetSelect: '目标选择',
folderSelect: '目录选择',
assetSelect: '资产选择',
accountSelect: '帐号选择',
all: '全部',
selectItem: '选择{name}',
selectItemTip: '请选择{name}',
regex: '正则表达式',
regexTip: '请输入正则表达式',
tag: '标签',
tags: '选择标签',
tagsTip: '请选择标签',
excludeItem: '排除{name}',
excludeItemTip: '请选择需要排除的{name}',
accessControl: '访问控制',
authorizationRole: '授权角色',
authorizationRoleTip: '请选择授权角色',
accessTime: '访问时间',
customTime: '自定义时间',
timeTemplate: '时间模板',
timeTemplateTip: '请选择时间模板',
editCustomTime: '编辑自定义时间',
customTimeTip: '未设置自定义时间',
customTimeTip2: '请设置自定义时间',
timeZone: '时区',
time: '时段',
commandFilter: '命令拦截',
commandTip1: '请选择命令',
commandTip2: '请选择命令模板',
ipWhiteList: 'IP白名单',
ipWhiteListTip: '请选择IP白名单',
validTime: '有效时间',
permanentValidity: '永久有效',
select: '选择 ({count}个)',
start: '开始',
end: '结束'
}
}
export default oneterm_zh

View File

@@ -1,146 +1,180 @@
// @ts-ignore
import { BasicLayout, RouteView } from '@/layouts'
import user from '@/store/global/user'
const genOnetermRoutes = () => {
return {
path: '/oneterm',
name: 'oneterm',
component: BasicLayout,
meta: { title: 'oneterm.menu.oneterm', keepAlive: false },
redirect: () => {
const { detailPermissions } = user.state
return {
path: '/oneterm',
name: 'oneterm',
component: BasicLayout,
meta: { title: 'oneterm.menu.oneterm', keepAlive: false },
redirect: () => {
const { detailPermissions } = user.state
if (detailPermissions['oneterm'].some(item => item.name === 'WorkStation')) {
return '/oneterm/workstation'
}
if (detailPermissions['oneterm'].some(item => item.name === 'Dashboard')) {
return '/oneterm/dashboard'
}
if (detailPermissions['oneterm'].some(item => item.name === 'WorkStation')) {
return '/oneterm/workstation'
}
if (detailPermissions['oneterm'].some(item => item.name === 'Dashboard')) {
return '/oneterm/dashboard'
}
return '/oneterm/workstation'
},
return '/oneterm/workstation'
},
children: [
{
path: '/oneterm/dashboard',
name: 'onterm_dashboard',
component: () => import('../views/dashboard'),
meta: { title: 'dashboard', appName: 'oneterm', icon: 'ops-oneterm-dashboard-selected', selectedIcon: 'ops-oneterm-dashboard-selected', keepAlive: false, permission: ['oneterm_admin', 'admin'] }
},
{
path: '/oneterm/workstation',
name: 'onterm_work_station',
component: () => import('../views/workStation'),
meta: {
title: 'oneterm.menu.workStation', icon: 'ops-oneterm-workstation-selected', selectedIcon: 'ops-oneterm-workstation-selected', keepAlive: false
}
},
{
path: '/oneterm/resource',
name: 'oneterm_resource',
component: RouteView,
meta: { title: 'oneterm.menu.resourceControl', appName: 'oneterm', icon: 'ops-oneterm-asset-management', selectedIcon: 'ops-oneterm-asset-management', permission: ['oneterm_admin', 'admin'] },
redirect: '/oneterm/assets/assets',
children: [
{
path: '/oneterm/dashboard',
name: 'onterm_dashboard',
component: () => import('../views/dashboard'),
meta: { title: 'dashboard', appName: 'oneterm', icon: 'ops-oneterm-dashboard-selected', selectedIcon: 'ops-oneterm-dashboard-selected', keepAlive: false, permission: ['oneterm_admin', 'admin'] }
path: `/oneterm/basicresource`,
name: `oneterm_resource_management`,
meta: { title: 'oneterm.menu.basicResource', appName: 'oneterm', disabled: true, style: 'margin-left: 12px', permission: ['oneterm_admin', 'admin'] },
},
{
path: '/oneterm/workstation',
name: 'onterm_work_station',
component: () => import('../views/workStation'),
meta: {
title: 'oneterm.menu.workStation', icon: 'ops-oneterm-workstation-selected', selectedIcon: 'ops-oneterm-workstation-selected', keepAlive: false
}
path: '/oneterm/asset',
name: 'oneterm_asset_list',
meta: { title: 'oneterm.menu.assetManagement', icon: 'ops-oneterm-assetlist', selectedIcon: 'ops-oneterm-assetlist-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/assets/assets')
},
{
path: '/oneterm/assets',
name: 'oneterm_assets',
component: RouteView,
meta: { title: 'oneterm.menu.assetManagement', appName: 'oneterm', icon: 'ops-oneterm-asset-management', selectedIcon: 'ops-oneterm-asset-management', permission: ['oneterm_admin', 'admin'] },
redirect: '/oneterm/assets/assets',
children: [{
path: '/oneterm/assetlist',
name: 'oneterm_asset_list',
meta: { title: 'oneterm.menu.assets', icon: 'ops-oneterm-assetlist', selectedIcon: 'ops-oneterm-assetlist-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/assets/assets')
}, {
path: '/oneterm/account',
name: 'oneterm_account',
meta: { title: 'oneterm.menu.accounts', icon: 'ops-oneterm-account', selectedIcon: 'ops-oneterm-account-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/assets/account')
}, {
path: '/oneterm/gateway',
name: 'oneterm_gateway',
meta: { title: 'oneterm.menu.gateways', icon: 'ops-oneterm-gateway', selectedIcon: 'ops-oneterm-gateway-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/assets/gateway')
}]
path: '/oneterm/account',
name: 'oneterm_account_management',
meta: { title: 'oneterm.menu.accountManagement', icon: 'ops-oneterm-account', selectedIcon: 'ops-oneterm-account-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/assets/account')
},
{
path: '/oneterm/audit',
name: 'oneterm_session',
component: RouteView,
meta: { title: 'oneterm.menu.auditCentre', icon: 'ops-oneterm-log-selected', selectedIcon: 'ops-oneterm-log-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
redirect: '/oneterm/session/online',
hideChildrenInMenu: false,
children: [
{
path: `/oneterm/session`,
name: `oneterm_session`,
meta: { title: 'oneterm.menu.sessionAuditing', disabled: true, style: 'margin-left: 12px', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
},
{
path: '/oneterm/session/online',
name: 'oneterm_session_online',
meta: { title: 'oneterm.menu.onlineSession', icon: 'ops-oneterm-sessiononline', selectedIcon: 'ops-oneterm-sessiononline-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/session/online.vue')
}, {
path: '/oneterm/session/history',
name: 'oneterm_session_history',
meta: { title: 'oneterm.menu.offlineSession', icon: 'ops-oneterm-sessionhistory', selectedIcon: 'ops-oneterm-sessionhistory-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/session/history.vue')
},
{
path: `/oneterm/log`,
name: `oneterm_log`,
meta: { title: 'oneterm.menu.logAuditing', disabled: true, style: 'margin-left: 12px', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
},
{
path: '/oneterm/log/login',
name: 'oneterm_log_login',
meta: { title: 'oneterm.menu.loginLog', icon: 'ops-oneterm-login', selectedIcon: 'ops-oneterm-login-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/log/login')
}, {
path: '/oneterm/log/operation',
name: 'oneterm_log_operation',
meta: { title: 'oneterm.menu.operationLog', icon: 'ops-oneterm-operation', selectedIcon: 'ops-oneterm-operation-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/log/operation')
}, {
path: '/oneterm/log/file',
name: 'oneterm_log_file',
meta: { title: 'oneterm.menu.fileLog', appName: 'oneterm', icon: 'ops-oneterm-file_log', selectedIcon: 'ops-oneterm-file_log-selected', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/log/file')
}
]
path: '/oneterm/gateway',
name: 'oneterm_gateway_management',
meta: { title: 'oneterm.menu.gatewayManagement', icon: 'ops-oneterm-gateway', selectedIcon: 'ops-oneterm-gateway-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/assets/gateway')
},
{
path: '/oneterm/settings',
name: 'onterm_settings',
component: () => import('../views/systemSettings'),
meta: { title: 'oneterm.menu.systemSettings', appName: 'oneterm', icon: 'veops-setting2', selectedIcon: 'veops-setting2', keepAlive: false }
path: `/oneterm/access`,
name: `oneterm_access`,
meta: { title: 'oneterm.menu.accessControl', appName: 'oneterm', disabled: true, style: 'margin-left: 12px', permission: ['oneterm_admin', 'admin'] },
},
{
path: '/oneterm/terminal',
name: 'oneterm_terminal',
hidden: true,
component: () => import('../views/connect/terminal/index.vue'),
meta: { title: 'oneterm.menu.terminal', keepAlive: false }
path: '/oneterm/access/auth',
name: 'oneterm_access_auth',
meta: { title: 'oneterm.menu.accessAuthorization', appName: 'oneterm', icon: 'ops-oneterm-assetlist', selectedIcon: 'ops-oneterm-assetlist-selected', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/access/auth')
},
{
path: '/oneterm/guacamole/:asset_id/:account_id/:protocol',
name: 'oneterm_guacamole',
hidden: true,
component: () => import('../views/connect/guacamoleClient/index.vue'),
meta: { title: 'oneterm.menu.terminal', keepAlive: false }
path: '/oneterm/access/command',
name: 'oneterm_access_command',
meta: { title: 'oneterm.menu.commandFilter', appName: 'oneterm', icon: 'ops-oneterm-assetlist', selectedIcon: 'ops-oneterm-assetlist-selected', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/access/command')
},
{
path: '/oneterm/replay/:session_id',
name: 'oneterm_replay',
hidden: true,
component: () => import('../views/replay'),
meta: { title: 'oneterm.menu.replay', keepAlive: false }
},
{
path: '/oneterm/replay/guacamole/:session_id',
name: 'oneterm_replay_guacamole',
hidden: true,
component: () => import('../views/replay/guacamoleReplay.vue'),
meta: { title: 'oneterm.menu.replay', keepAlive: false }
},
path: '/oneterm/access/time',
name: 'oneterm_access_time',
meta: { title: 'oneterm.menu.accessTime', appName: 'oneterm', icon: 'ops-oneterm-assetlist', selectedIcon: 'ops-oneterm-assetlist-selected', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/access/time')
}
]
}
},
{
path: '/oneterm/audit',
name: 'oneterm_session',
component: RouteView,
meta: { title: 'oneterm.menu.auditCentre', icon: 'ops-oneterm-log-selected', selectedIcon: 'ops-oneterm-log-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
redirect: '/oneterm/session/online',
hideChildrenInMenu: false,
children: [
{
path: `/oneterm/session`,
name: `oneterm_session`,
meta: { title: 'oneterm.menu.sessionAuditing', disabled: true, style: 'margin-left: 12px', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
},
{
path: '/oneterm/session/online',
name: 'oneterm_session_online',
meta: { title: 'oneterm.menu.onlineSession', icon: 'ops-oneterm-sessiononline', selectedIcon: 'ops-oneterm-sessiononline-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/session/online.vue')
},
{
path: '/oneterm/session/history',
name: 'oneterm_session_history',
meta: { title: 'oneterm.menu.offlineSession', icon: 'ops-oneterm-sessionhistory', selectedIcon: 'ops-oneterm-sessionhistory-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/session/history.vue')
},
{
path: `/oneterm/log`,
name: `oneterm_log`,
meta: { title: 'oneterm.menu.logAuditing', disabled: true, style: 'margin-left: 12px', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
},
{
path: '/oneterm/log/login',
name: 'oneterm_log_login',
meta: { title: 'oneterm.menu.loginLog', icon: 'ops-oneterm-login', selectedIcon: 'ops-oneterm-login-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/log/login')
},
{
path: '/oneterm/log/operation',
name: 'oneterm_log_operation',
meta: { title: 'oneterm.menu.operationLog', icon: 'ops-oneterm-operation', selectedIcon: 'ops-oneterm-operation-selected', appName: 'oneterm', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/log/operation')
},
{
path: '/oneterm/log/file',
name: 'oneterm_log_file',
meta: { title: 'oneterm.menu.fileLog', appName: 'oneterm', icon: 'ops-oneterm-file_log', selectedIcon: 'ops-oneterm-file_log-selected', permission: ['oneterm_admin', 'admin'] },
component: () => import('../views/log/file')
}
]
},
{
path: '/oneterm/settings',
name: 'onterm_settings',
component: () => import('../views/systemSettings'),
meta: { title: 'oneterm.menu.systemSettings', appName: 'oneterm', icon: 'veops-setting2', selectedIcon: 'veops-setting2', keepAlive: false }
},
{
path: '/oneterm/terminal',
name: 'oneterm_terminal',
hidden: true,
component: () => import('../views/connect/terminal/index.vue'),
meta: { title: 'oneterm.menu.terminal', keepAlive: false }
},
{
path: '/oneterm/guacamole/:asset_id/:account_id/:protocol',
name: 'oneterm_guacamole',
hidden: true,
component: () => import('../views/connect/guacamoleClient/index.vue'),
meta: { title: 'oneterm.menu.terminal', keepAlive: false }
},
{
path: '/oneterm/replay/:session_id',
name: 'oneterm_replay',
hidden: true,
component: () => import('../views/replay'),
meta: { title: 'oneterm.menu.replay', keepAlive: false }
},
{
path: '/oneterm/replay/guacamole/:session_id',
name: 'oneterm_replay_guacamole',
hidden: true,
component: () => import('../views/replay/guacamoleReplay.vue'),
meta: { title: 'oneterm.menu.replay', keepAlive: false }
},
]
}
}
export default genOnetermRoutes

View File

@@ -1,8 +1,8 @@
const onetermStore = {
namespaced: true,
name: 'onetermStore',
state: {},
mutations: {},
actions: {}
namespaced: true,
name: 'onetermStore',
state: {},
mutations: {},
actions: {}
}
export default onetermStore

View File

@@ -1,50 +1,50 @@
import i18n from '@/lang'
export const getAllParentNodesLabel = (node, label) => {
if (node.parentNode) {
return getAllParentNodesLabel(node.parentNode, `${node.parentNode.label}-${label}`)
}
return label
if (node.parentNode) {
return getAllParentNodesLabel(node.parentNode, `${node.parentNode.label}-${label}`)
}
return label
}
export const getTreeSelectLabel = (node) => {
return `${getAllParentNodesLabel(node, node.label)}`
return `${getAllParentNodesLabel(node, node.label)}`
}
export const setLocalStorage = (name, param) => {
let storageData = JSON.parse(localStorage.getItem(name))
if (storageData) {
storageData = { ...storageData, ...param }
} else {
storageData = { ...param }
}
localStorage.setItem(name, JSON.stringify(storageData))
let storageData = JSON.parse(localStorage.getItem(name))
if (storageData) {
storageData = { ...storageData, ...param }
} else {
storageData = { ...param }
}
localStorage.setItem(name, JSON.stringify(storageData))
}
class Strings {
hasText = function (text) {
return !(text === undefined || text === null || text.length === 0)
}
hasText = function (text) {
return !(text === undefined || text === null || text.length === 0)
}
zeroPad = function zeroPad(num, minLength) {
let str = num.toString()
while (str.length < minLength) { str = '0' + str }
return str
};
zeroPad = function zeroPad(num, minLength) {
let str = num.toString()
while (str.length < minLength) { str = '0' + str }
return str
};
}
export const strings = new Strings()
class Times {
formatTime = function formatTime(millis) {
const totalSeconds = Math.floor(millis / 1000)
formatTime = function formatTime(millis) {
const totalSeconds = Math.floor(millis / 1000)
// Split into seconds and minutes
const seconds = totalSeconds % 60
const minutes = Math.floor(totalSeconds / 60)
// Split into seconds and minutes
const seconds = totalSeconds % 60
const minutes = Math.floor(totalSeconds / 60)
// Format seconds and minutes as MM:SS
return strings.zeroPad(minutes, 2) + ':' + strings.zeroPad(seconds, 2)
};
// Format seconds and minutes as MM:SS
return strings.zeroPad(minutes, 2) + ':' + strings.zeroPad(seconds, 2)
};
}
export const times = new Times()

View File

@@ -0,0 +1,169 @@
<template>
<a-form-model-item :label="$t('oneterm.auth.accessTime')" :prop="formItemProp">
<a-radio-group
:value="accessTimeType"
@change="handleRadioChange"
>
<a-radio :value="ACCESS_TIME_TYPE.TIME_TEMPLATE">
{{ $t('oneterm.auth.timeTemplate') }}
</a-radio>
<a-radio :value="ACCESS_TIME_TYPE.CUSTOM_TIME">
{{ $t('oneterm.auth.customTime') }}
<a
v-if="accessTimeType === ACCESS_TIME_TYPE.CUSTOM_TIME"
@click="openCustomTimeModal"
>
<ops-icon type="veops-edit"/>
</a>
</a-radio>
</a-radio-group>
<a-select
v-if="accessTimeType === ACCESS_TIME_TYPE.TIME_TEMPLATE"
:value="form.access_control.time_template.template_id"
:options="timeTemplateSelectOptions"
:placeholder="$t('oneterm.auth.timeTemplateTip')"
@change="handleTimeTemplateChange"
/>
<div class="custom-time" v-else-if="accessTimeType === ACCESS_TIME_TYPE.CUSTOM_TIME">
<template v-if="!form.access_control.custom_time_ranges.length">
<a-icon type="exclamation-circle" />
{{ $t('oneterm.auth.customTimeTip') }}
</template>
<template v-else>
<div class="custom-time-row">
<span class="custom-time-label">{{ $t('oneterm.auth.timeZone') }}:</span>
{{ form.access_control.timezone }}
</div>
<div class="custom-time-row">
<span class="custom-time-label">{{ $t('oneterm.auth.time') }}:</span>
<div>
<div
v-for="(item, index) in customTimeText"
:key="index"
>
<span>{{ item.start_time }}~{{ item.end_time }}</span>
<span class="custom-time-week">{{ item.weekText }}</span>
</div>
</div>
</div>
</template>
</div>
<CustomTimeModal
ref="customTimeModalRef"
@ok="handleCustomTimeModalOk"
/>
</a-form-model-item>
</template>
<script>
import { getTimeTemplateList } from '@/modules/oneterm/api/timeTemplate.js'
import { ACCESS_TIME_TYPE } from './constants.js'
import CustomTimeModal from './customTimeModal.vue'
export default {
name: 'AccessTime',
components: {
CustomTimeModal
},
props: {
accessTimeType: {
type: String,
default: ACCESS_TIME_TYPE.TIME_TEMPLATE
},
form: {
type: Object,
default: () => {}
}
},
data() {
return {
ACCESS_TIME_TYPE,
timeTemplateSelectOptions: [],
}
},
computed: {
formItemProp() {
return this.accessTimeType === ACCESS_TIME_TYPE.TIME_TEMPLATE ? 'access_control.time_template.template_id' : 'access_control.custom_time_ranges'
},
customTimeText() {
const custom_time_ranges = this.form?.access_control?.custom_time_ranges || []
return custom_time_ranges.map((item) => {
return {
start_time: item.start_time,
end_time: item.end_time,
weekText: item.weekdays.map((day) => this.$t(`oneterm.timeTemplate.day${day}`)).join(', ')
}
})
}
},
mounted() {
this.getTimeTemplateList()
},
methods: {
getTimeTemplateList() {
getTimeTemplateList({
page_index: 1,
page_size: 9999,
}).then((res) => {
const list = res?.data?.list || []
this.timeTemplateSelectOptions = list.map((item) => ({
value: item.id,
label: item.name
}))
})
},
handleRadioChange(e) {
const value = e?.target?.value
if (value !== this.accessTimeType) {
this.$emit('update:accessTimeType', value)
}
},
handleTimeTemplateChange(value) {
this.$emit('change', ['access_control', 'time_template', 'template_id'], value)
},
openCustomTimeModal() {
const { custom_time_ranges = [], timezone } = this.form?.access_control || {}
this.$refs.customTimeModalRef.open({
custom_time_ranges,
timezone
})
},
handleCustomTimeModalOk(data) {
const { custom_time_ranges, timezone } = data
this.$emit('change', ['access_control', 'custom_time_ranges'], custom_time_ranges)
this.$emit('change', ['access_control', 'timezone'], timezone)
}
}
}
</script>
<style lang="less" scoped>
.custom-time {
&-row {
display: flex;
}
&-label {
flex-shrink: 0;
margin-right: 6px;
}
&-week {
margin-left: 6px;
font-size: 12px;
color: #999999;
}
&-edit {
flex-shrink: 0;
margin-left: 12px;
}
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<a-form-model-item :label="$t('oneterm.auth.authorizationRole')" prop="rids">
<EmployeeTreeSelect
:value="rids"
multiple
:idType="2"
departmentKey="acl_rid"
employeeKey="acl_rid"
:placeholder="`${$t(`placeholder2`)}`"
class="custom-treeselect custom-treeselect-white"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-multiple-lineHeight': '18px',
}"
:limit="1"
:otherOptions="visualRoleList"
@change="handleRoleChange"
/>
</a-form-model-item>
</template>
<script>
import { searchRole } from '@/modules/acl/api/role'
import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue'
export default {
name: 'AuthorizationRole',
components: {
EmployeeTreeSelect
},
props: {
form: {
type: Object,
default: () => {}
}
},
data() {
return {
visualRoleList: []
}
},
computed: {
rids() {
return (this.form?.rids || []).map((r) => `employee-${r}`)
}
},
mounted() {
this.getRoleList()
},
methods: {
async getRoleList() {
const res = await searchRole({ page_size: 9999, page: 1, app_id: 'oneterm', user_role: 0, user_only: 0, is_all: true })
const visualRoleList = []
const roleList = (res?.roles || []).filter((item) => !/_virtual$/.test(item.name))
if (roleList.length) {
visualRoleList.push({
acl_rid: -100,
department_name: this.$t('acl.visualRole'),
sub_departments: [],
employees: roleList.map((item) => {
return {
nickname: item.name,
acl_rid: item.id
}
})
})
}
this.$set(this, 'visualRoleList', visualRoleList)
},
handleRoleChange(value) {
const rids = (value || []).map((r) => Number(r.split('-')[1]))
this.$emit('change', ['rids'], rids)
}
}
}
</script>

View File

@@ -0,0 +1,69 @@
<template>
<a-form-model-item :label="$t('oneterm.auth.commandFilter')" prop="access_control.cmd_ids">
<a-select
mode="multiple"
:value="form.access_control.cmd_ids"
:options="commandSelectOptions"
:placeholder="$t('oneterm.auth.commandTip1')"
@change="(value) => $emit('change', ['access_control', 'cmd_ids'], value)"
/>
<a-select
mode="multiple"
:value="form.access_control.template_ids"
:options="commandTemplateSelectOptions"
:placeholder="$t('oneterm.auth.commandTip2')"
@change="(value) => $emit('change', ['access_control', 'template_ids'], value)"
/>
</a-form-model-item>
</template>
<script>
import { getCommandList } from '@/modules/oneterm/api/command.js'
import { getCommandTemplateList } from '@/modules/oneterm/api/commandTemplate.js'
export default {
name: 'Command',
props: {
form: {
type: Object,
default: () => {}
}
},
data() {
return {
commandSelectOptions: [],
commandTemplateSelectOptions: [],
}
},
mounted() {
this.getCommandList()
this.getCommandTemplateList()
},
methods: {
getCommandList() {
getCommandList({
page_index: 1,
page_size: 9999
}).then((res) => {
const list = res?.data?.list || []
this.commandSelectOptions = list.map((item) => ({
value: item.id,
label: item.name
}))
})
},
getCommandTemplateList() {
getCommandTemplateList({
page_index: 1,
page_size: 9999
}).then((res) => {
const list = res?.data?.list || []
this.commandTemplateSelectOptions = list.map((item) => ({
value: item.id,
label: item.name
}))
})
},
}
}
</script>

View File

@@ -0,0 +1,4 @@
export const ACCESS_TIME_TYPE = {
TIME_TEMPLATE: 'timeTemplate',
CUSTOM_TIME: 'customTime'
}

View File

@@ -0,0 +1,138 @@
<template>
<a-modal
:title="$t('oneterm.auth.editCustomTime')"
:visible="visible"
:width="1000"
:bodyStyle="{
maxHeight: '60vh',
overflowY: 'auto'
}"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form-model
ref="customTimeFormRef"
:model="form"
:rules="rules"
:label-col="{ span: 5 }"
>
<a-form-model-item :wrapper-col="{ span: 16 }" :label="$t('oneterm.timeTemplate.timeZone')" prop="timezone">
<a-select
v-model="form.timezone"
showSearch
:placeholder="$t('placeholder2')"
:options="timezoneSelectOptions"
/>
</a-form-model-item>
<a-form-model-item :wrapper-col="{ span: 16 }" :label="$t('oneterm.timeTemplate.timeRange')" prop="custom_time_ranges">
<DragWeekTime
v-model="form.custom_time_ranges"
:data="weekTimeData"
@onClear="clearWeektime"
/>
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import _ from 'lodash'
import momentTimezone from 'moment-timezone'
import { mergeTimeRange } from '@/modules/oneterm/views/access/time/mergeTimeRange.js'
import { splitTimeRange } from '@/modules/oneterm/views/access/time/splitTimeRange.js'
import DragWeekTime from '@/modules/oneterm/components/dragWeektime'
import weekTimeData from '@/modules/oneterm/components/dragWeektime/weektimeData'
const DEFAULT_FORM = {
timezone: momentTimezone.tz.guess(),
custom_time_ranges: []
}
export default {
name: 'CustomTimeModal',
components: {
DragWeekTime
},
data() {
return {
visible: false,
form: { ...DEFAULT_FORM },
rules: {
timezone: [{ required: true, message: this.$t(`placeholder1`) }],
custom_time_ranges: [{ required: true, message: this.$t(`placeholder2`) }],
},
weekTimeData: _.cloneDeep(weekTimeData)
}
},
computed: {
timezoneSelectOptions() {
const names = momentTimezone.tz.names()
return names.map((value) => {
return {
value,
label: value
}
})
}
},
methods: {
open(data) {
this.visible = true
if (data) {
let custom_time_ranges = []
if (data?.custom_time_ranges?.length) {
const timeRanges = splitTimeRange(data.custom_time_ranges)
custom_time_ranges = timeRanges.map((item) => {
const childData = this.weekTimeData?.[item?.day - 1]?.child
if (childData?.length) {
childData.forEach((t) => {
this.$set(t, 'check', Boolean(item?.value?.length) && item.value.includes(t.value))
})
}
return {
id: item.day,
day: item.day,
value: item.value,
}
})
}
this.form = {
timezone: data?.timezone ?? momentTimezone.tz.guess(),
custom_time_ranges
}
}
},
clearWeektime() {
this.weekTimeData.forEach((item) => {
item.child.forEach((t) => {
this.$set(t, 'check', false)
})
})
this.form.custom_time_ranges = []
},
handleCancel() {
this.visible = false
},
async handleOk() {
this.$refs.customTimeFormRef.validate(async (valid) => {
if (!valid) return
let custom_time_ranges = []
if (this?.form?.custom_time_ranges?.length) {
custom_time_ranges = mergeTimeRange(this.form.custom_time_ranges)
}
this.$emit('ok', {
custom_time_ranges,
timezone: this.form.timezone
})
this.handleCancel()
})
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,92 @@
<template>
<div>
<AuthorizationRole
:form="form"
@change="handleFormChange"
/>
<a-form-model-item
prop="permissions"
:label="$t('oneterm.accessControl.permissionConfig')"
:extra="$t('oneterm.accessControl.permissionConfigTip')"
>
<PermissionCheckbox
:value="form.permissions"
@change="(key, checked) => handleFormChange(['permissions', key], checked)"
/>
</a-form-model-item>
<AccessTime
:accessTimeType="accessTimeType"
:form="form"
@update:accessTimeType="accessTimeType = $event"
@change="handleFormChange"
/>
<Command
:form="form"
@change="handleFormChange"
/>
<IPWhiteList
:form="form"
@change="handleFormChange"
/>
</div>
</template>
<script>
import { ACCESS_TIME_TYPE } from './constants.js'
import AuthorizationRole from './authorizationRole.vue'
import PermissionCheckbox from '@/modules/oneterm/views/systemSettings/accessControl/permissionCheckbox.vue'
import AccessTime from './accessTime.vue'
import Command from './command.vue'
import IPWhiteList from './ipWhiteList.vue'
export default {
name: 'AccessControl',
components: {
AuthorizationRole,
PermissionCheckbox,
AccessTime,
Command,
IPWhiteList
},
props: {
form: {
type: Object,
default: () => {}
}
},
data() {
return {
accessTimeType: ACCESS_TIME_TYPE.TIME_TEMPLATE
}
},
methods: {
initAccessTimeType(form) {
let accessTimeType = ACCESS_TIME_TYPE.TIME_TEMPLATE
if (
form.access_control?.custom_time_ranges?.length &&
form.access_control?.timezone
) {
accessTimeType = ACCESS_TIME_TYPE.CUSTOM_TIME
}
this.accessTimeType = accessTimeType
},
getAccessTimeType() {
return this.accessTimeType
},
handleFormChange(keys, value) {
this.$emit(
'change',
{
keys,
value
}
)
},
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,40 @@
<template>
<a-form-model-item :label="$t('oneterm.auth.ipWhiteList')" prop="access_control.ip_whitelist">
<a-select
mode="tags"
:value="form.access_control.ip_whitelist"
:options="tagSelectOptions"
:placeholder="$t('oneterm.auth.ipWhiteListTip')"
@change="handleTagChange"
/>
</a-form-model-item>
</template>
<script>
import _ from 'lodash'
export default {
name: 'IPWhiteList',
props: {
form: {
type: Object,
default: () => {}
}
},
data() {
return {
tagSelectOptions: [],
}
},
methods: {
handleTagChange(value) {
this.$emit('change', ['access_control', 'ip_whitelist'], value)
const tagSelectOptions = this.tagSelectOptions.concat(value.map((item) => ({
label: item,
value: item
})))
this.tagSelectOptions = _.uniqBy(tagSelectOptions, 'value')
}
}
}
</script>

View File

@@ -0,0 +1,66 @@
<template>
<div class="basic-form">
<a-form-model-item :label="$t('name')" prop="name">
<a-input
:value="form.name"
:placeholder="$t('placeholder1')"
@change="(e) => handleFormChange(['name'], e.target.value)"
/>
</a-form-model-item>
<a-form-model-item :label="$t('description')" prop="description">
<a-input
:value="form.description"
:placeholder="$t('placeholder1')"
@change="(e) => handleFormChange(['description'], e.target.value)"
/>
</a-form-model-item>
<ValidTime
:valid_from="form.valid_from"
:valid_to="form.valid_to"
@change="handleFormChange"
/>
<a-form-model-item :label="$t('oneterm.isEnable')" prop="enabled">
<a-switch
:checked="form.enabled"
@change="(checked) => handleFormChange(['enabled'], checked)"
/>
</a-form-model-item>
</div>
</template>
<script>
import ValidTime from './validTime.vue'
export default {
name: 'BasicInfo',
components: {
ValidTime
},
props: {
form: {
type: Object,
default: () => {}
}
},
data() {
return {}
},
methods: {
handleFormChange(keys, value) {
this.$emit(
'change',
{
keys,
value
}
)
}
},
}
</script>
<style lang="less" scoped>
.basic-form {
width: 100%;
}
</style>

View File

@@ -0,0 +1,48 @@
<template>
<a-form-model-item :label="$t('oneterm.auth.validTime')" prop="valid_from">
<a-range-picker
v-model="validTime"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</a-form-model-item>
</template>
<script>
import moment from 'moment'
export default {
name: 'ValidTime',
props: {
valid_from: {
type: String,
default: undefined
},
valid_to: {
type: String,
default: undefined
}
},
computed: {
validTime: {
get() {
if (this.valid_from && this.valid_to) {
return [moment(this.valid_from), moment(this.valid_to)]
}
return []
},
set(val) {
if (Array.isArray(val) && val.length === 2) {
this.$emit('change', ['valid_from'], val[0] ? val[0].format('YYYY-MM-DD HH:mm:ss') : undefined)
this.$emit('change', ['valid_to'], val[1] ? val[1].format('YYYY-MM-DD HH:mm:ss') : undefined)
} else {
this.$emit('change', ['valid_from'], undefined)
this.$emit('change', ['valid_to'], undefined)
}
return val
}
}
}
}
</script>

View File

@@ -0,0 +1,258 @@
<template>
<CustomDrawer
width="850px"
:visible="visible"
:title="title"
:maskClosable="false"
@close="handleClose"
>
<a-form-model
ref="authFormRef"
:model="form"
:rules="rules"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 16 }"
class="auth-form"
>
<div class="auth-form-title">{{ $t('oneterm.auth.basicInfo') }}</div>
<BasicInfo
:form="form"
@change="handleFormChange"
/>
<div class="auth-form-title">{{ $t('oneterm.auth.targetSelect') }}</div>
<TargetSelect
:form="form"
@change="handleFormChange"
/>
<div class="auth-form-title">{{ $t('oneterm.auth.accessControl') }}</div>
<AccessControl
:form="form"
ref="accessControlRef"
@change="handleFormChange"
/>
<div class="custom-drawer-bottom-action">
<a-button @click="handleClose">{{ $t('cancel') }}</a-button>
<a-button @click="handleSubmit" type="primary">{{ $t('confirm') }}</a-button>
</div>
</a-form-model>
</CustomDrawer>
</template>
<script>
import _ from 'lodash'
import momentTimezone from 'moment-timezone'
import { postAuth, putAuthById } from '@/modules/oneterm/api/authorizationV2.js'
import { getConfig } from '@/modules/oneterm/api/config'
import { TARGET_SELECT_TYPE } from '../constants.js'
import { ACCESS_TIME_TYPE } from './accessControl/constants.js'
import { PERMISSION_TYPE } from '@/modules/oneterm/views/systemSettings/accessControl/constants.js'
import BasicInfo from './basicInfo/index.vue'
import TargetSelect from './targetSelect/index.vue'
import AccessControl from './accessControl/index.vue'
const DEFAULT_PERMISSIONS = Object.values(PERMISSION_TYPE).reduce((config, key) => {
config[key] = false
return config
}, {})
const DEFAULT_FORM = {
name: '',
description: '',
enabled: true,
node_selector: {
type: TARGET_SELECT_TYPE.ALL,
values: [],
exclude_ids: []
},
asset_selector: {
type: TARGET_SELECT_TYPE.ALL,
values: [],
exclude_ids: []
},
account_selector: {
type: TARGET_SELECT_TYPE.ALL,
values: [],
exclude_ids: []
},
rids: [],
permissions: DEFAULT_PERMISSIONS,
access_control: {
time_template: {
template_id: undefined
},
custom_time_ranges: [],
timezone: momentTimezone.tz.guess(),
cmd_ids: [],
template_ids: [],
ip_whitelist: [],
},
valid_from: undefined,
valid_to: undefined
}
export default {
name: 'AuthDrawer',
components: {
BasicInfo,
TargetSelect,
AccessControl
},
data() {
return {
visible: false,
authId: '',
form: _.cloneDeep(DEFAULT_FORM),
rules: {
name: [{ required: true, message: this.$t('placeholder1') }],
rids: [{ required: true, message: this.$t('oneterm.auth.authorizationRoleTip') }],
permissions: [{ required: true, message: this.$t('placeholder2') }]
},
confirmLoading: false
}
},
computed: {
title() {
if (this.authId) {
return this.$t('oneterm.auth.editAuth')
}
return this.$t('oneterm.auth.createAuth')
}
},
mounted() {
this.initDefaultPermissions()
},
methods: {
initDefaultPermissions() {
getConfig({
info: true
}).then((res) => {
const default_permissions = res?.data?.default_permissions
Object.keys(DEFAULT_FORM.permissions).forEach((key) => {
DEFAULT_FORM.permissions[key] = default_permissions?.[key] ?? DEFAULT_FORM.permissions[key]
})
this.form.permissions = DEFAULT_FORM.permissions
})
},
open(data) {
this.visible = true
if (data) {
const editData = _.cloneDeep(data)
// merge initialization form data (editData || DEFAULT_FORM)
const form = {}
Object.keys(DEFAULT_FORM).forEach((key) => {
if (typeof DEFAULT_FORM[key] === 'object' && !Array.isArray(DEFAULT_FORM[key])) {
form[key] = {}
Object.keys(DEFAULT_FORM[key]).forEach((childKey) => {
form[key][childKey] = editData?.[key]?.[childKey] ?? DEFAULT_FORM[key][childKey]
})
} else {
form[key] = editData?.[key] ?? DEFAULT_FORM[key]
}
})
this.form = form
this.authId = editData?.id ?? ''
}
this.$nextTick(() => {
this.$refs.accessControlRef.initAccessTimeType(this.form)
})
},
/**
* update form data
* @param keys key list [root key, parent key, child key, ...]
* @param value updated value
*/
handleFormChange({
keys,
value,
}) {
let obj = this.form
if (keys.length > 1) {
obj = _.get(this.form, keys.slice(0, -1).join('.'))
}
this.$set(obj, keys.slice(-1), value)
},
handleClose() {
this.form = _.cloneDeep(DEFAULT_FORM)
this.authId = ''
this.$refs.authFormRef.resetFields()
this.visible = false
},
async handleSubmit() {
this.$refs.authFormRef.validate(async (valid) => {
if (!valid) return
this.confirmLoading = true
try {
const { params, errorMessage } = this.handleSubmitParams()
if (errorMessage) {
this.$message.error(errorMessage)
this.confirmLoading = false
return
}
if (this.authId) {
await putAuthById(this.authId, params)
this.$message.success(this.$t('editSuccess'))
} else {
await postAuth(params)
this.$message.success(this.$t('createSuccess'))
}
this.$emit('submit')
this.handleClose()
} catch (e) {
console.error('submit error:', e)
} finally {
this.confirmLoading = false
}
})
},
handleSubmitParams() {
const params = _.cloneDeep(this.form)
const errorMessage = ''
const accessTimeType = this.$refs.accessControlRef.getAccessTimeType()
switch (accessTimeType) {
case ACCESS_TIME_TYPE.TIME_TEMPLATE:
params.access_control.custom_time_ranges = undefined
params.access_control.timezone = undefined
if (!params?.access_control?.time_template?.template_id) {
params.access_control.time_template = undefined
}
break
case ACCESS_TIME_TYPE.CUSTOM_TIME:
params.access_control.time_template = undefined
break
default:
break
}
return {
params,
errorMessage
}
}
},
}
</script>
<style lang="less" scoped>
.auth-form {
.auth-form-title {
margin-bottom: 16px;
font-weight: 600;
}
/deep/ .ant-input-number {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,212 @@
<template>
<div>
<a-form-model-item :label="$t('oneterm.auth.folderSelect')" prop="node_selector">
<TypeRadio
idName="oneterm.folder"
:selectData="form.node_selector"
@change="(value) => handleFormChange(['node_selector'], value)"
>
<template #id>
<a-tree-select
:value="form.node_selector.values"
multiple
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:placeholder="$t('oneterm.auth.selectItemTip', { name: $t('oneterm.folder') })"
:tree-data="nodeSelectTreeData"
:load-data="onLoadData"
@change="(value) => handleFormChange(['node_selector', 'values'], value)"
/>
</template>
</TypeRadio>
<div class="exclude-row">
<div class="exclude-row-label">{{ $t('oneterm.auth.excludeItem', { name: $t('oneterm.folder') }) }}: </div>
<a-tree-select
:value="form.node_selector.exclude_ids"
multiple
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:placeholder="$t('oneterm.auth.excludeItemTip', { name: $t('oneterm.folder')})"
:tree-data="nodeSelectTreeData"
:load-data="onLoadData"
@change="(value) => handleFormChange(['node_selector', 'exclude_ids'], value)"
/>
</div>
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.auth.assetSelect')" prop="asset_selector">
<TypeRadio
idName="oneterm.asset"
:selectData="form.asset_selector"
@change="(value) => handleFormChange(['asset_selector'], value)"
>
<template #id>
<a-select
mode="multiple"
:value="form.asset_selector.values"
:options="assetSelectOptions"
:placeholder="$t('oneterm.auth.selectItemTip', { name: $t('oneterm.asset') })"
@change="(value) => handleFormChange(['asset_selector', 'values'], value)"
/>
</template>
</TypeRadio>
<div class="exclude-row">
<div class="exclude-row-label">{{ $t('oneterm.auth.excludeItem', { name: $t('oneterm.asset')}) }}: </div>
<a-select
mode="multiple"
:value="form.asset_selector.exclude_ids"
:options="assetSelectOptions"
:placeholder="$t('oneterm.auth.excludeItemTip', { name: $t('oneterm.asset')})"
@change="(value) => handleFormChange(['asset_selector', 'exclude_ids'], value)"
/>
</div>
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.auth.accountSelect')" prop="account_selector">
<TypeRadio
idName="oneterm.account"
:selectData="form.account_selector"
@change="(value) => handleFormChange(['account_selector'], value)"
>
<template #id>
<a-select
mode="multiple"
:value="form.account_selector.values"
:options="accountSelectOptions"
:placeholder="$t('oneterm.auth.selectItemTip', { name: $t('oneterm.account') })"
@change="(value) => handleFormChange(['account_selector', 'values'], value)"
/>
</template>
</TypeRadio>
<div class="exclude-row">
<div class="exclude-row-label">{{ $t('oneterm.auth.excludeItem', { name: $t('oneterm.account')}) }}: </div>
<a-select
mode="multiple"
:value="form.account_selector.exclude_ids"
:options="accountSelectOptions"
:placeholder="$t('oneterm.auth.excludeItemTip', { name: $t('oneterm.account')})"
@change="(value) => handleFormChange(['account_selector', 'exclude_ids'], value)"
/>
</div>
</a-form-model-item>
</div>
</template>
<script>
import { getAccountList } from '@/modules/oneterm/api/account'
import { getAssetList } from '@/modules/oneterm/api/asset'
import { getNodeList } from '@/modules/oneterm/api/node'
import TypeRadio from './typeRadio.vue'
export default {
name: 'TargetSelect',
components: {
TypeRadio
},
props: {
form: {
type: Object,
default: () => {}
}
},
data() {
return {
nodeSelectTreeData: [],
accountSelectOptions: [],
assetSelectOptions: []
}
},
mounted() {
this.getNodeList()
this.getAccountList()
this.getAssetList()
},
methods: {
getNodeList() {
getNodeList({
info: true,
parent_id: 0
}).then((res) => {
const list = res?.data?.list || []
this.nodeSelectTreeData = list.map((item) => ({
id: String(item.id),
value: String(item.id),
label: item.name,
isLeaf: !item.has_child,
}))
})
},
onLoadData(treeNode) {
return new Promise((resolve) => {
if (treeNode.dataRef.children) {
resolve()
return
}
getNodeList({
parent_id: treeNode.dataRef.id,
info: true
}).then((res) => {
treeNode.dataRef.children = (res?.data?.list ?? []).map((item) => ({
id: String(item.id),
value: String(item.id),
label: item.name,
isLeaf: !item.has_child,
}))
this.nodeSelectTreeData = [...this.nodeSelectTreeData]
resolve()
})
})
},
getAccountList() {
getAccountList({
page_index: 1,
page_size: 9999
}).then((res) => {
const list = res?.data?.list || []
this.accountSelectOptions = list.map((item) => ({
value: String(item.id),
label: item.name
}))
})
},
getAssetList() {
getAssetList({
page_index: 1,
page_size: 9999,
info: true
}).then((res) => {
const list = res?.data?.list || []
this.assetSelectOptions = list.map((item) => ({
value: String(item.id),
label: item.name
}))
})
},
handleFormChange(keys, value) {
this.$emit(
'change',
{
keys,
value
}
)
}
}
}
</script>
<style lang="less" scoped>
.exclude-row {
display: flex;
align-items: flex-start;
margin-top: 12px;
&-label {
line-height: 32px;
flex-shrink: 0;
margin-right: 12px;
}
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<div>
<a-radio-group
:value="selectData.type"
:options="radioOptions"
@change="handleRadioChange"
/>
<slot
v-if="selectData.type === TARGET_SELECT_TYPE.ID"
name="id"
></slot>
<a-input
v-else-if="selectData.type === TARGET_SELECT_TYPE.REGEX"
:value="selectData.values[0]"
:placeholder="$t('oneterm.auth.regexTip')"
@change="handleRegexInputChange"
></a-input>
<a-select
v-else-if="selectData.type === TARGET_SELECT_TYPE.TAG"
mode="tags"
:value="selectData.values"
:options="tagSelectOptions"
:placeholder="$t('oneterm.auth.tagsTip')"
@change="handleTagChange"
/>
</div>
</template>
<script>
import _ from 'lodash'
import { TARGET_SELECT_TYPE, TARGET_SELECT_TYPE_NAME } from '../../constants.js'
export default {
name: 'TypeRadio',
props: {
idName: {
type: String,
default: ''
},
selectData: {
type: Object,
default: () => {}
}
},
data() {
return {
TARGET_SELECT_TYPE,
TARGET_SELECT_TYPE_NAME,
tagSelectOptions: []
}
},
computed: {
radioOptions() {
return Object.values(TARGET_SELECT_TYPE).map((key) => {
return {
value: key,
label: key === TARGET_SELECT_TYPE.ID ? this.$t(TARGET_SELECT_TYPE_NAME[key], { name: this.$t(this.idName) }) : this.$t(TARGET_SELECT_TYPE_NAME[key])
}
})
}
},
methods: {
handleRadioChange(e) {
const type = e?.target?.value || TARGET_SELECT_TYPE.ALL
const values = []
switch (type) {
case TARGET_SELECT_TYPE.REGEX:
values.push('')
break
default:
break
}
this.$emit('change', {
type,
values,
exclude_ids: this.selectData.exclude_ids
})
},
handleRegexInputChange(e) {
const value = e?.target?.value
this.$emit('change', {
type: this.selectData.type,
exclude_ids: this.selectData.exclude_ids,
values: [value],
})
},
handleTagChange(value) {
this.$emit('change', {
type: this.selectData.type,
exclude_ids: this.selectData.exclude_ids,
values: value,
})
const tagSelectOptions = this.tagSelectOptions.concat(value.map((item) => ({
label: item,
value: item
})))
this.tagSelectOptions = _.uniqBy(tagSelectOptions, 'value')
}
}
}
</script>

View File

@@ -0,0 +1,13 @@
export const TARGET_SELECT_TYPE = {
ALL: 'all',
ID: 'ids',
REGEX: 'regex',
TAG: 'tags'
}
export const TARGET_SELECT_TYPE_NAME = {
[TARGET_SELECT_TYPE.ALL]: 'oneterm.auth.all',
[TARGET_SELECT_TYPE.ID]: 'oneterm.auth.selectItem',
[TARGET_SELECT_TYPE.REGEX]: 'oneterm.auth.regex',
[TARGET_SELECT_TYPE.TAG]: 'oneterm.auth.tags',
}

View File

@@ -0,0 +1,405 @@
<template>
<div class="access-auth">
<a-spin :tip="loadTip" :spinning="loading">
<div class="access-auth-header">
<a-space>
<a-input-search
v-model="searchValue"
allow-clear
:placeholder="$t('placeholderSearch')"
:style="{ width: '250px' }"
class="ops-input ops-input-radius"
@search="updateTableData()"
/>
<div class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
<span @click="batchDelete">{{ $t('delete') }}</span>
<span @click="batchChangeEnabled(true)">{{ $t('oneterm.enabled') }}</span>
<span @click="batchChangeEnabled(false)">{{ $t('oneterm.disabled') }}</span>
<span>{{ $t('selectRows', { rows: selectedRowKeys.length }) }}</span>
</div>
</a-space>
<a-space>
<a-button type="primary" @click="openAuthDrawer(null)">{{ $t(`create`) }}</a-button>
<a-button @click="updateTableData()">{{ $t(`refresh`) }}</a-button>
</a-space>
</div>
<ops-table
size="small"
ref="opsTable"
class="ops-stripe-table"
stripe
show-overflow
show-header-overflow
resizable
:data="tableData"
:checkbox-config="{ reserve: true, highlight: true, range: true }"
:row-config="{ keyField: 'id', height: '80px' }"
:column-config="{ width: 200 }"
:height="tableHeight"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
@checkbox-range-end="onSelectRangeEnd"
>
<vxe-column type="checkbox" width="60px"></vxe-column>
<vxe-column :title="$t('name')" field="name"></vxe-column>
<vxe-column :title="$t('description')" field="description" width="auto" min-width="200"></vxe-column>
<vxe-column :title="$t('oneterm.isEnable')" field="enabled" width="100">
<template #default="{row}">
<EnabledStatus
:status="Boolean(row.enabled)"
@change="changeIsEnabled(row)"
/>
</template>
</vxe-column>
<vxe-column :title="$t('created_at')" field="created_at">
<template #default="{row}">
{{ row.createdTimeText }}
</template>
</vxe-column>
<vxe-column :title="$t('oneterm.auth.targetSelect')" field="targetSelect">
<template #default="{row}">
<a-tooltip
v-for="(item, index) in row.targetSelect"
:key="index"
:title="item"
placement="topLeft"
>
<div class="access-auth-target-select">
{{ item }}
</div>
</a-tooltip>
</template>
</vxe-column>
<vxe-column :title="$t('oneterm.accessControl.permissionConfig')" field="permissions" width="170">
<template #default="{row}">
<div class="access-auth-permisson">
<span
v-for="(item) in permissionConfigKeys"
:key="item"
>
{{ $t(PERMISSION_TYPE_NAME[item]) }}
<a-icon
v-if="row.permissions && row.permissions[item]"
type="check-square"
style="color: #00b42a"
/>
<a-icon
v-else
type="close-square"
style="color: #fd4c6a"
/>
</span>
</div>
</template>
</vxe-column>
<vxe-column :title="$t('oneterm.auth.validTime')" field="validTime">
<template #default="{row}">
<div
v-for="(item) in row.validTime"
:key="item"
>
{{ item }}
</div>
</template>
</vxe-column>
<vxe-column :title="$t('operation')" width="100" fixed="right">
<template #default="{row}">
<a-space>
<a @click="openAuthDrawer(row)"><ops-icon type="icon-xianxing-edit"/></a>
<a @click="copyAuth(row)"><ops-icon type="veops-copy"/></a>
<a-popconfirm :title="$t('confirmDelete')" @confirm="deleteAuth(row)">
<a style="color:red"><ops-icon type="icon-xianxing-delete"/></a>
</a-popconfirm>
</a-space>
</template>
</vxe-column>
</ops-table>
<div class="access-auth-pagination">
<a-pagination
size="small"
show-size-changer
:current="currentPage"
:total="totalResult"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
:page-size="pageSize"
:default-current="1"
@change="pageOrSizeChange"
@showSizeChange="pageOrSizeChange"
/>
</div>
</a-spin>
<AuthDrawer ref="authDrawerRef" @submit="updateTableData()" />
</div>
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import { mapState } from 'vuex'
import { getAuthList, deleteAuthById, putAuthById } from '@/modules/oneterm/api/authorizationV2.js'
import { TARGET_SELECT_TYPE } from './constants.js'
import { PERMISSION_TYPE_NAME, PERMISSION_TYPE } from '@/modules/oneterm/views/systemSettings/accessControl/constants.js'
import AuthDrawer from './authDrawer/index.vue'
import EnabledStatus from '@/modules/oneterm/components/enabledStatus/index.vue'
export default {
name: 'AccessAuth',
components: {
AuthDrawer,
EnabledStatus
},
data() {
return {
PERMISSION_TYPE_NAME,
searchValue: '',
tableData: [],
currentPage: 1,
pageSize: 20,
totalResult: 0,
selectedRowKeys: [],
loading: false,
loadTip: '',
permissionConfigKeys: Object.values(PERMISSION_TYPE),
authRawKeys: [
'access_control',
'account_selector',
'node_selector',
'asset_selector',
'description',
'enabled',
'name',
'permissions',
'rids',
'valid_from',
'valid_to'
]
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
tableHeight() {
return this.windowHeight - 204
}
},
mounted() {
this.updateTableData()
},
methods: {
updateTableData() {
this.loading = true
getAuthList({
page_index: this.currentPage,
page_size: this.pageSize,
search: this.searchValue
})
.then((res) => {
const tableData = res?.data?.list || []
tableData.forEach((row) => {
row.createdTimeText = moment(row.created_at).format('YYYY-MM-DD HH:mm:ss')
if (row.valid_from && row.valid_to) {
row.validTime = [
`${this.$t('oneterm.auth.start')}: ${row.valid_from}`,
`${this.$t('oneterm.auth.end')}: ${row.valid_to}`
]
} else {
row.validTime = [
this.$t('oneterm.auth.permanentValidity')
]
}
row.targetSelect = [
`${this.$t('oneterm.node')}: ${this.handleTargetSelectText(row.node_selector)}`,
`${this.$t('oneterm.asset')}: ${this.handleTargetSelectText(row.asset_selector)}`,
`${this.$t('oneterm.account')}: ${this.handleTargetSelectText(row.account_selector)}`
]
})
this.tableData = tableData
this.totalResult = res?.data?.count ?? 0
})
.finally(() => {
this.loading = false
})
},
handleTargetSelectText(data) {
const values = data?.values || []
switch (data.type) {
case TARGET_SELECT_TYPE.ALL:
return this.$t('oneterm.auth.all')
case TARGET_SELECT_TYPE.ID:
return this.$t('oneterm.auth.select', { count: values.length })
case TARGET_SELECT_TYPE.REGEX:
return `${this.$t('oneterm.auth.regex')} (${values.join(', ')})`
case TARGET_SELECT_TYPE.TAG:
return `${this.$t('oneterm.auth.tag')} (${values.join(', ')})`
default:
return ''
}
},
onSelectChange() {
const opsTable = this.$refs.opsTable.getVxetableRef()
const records = [...opsTable.getCheckboxRecords(), ...opsTable.getCheckboxReserveRecords()]
this.selectedRowKeys = records.map((i) => i.id)
},
onSelectRangeEnd({ records }) {
this.selectedRowKeys = records.map((i) => i.id)
},
pageOrSizeChange(currentPage, pageSize) {
this.currentPage = currentPage
this.pageSize = pageSize
this.updateTableData()
},
openAuthDrawer(data) {
this.$refs.authDrawerRef.open(data)
},
deleteAuth(row) {
this.loading = true
deleteAuthById(row.id)
.then((res) => {
this.$message.success(this.$t('deleteSuccess'))
this.updateTableData()
})
.finally(() => {
this.loading = false
})
},
copyAuth(row) {
const data = _.omit(_.cloneDeep(row), 'id')
data.name += '-copy'
this.$refs.authDrawerRef.open(data)
},
async batchDelete() {
this.$confirm({
title: this.$t('warning'),
content: this.$t('confirmDelete'),
onOk: async () => {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = `${this.$t('deleting')}...`
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await deleteAuthById(this.selectedRowKeys[i])
.then(() => {
successNum += 1
})
.catch(() => {
errorNum += 1
})
.finally(() => {
this.loadTip = this.$t('deletingTip', { total: this.selectedRowKeys.length, successNum, errorNum })
})
}
this.afterBatch()
},
})
},
batchChangeEnabled(enabled) {
this.$confirm({
title: this.$t('warning'),
content: this.$t('oneterm.confirmEnable'),
onOk: async () => {
const opsTable = this.$refs.opsTable.getVxetableRef()
const records = [...opsTable.getCheckboxRecords(), ...opsTable.getCheckboxReserveRecords()]
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = `${this.$t('oneterm.switching')}...`
for (let i = 0; i < records.length; i++) {
const params = _.pick(_.cloneDeep(records[i]), this.authRawKeys)
params.enabled = enabled
await putAuthById(records[i].id, params)
.then(() => {
successNum += 1
})
.catch(() => {
errorNum += 1
})
.finally(() => {
this.loadTip = this.$t('oneterm.switchingTip', { total: records.length, successNum, errorNum })
})
}
this.afterBatch()
},
})
},
afterBatch() {
this.loading = false
this.loadTip = ''
this.selectedRowKeys = []
this.$refs.opsTable.getVxetableRef().clearCheckboxRow()
this.$refs.opsTable.getVxetableRef().clearCheckboxReserve()
this.$nextTick(() => {
this.updateTableData()
})
},
changeIsEnabled(row) {
const params = _.pick(_.cloneDeep(row), this.authRawKeys)
params.enabled = !params.enabled
putAuthById(row.id, params).then(() => {
this.$message.success(this.$t('editSuccess'))
this.updateTableData()
})
}
},
}
</script>
<style lang="less" scoped>
.access-auth {
background-color: #fff;
height: 100%;
border-radius: 6px;
padding: 18px;
/deep/ .vxe-body--row {
height: 80px;
}
/deep/ .vxe-cell {
max-height: max-content !important;
}
&-header {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
&-target-select {
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
&-permisson {
display: flex;
flex-wrap: wrap;
column-gap: 8px;
& > span {
width: 45%;
}
}
&-pagination {
text-align: right;
margin-top: 8px;
}
}
</style>

View File

@@ -0,0 +1,147 @@
<template>
<a-modal
:title="title"
:visible="visible"
:confirmLoading="confirmLoading"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form-model
ref="commandFormRef"
:model="form"
:rules="rules"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 16 }"
>
<a-form-model-item :label="$t('name')" prop="name">
<a-input v-model="form.name" :placeholder="$t('placeholder1')" />
</a-form-model-item>
<a-form-model-item :label="$t('description')" prop="description">
<a-input v-model="form.description" :placeholder="$t('placeholder1')" />
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.command')" prop="cmd">
<a-input v-model="form.cmd" :placeholder="$t('placeholder1')" />
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.commandFilter.riskLevel')" prop="risk_level">
<a-select
v-model="form.risk_level"
:placeholder="$t('placeholder2')"
:options="rishLevelSelectOptions"
/>
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.commandFilter.category')" prop="category">
<a-select
v-model="form.category"
:placeholder="$t('placeholder2')"
:options="categorySelectOptions"
/>
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.isEnable')" prop="enable">
<a-switch v-model="form.enable" />
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.commandFilter.regexp')" prop="is_re">
<a-switch v-model="form.is_re" />
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import { COMMAND_CATEGORY, COMMAND_CATEGORY_NAME, COMMAND_RISK_NAME } from '../constants.js'
import { postCommand, putCommandById } from '@/modules/oneterm/api/command'
const DEFAULT_FORM = {
name: '',
description: '',
cmd: '',
risk_level: 0,
category: COMMAND_CATEGORY.SECURITY,
enable: true,
is_re: true
}
export default {
name: 'CommandModal',
data() {
return {
visible: false,
commandId: '',
form: { ...DEFAULT_FORM },
rules: {
name: [{ required: true, message: this.$t('placeholder1') }],
},
confirmLoading: false
}
},
computed: {
title() {
if (this.commandId) {
return this.$t('oneterm.commandFilter.editCommand')
}
return this.$t('oneterm.commandFilter.createCommand')
},
categorySelectOptions() {
return Object.values(COMMAND_CATEGORY).map((value) => {
return {
value,
label: this.$t(COMMAND_CATEGORY_NAME[value])
}
})
},
rishLevelSelectOptions() {
return [0, 1, 2, 3].map((value) => {
return {
value,
label: this.$t(COMMAND_RISK_NAME[value])
}
})
}
},
methods: {
open(data) {
this.visible = true
if (data) {
this.form = {
name: data?.name ?? '',
description: data?.description ?? '',
cmd: data.cmd ?? '',
category: data?.category ?? COMMAND_CATEGORY.SECURITY,
risk_level: data?.risk_level ?? 0,
enable: Boolean(data.enable),
is_re: Boolean(data.is_re)
}
this.commandId = data?.id ?? ''
}
},
handleCancel() {
this.form = { ...DEFAULT_FORM }
this.commandId = ''
this.$refs.commandFormRef.resetFields()
this.visible = false
},
async handleOk() {
this.$refs.commandFormRef.validate(async (valid) => {
if (!valid) return
this.confirmLoading = true
try {
if (this.commandId) {
await putCommandById(this.commandId, { ...this.form })
this.$message.success(this.$t('editSuccess'))
} else {
await postCommand({ ...this.form })
this.$message.success(this.$t('createSuccess'))
}
this.$emit('submit')
this.handleCancel()
} catch (e) {
console.error('submit error:', e)
} finally {
this.confirmLoading = false
}
})
},
},
}
</script>

View File

@@ -0,0 +1,343 @@
<template>
<div class="command-management">
<a-spin :tip="loadTip" :spinning="loading">
<div class="command-management-header">
<a-space>
<a-input-search
v-model="searchValue"
allow-clear
:placeholder="$t('placeholderSearch')"
:style="{ width: '250px' }"
class="ops-input ops-input-radius"
@search="updateTableData()"
/>
<div class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
<span @click="batchDelete">{{ $t('delete') }}</span>
<span @click="batchChangeEnabled(true)">{{ $t('oneterm.enabled') }}</span>
<span @click="batchChangeEnabled(false)">{{ $t('oneterm.disabled') }}</span>
<span>{{ $t('selectRows', { rows: selectedRowKeys.length }) }}</span>
</div>
</a-space>
<a-space>
<a-button type="primary" @click="openModal(null)">{{ $t('create') }}</a-button>
<a-button @click="updateTableData()">{{ $t('refresh') }}</a-button>
</a-space>
</div>
<ops-table
size="small"
ref="opsTable"
class="ops-stripe-table"
stripe
show-overflow
show-header-overflow
resizable
:data="tableData"
:checkbox-config="{ reserve: true, highlight: true, range: true }"
:row-config="{ keyField: 'id' }"
:height="tableHeight"
:filter-config="{ remote: true }"
@filter-change="handleFilterChange"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
@checkbox-range-end="onSelectRangeEnd"
>
<vxe-column type="checkbox" width="60px"></vxe-column>
<vxe-column :title="$t('oneterm.name')" field="name"></vxe-column>
<vxe-column :title="$t('oneterm.command')" field="cmd"></vxe-column>
<vxe-column
:title="$t('oneterm.commandFilter.riskLevel')"
field="risk_level"
:filters="riskLevelFilters"
:filter-multiple="false"
>
<template #default="{row}">
<RiskDisplay :type="row.risk_level" />
</template>
</vxe-column>
<vxe-column
:title="$t('oneterm.commandFilter.category')"
field="category"
:filters="categoryFilters"
:filter-multiple="false"
>
<template #default="{row}">
{{ $t(row.categoryName) }}
</template>
</vxe-column>
<vxe-column :title="$t('oneterm.isEnable')" field="enable">
<template #default="{row}">
<EnabledStatus
:status="Boolean(row.enable)"
@change="changeEnable(row)"
/>
</template>
</vxe-column>
<vxe-column :title="$t('created_at')" width="170">
<template #default="{row}">
{{ row.createdTimeText }}
</template>
</vxe-column>
<vxe-column :title="$t('operation')" width="100">
<template #default="{row}">
<a-space>
<a @click="openModal(row)"><ops-icon type="icon-xianxing-edit"/></a>
<a-popconfirm :title="$t('confirmDelete')" @confirm="deleteCommand(row)">
<a style="color:red"><ops-icon type="icon-xianxing-delete"/></a>
</a-popconfirm>
</a-space>
</template>
</vxe-column>
</ops-table>
<div class="command-management-pagination">
<a-pagination
size="small"
show-size-changer
:current="currentPage"
:total="totalResult"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
:page-size="pageSize"
:default-current="1"
@change="pageOrSizeChange"
@showSizeChange="pageOrSizeChange"
/>
</div>
</a-spin>
<CommandModal ref="commandModal" @submit="updateTableData()" />
</div>
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import { mapState } from 'vuex'
import { getCommandList, deleteCommandById, putCommandById } from '@/modules/oneterm/api/command.js'
import { COMMAND_CATEGORY, COMMAND_CATEGORY_NAME, COMMAND_RISK_NAME } from '../constants.js'
import CommandModal from './commandModal.vue'
import RiskDisplay from './riskDisplay.vue'
import EnabledStatus from '@/modules/oneterm/components/enabledStatus/index.vue'
export default {
name: 'CommandManagement',
components: {
CommandModal,
RiskDisplay,
EnabledStatus
},
data() {
return {
searchValue: '',
currentRiskLevel: [],
currentCategory: [],
tableData: [],
currentPage: 1,
pageSize: 20,
totalResult: 0,
selectedRowKeys: [],
loading: false,
loadTip: '',
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
tableHeight() {
return this.windowHeight - 254
},
riskLevelFilters() {
return [0, 1, 2, 3].map((value) => {
return {
value,
label: this.$t(COMMAND_RISK_NAME[value])
}
})
},
categoryFilters() {
return Object.values(COMMAND_CATEGORY).map((value) => {
return {
value,
label: this.$t(COMMAND_CATEGORY_NAME[value])
}
})
},
},
mounted() {
this.updateTableData()
},
methods: {
updateTableData() {
this.loading = true
const risk_level = this?.currentRiskLevel?.length ? this.currentRiskLevel.join(',') : undefined
const category = this?.currentCategory?.length ? this.currentCategory.join(',') : undefined
getCommandList({
page_index: this.currentPage,
page_size: this.pageSize,
search: this.searchValue,
risk_level,
category
})
.then((res) => {
const tableData = res?.data?.list || []
tableData.forEach((row) => {
row.categoryName = COMMAND_CATEGORY_NAME?.[row.category] ?? '-'
row.createdTimeText = moment(row.created_at).format('YYYY-MM-DD HH:mm:ss')
})
this.tableData = tableData
this.totalResult = res?.data?.count ?? 0
})
.finally(() => {
this.loading = false
})
},
onSelectChange() {
const opsTable = this.$refs.opsTable.getVxetableRef()
const records = [...opsTable.getCheckboxRecords(), ...opsTable.getCheckboxReserveRecords()]
this.selectedRowKeys = records.map((i) => i.id)
},
onSelectRangeEnd({ records }) {
this.selectedRowKeys = records.map((i) => i.id)
},
pageOrSizeChange(currentPage, pageSize) {
this.currentPage = currentPage
this.pageSize = pageSize
this.updateTableData()
},
openModal(data) {
this.$refs.commandModal.open(data)
},
deleteCommand(row) {
this.loading = true
deleteCommandById(row.id)
.then((res) => {
this.$message.success(this.$t('deleteSuccess'))
this.updateTableData()
})
.finally(() => {
this.loading = false
})
},
async batchDelete() {
this.$confirm({
title: this.$t('warning'),
content: this.$t('confirmDelete'),
onOk: async () => {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = `${this.$t('deleting')}...`
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await deleteCommandById(this.selectedRowKeys[i])
.then(() => {
successNum += 1
})
.catch(() => {
errorNum += 1
})
.finally(() => {
this.loadTip = this.$t('deletingTip', { total: this.selectedRowKeys.length, successNum, errorNum })
})
}
this.afterBatch()
},
})
},
batchChangeEnabled(enabled) {
this.$confirm({
title: this.$t('warning'),
content: this.$t('oneterm.confirmEnable'),
onOk: async () => {
const opsTable = this.$refs.opsTable.getVxetableRef()
const records = [...opsTable.getCheckboxRecords(), ...opsTable.getCheckboxReserveRecords()]
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = `${this.$t('oneterm.switching')}...`
for (let i = 0; i < records.length; i++) {
const params = _.omit(_.cloneDeep(records[i]), ['categoryName', 'createdTimeText', 'id'])
params.enable = enabled
await putCommandById(records[i].id, params)
.then(() => {
successNum += 1
})
.catch(() => {
errorNum += 1
})
.finally(() => {
this.loadTip = this.$t('oneterm.switchingTip', { total: records.length, successNum, errorNum })
})
}
this.afterBatch()
},
})
},
afterBatch() {
this.loading = false
this.loadTip = ''
this.selectedRowKeys = []
this.$refs.opsTable.getVxetableRef().clearCheckboxRow()
this.$refs.opsTable.getVxetableRef().clearCheckboxReserve()
this.$nextTick(() => {
this.updateTableData()
})
},
changeEnable(row) {
const params = _.omit(_.cloneDeep(row), ['categoryName', 'createdTimeText', 'id'])
params.enable = !params.enable
putCommandById(
row.id,
params
).then(() => {
this.$message.success(this.$t('editSuccess'))
this.updateTableData()
})
},
handleFilterChange(e) {
switch (e.field) {
case 'risk_level':
this.currentRiskLevel = e?.values
this.updateTableData()
break
case 'category':
this.currentCategory = e?.values
this.updateTableData()
break
default:
break
}
}
},
}
</script>
<style lang="less" scoped>
.command-management {
background-color: #fff;
height: 100%;
border-radius: 6px;
padding: 18px;
&-header {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
&-pagination {
text-align: right;
margin-top: 8px;
}
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<span class="risk">
<span
class="risk-status"
:style="{
backgroundColor: color + '22',
'--innerBackgroundColor': color
}"
></span>
<span class="risk-text">
{{ $t(text) }}
</span>
</span>
</template>
<script>
import { COMMAND_RISK_NAME } from '../constants.js'
export default {
name: 'RiskDisplay',
props: {
type: {
type: Number,
default: 0
}
},
data() {
return {
COMMAND_RISK_COLOR: {
0: '#52c41a',
1: '#faad14',
2: '#ff4d4f',
3: '#722323'
}
}
},
computed: {
text() {
return COMMAND_RISK_NAME?.[this.type]
},
color() {
return this.COMMAND_RISK_COLOR?.[this.type]
}
}
}
</script>
<style lang="less" scoped>
.risk {
display: inline-flex;
align-items: center;
&-status {
width: 14px;
height: 14px;
border-radius: 50%;
position: relative;
margin-right: 4px;
&::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 8px;
border-radius: 50%;
margin-top: -4px;
margin-left: -4px;
background-color: var(--innerBackgroundColor);
}
}
}
</style>

View File

@@ -0,0 +1,164 @@
<template>
<a-modal
:title="title"
:visible="visible"
:confirmLoading="confirmLoading"
:width="800"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form-model
ref="commandTemplateForm"
:model="form"
:rules="rules"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 16 }"
>
<a-form-model-item :label="$t('name')" prop="name">
<a-input v-model="form.name" :placeholder="$t(`placeholder1`)"/>
</a-form-model-item>
<a-form-model-item :label="$t('description')" prop="description">
<a-input v-model="form.description" :placeholder="$t('placeholder1')"/>
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.commandFilter.category')" prop="category">
<a-select
v-model="form.category"
:placeholder="$t('placeholder2')"
:options="categorySelectOptions"
/>
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.commandFilter.selectCommand')" prop="cmd_ids">
<a-transfer
:data-source="allCommand"
:target-keys="form.cmd_ids"
:selected-keys="transferSelectedKeys"
:render="item => item.title"
:titles="[$t('oneterm.commandFilter.unselectCommand'), $t('oneterm.commandFilter.selectedCommand')]"
:listStyle="{
width: 'calc((100% - 40px) / 2)',
height: '300px'
}"
@change="handleTransferChange"
@selectChange="handleTransferSelectChange"
/>
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import { COMMAND_CATEGORY, COMMAND_CATEGORY_NAME } from '../constants.js'
import { postCommandTemplate, putCommandTemplateById } from '@/modules/oneterm/api/commandTemplate.js'
import { getCommandList } from '@/modules/oneterm/api/command.js'
const DEFAULT_FORM = {
name: '',
description: '',
category: COMMAND_CATEGORY.SECURITY,
cmd_ids: [],
}
export default {
name: 'CommandTemplateModal',
data() {
return {
visible: false,
commandTemplateId: '',
form: { ...DEFAULT_FORM },
rules: {
name: [{ required: true, message: this.$t(`placeholder1`) }],
cmd_ids: [{ required: true, message: this.$t(`placeholder2`) }],
},
allCommand: [],
transferSelectedKeys: [],
confirmLoading: false
}
},
computed: {
title() {
if (this.commandTemplateId) {
return this.$t('oneterm.commandFilter.editCommandTemplate')
}
return this.$t('oneterm.commandFilter.createCommandTemplate')
},
categorySelectOptions() {
return Object.values(COMMAND_CATEGORY).map((value) => {
return {
value,
label: this.$t(COMMAND_CATEGORY_NAME[value])
}
})
}
},
methods: {
open(data) {
this.visible = true
if (data) {
this.form = {
name: data?.name ?? '',
description: data?.description ?? '',
category: data?.category ?? COMMAND_CATEGORY.SECURITY,
cmd_ids: (data?.cmd_ids ?? []).map((id) => String(id))
}
this.commandTemplateId = data?.id ?? ''
}
this.getAllCommand()
},
async getAllCommand() {
const res = await getCommandList({
page_index: 1,
page_size: 9999,
})
const allCommand = res?.data?.list || []
this.allCommand = allCommand.map((item) => {
return {
key: String(item.id),
title: item.name
}
})
this.form.cmd_ids = this.form.cmd_ids.filter((id) => this.allCommand.some((command) => command?.key === id))
},
handleCancel() {
this.form = { ...DEFAULT_FORM }
this.allCommand = []
this.transferSelectedKeys = []
this.commandTemplateId = ''
this.$refs.commandTemplateForm.resetFields()
this.visible = false
},
handleTransferChange(nextTargetKeys) {
this.form.cmd_ids = nextTargetKeys
},
handleTransferSelectChange(sourceSelectedKeys, targetSelectedKeys) {
this.transferSelectedKeys = [...sourceSelectedKeys, ...targetSelectedKeys]
},
async handleOk() {
this.$refs.commandTemplateForm.validate(async (valid) => {
if (!valid) return
this.confirmLoading = true
try {
const params = {
...this.form,
cmd_ids: this.form.cmd_ids.map((id) => Number(id))
}
if (this.commandTemplateId) {
await putCommandTemplateById(this.commandTemplateId, params)
this.$message.success(this.$t('editSuccess'))
} else {
await postCommandTemplate(params)
this.$message.success(this.$t('createSuccess'))
}
this.$emit('submit')
this.handleCancel()
} catch (e) {
console.error('submit error:', e)
} finally {
this.confirmLoading = false
}
})
},
},
}
</script>

View File

@@ -1,67 +1,79 @@
<template>
<div class="command-intercept">
<div class="command-template-management">
<a-spin :tip="loadTip" :spinning="loading">
<div class="command-intercept-header">
<div class="command-template-management-header">
<a-space>
<a-input-search
allow-clear
v-model="filterName"
v-model="searchValue"
:placeholder="$t('placeholderSearch')"
:style="{ width: '250px' }"
class="ops-input ops-input-radius"
:placeholder="$t('placeholderSearch')"
allow-clear
@search="updateTableData()"
/>
<div class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
<span @click="batchDelete">{{ $t(`delete`) }}</span>
<span @click="batchDelete">{{ $t('delete') }}</span>
<span>{{ $t('selectRows', { rows: selectedRowKeys.length }) }}</span>
</div>
</a-space>
<a-space>
<a-button type="primary" @click="openModal(null)">{{ $t(`create`) }}</a-button>
<a-button @click="updateTableData()">{{ $t(`refresh`) }}</a-button>
<a-button type="primary" @click="openModal(null)">{{ $t('create') }}</a-button>
<a-button @click="updateTableData()">{{ $t('refresh') }}</a-button>
</a-space>
</div>
<ops-table
size="small"
ref="opsTable"
stripe
class="ops-stripe-table"
:data="tableData"
stripe
show-overflow
show-header-overflow
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
@checkbox-range-end="onSelectRangeEnd"
resizable
:data="tableData"
:checkbox-config="{ reserve: true, highlight: true, range: true }"
:row-config="{ keyField: 'id' }"
:height="tableHeight"
resizable
:filter-config="{ remote: true }"
@filter-change="handleFilterChange"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
@checkbox-range-end="onSelectRangeEnd"
>
<vxe-column type="checkbox" width="60px"></vxe-column>
<vxe-column :title="$t(`oneterm.name`)" field="name"></vxe-column>
<vxe-column :title="$t(`oneterm.command`)" field="cmd"></vxe-column>
<vxe-column :title="$t(`oneterm.commandIntercept.enable`)" field="enable">
<vxe-column :title="$t('name')" field="name"></vxe-column>
<vxe-column :title="$t('description')" field="description"></vxe-column>
<vxe-column :title="$t('oneterm.commandFilter.commandCount')" field="description">
<template #default="{row}">
<a-switch :checked="Boolean(row.enable)" @change="changeEnable(row)" />
{{ row.cmd_ids.length }}
</template>
</vxe-column>
<vxe-column :title="$t(`created_at`)" width="120">
<vxe-column
:title="$t('oneterm.commandFilter.category')"
field="category"
:filters="categoryFilters"
:filter-multiple="false"
>
<template #default="{row}">
{{ moment(row.created_at).format('YYYY-MM-DD') }}
{{ $t(row.categoryName) }}
</template>
</vxe-column>
<vxe-column :title="$t(`operation`)" width="100">
<vxe-column :title="$t('created_at')" width="170">
<template #default="{row}">
{{ row.createdTimeText }}
</template>
</vxe-column>
<vxe-column :title="$t('operation')" width="100">
<template #default="{row}">
<a-space>
<a @click="openModal(row)"><ops-icon type="icon-xianxing-edit"/></a>
<a-popconfirm :title="$t('confirmDelete')" @confirm="deleteCommand(row)">
<a-popconfirm :title="$t('confirmDelete')" @confirm="deleteCommandTemplate(row)">
<a style="color:red"><ops-icon type="icon-xianxing-delete"/></a>
</a-popconfirm>
</a-space>
</template>
</vxe-column>
</ops-table>
<div class="command-intercept-pagination">
<div class="command-template-management-pagination">
<a-pagination
size="small"
show-size-changer
@@ -82,23 +94,26 @@
/>
</div>
</a-spin>
<CommandModal ref="commandModal" @submit="updateTableData()" />
<CommandTemplateModal ref="commandTemplateModal" @submit="updateTableData()" />
</div>
</template>
<script>
import moment from 'moment'
import { mapState } from 'vuex'
import { getCommandList, deleteCommandById, putCommandById } from '@/modules/oneterm/api/command.js'
import { getCommandTemplateList, deleteCommandTemplateById } from '@/modules/oneterm/api/commandTemplate.js'
import { COMMAND_CATEGORY, COMMAND_CATEGORY_NAME } from '../constants.js'
import CommandModal from './commandModal.vue'
import CommandTemplateModal from './commandTemplateModal.vue'
export default {
name: 'CommandIntercept',
components: { CommandModal },
name: 'CommandTemplateManagement',
components: { CommandTemplateModal },
data() {
return {
filterName: '',
searchValue: '',
currentCategory: [],
tableData: [],
currentPage: 1,
pageSize: 20,
@@ -113,23 +128,38 @@ export default {
windowHeight: (state) => state.windowHeight,
}),
tableHeight() {
return this.windowHeight - 207
return this.windowHeight - 254
},
categoryFilters() {
return Object.values(COMMAND_CATEGORY).map((value) => {
return {
value,
label: this.$t(COMMAND_CATEGORY_NAME[value])
}
})
},
},
mounted() {
this.updateTableData()
},
methods: {
moment,
updateTableData() {
this.loading = true
getCommandList({
const category = this?.currentCategory?.length ? this.currentCategory.join(',') : undefined
getCommandTemplateList({
page_index: this.currentPage,
page_size: this.pageSize,
search: this.filterName,
search: this.searchValue,
category
})
.then((res) => {
this.tableData = res?.data?.list || []
const tableData = res?.data?.list || []
tableData.forEach((row) => {
row.categoryName = COMMAND_CATEGORY_NAME?.[row.category] ?? '-'
row.createdTimeText = moment(row.created_at).format('YYYY-MM-DD HH:mm:ss')
})
this.tableData = tableData
this.totalResult = res?.data?.count ?? 0
})
.finally(() => {
@@ -150,11 +180,11 @@ export default {
this.updateTableData()
},
openModal(data) {
this.$refs.commandModal.open(data)
this.$refs.commandTemplateModal.open(data)
},
deleteCommand(row) {
deleteCommandTemplate(row) {
this.loading = true
deleteCommandById(row.id)
deleteCommandTemplateById(row.id)
.then((res) => {
this.$message.success(this.$t('deleteSuccess'))
this.updateTableData()
@@ -164,17 +194,16 @@ export default {
})
},
async batchDelete() {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('confirmDelete'),
async onOk() {
title: this.$t('warning'),
content: this.$t('confirmDelete'),
onOk: async () => {
let successNum = 0
let errorNum = 0
that.loading = true
that.loadTip = `${that.$t('deleting')}...`
for (let i = 0; i < that.selectedRowKeys.length; i++) {
await deleteCommandById(that.selectedRowKeys[i], false)
this.loading = true
this.loadTip = `${this.$t('deleting')}...`
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await deleteCommandTemplateById(this.selectedRowKeys[i])
.then(() => {
successNum += 1
})
@@ -182,32 +211,36 @@ export default {
errorNum += 1
})
.finally(() => {
that.loadTip = that.$t('deletingTip', { total: that.selectedRowKeys.length, successNum, errorNum })
this.loadTip = this.$t('deletingTip', { total: this.selectedRowKeys.length, successNum, errorNum })
})
}
that.loading = false
that.loadTip = ''
that.selectedRowKeys = []
that.$refs.opsTable.getVxetableRef().clearCheckboxRow()
that.$refs.opsTable.getVxetableRef().clearCheckboxReserve()
that.$nextTick(() => {
that.updateTableData()
this.loading = false
this.loadTip = ''
this.selectedRowKeys = []
this.$refs.opsTable.getVxetableRef().clearCheckboxRow()
this.$refs.opsTable.getVxetableRef().clearCheckboxReserve()
this.$nextTick(() => {
this.updateTableData()
})
},
})
},
changeEnable(row) {
putCommandById(row.id, { ...row, enable: Boolean(!row.enable) }).then(() => {
this.$message.success(this.$t('editSuccess'))
this.updateTableData()
})
},
handleFilterChange(e) {
switch (e.field) {
case 'category':
this.currentCategory = e?.values
this.updateTableData()
break
default:
break
}
}
},
}
</script>
<style lang="less" scoped>
.command-intercept {
.command-template-management {
background-color: #fff;
height: 100%;
border-radius: 6px;

View File

@@ -0,0 +1,26 @@
export const COMMAND_CATEGORY = {
SECURITY: 'security',
SYSTEM: 'system',
DATABASE: 'database',
NETWORK: 'network',
FILE: 'file',
DEVELOPER: 'developer',
CUSTOM: 'custom'
}
export const COMMAND_CATEGORY_NAME = {
[COMMAND_CATEGORY.SECURITY]: 'oneterm.commandFilter.securityRelated',
[COMMAND_CATEGORY.SYSTEM]: 'oneterm.commandFilter.systemOperations',
[COMMAND_CATEGORY.DATABASE]: 'oneterm.commandFilter.databaseOperations',
[COMMAND_CATEGORY.NETWORK]: 'oneterm.commandFilter.networkOperations',
[COMMAND_CATEGORY.FILE]: 'oneterm.commandFilter.fileOperations',
[COMMAND_CATEGORY.DEVELOPER]: 'oneterm.commandFilter.developmentRelated',
[COMMAND_CATEGORY.CUSTOM]: 'other'
}
export const COMMAND_RISK_NAME = {
0: 'oneterm.commandFilter.safe',
1: 'oneterm.commandFilter.warning',
2: 'oneterm.commandFilter.dangerous',
3: 'oneterm.commandFilter.criticalDanger'
}

View File

@@ -0,0 +1,40 @@
<template>
<a-tabs
type="card"
class="ops-tab command-tab"
>
<a-tab-pane
key="commandManagement"
:tab="$t('oneterm.commandFilter.commandManagement')"
:forceRender="true"
>
<CommandManagement />
</a-tab-pane>
<a-tab-pane
key="commandTemplateManagement"
:tab="$t('oneterm.commandFilter.commandTemplateManagement')"
:forceRender="true"
>
<CommandTemplateManagement />
</a-tab-pane>
</a-tabs>
</template>
<script>
import CommandManagement from './commandManagement/index.vue'
import CommandTemplateManagement from './commandTemplateManagement/index.vue'
export default {
name: 'AccessCommand',
components: {
CommandManagement,
CommandTemplateManagement
}
}
</script>
<style lang="less" scoped>
.command-tab {
height: 100%;
}
</style>

View File

@@ -0,0 +1,17 @@
export const TIME_TEMPLATE_CATEGORY = {
WORK: 'work',
DUTY: 'duty',
MAINTENENCE: 'maintenance',
EMERGENCY: 'emergency',
ALWAYS: 'always',
CUSTOM: 'custom'
}
export const TIME_TEMPLATE_CATEGORY_NAME = {
[TIME_TEMPLATE_CATEGORY.WORK]: 'oneterm.timeTemplate.workTime',
[TIME_TEMPLATE_CATEGORY.DUTY]: 'oneterm.timeTemplate.dutyTime',
[TIME_TEMPLATE_CATEGORY.MAINTENENCE]: 'oneterm.timeTemplate.maintenanceTime',
[TIME_TEMPLATE_CATEGORY.EMERGENCY]: 'oneterm.timeTemplate.emergencyResponse',
[TIME_TEMPLATE_CATEGORY.ALWAYS]: 'oneterm.timeTemplate.allTime',
[TIME_TEMPLATE_CATEGORY.CUSTOM]: 'other'
}

View File

@@ -0,0 +1,314 @@
<template>
<div class="time-template">
<a-spin :tip="loadTip" :spinning="loading">
<div class="time-template-header">
<a-space>
<a-input-search
v-model="searchValue"
:placeholder="$t('placeholderSearch')"
:style="{ width: '250px' }"
class="ops-input ops-input-radius"
allow-clear
@search="updateTableData()"
/>
<div class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
<span @click="batchDelete">{{ $t('delete') }}</span>
<span @click="batchChangeActive(true)">{{ $t('oneterm.enabled') }}</span>
<span @click="batchChangeActive(false)">{{ $t('oneterm.disabled') }}</span>
<span>{{ $t('selectRows', { rows: selectedRowKeys.length }) }}</span>
</div>
</a-space>
<a-space>
<a-button type="primary" @click="openModal(null)">{{ $t(`create`) }}</a-button>
<a-button @click="updateTableData()">{{ $t(`refresh`) }}</a-button>
</a-space>
</div>
<ops-table
size="small"
ref="opsTable"
class="ops-stripe-table"
stripe
show-overflow
show-header-overflow
resizable
:data="tableData"
:checkbox-config="{ reserve: true, highlight: true, range: true }"
:row-config="{ keyField: 'id' }"
:height="tableHeight"
:filter-config="{ remote: true }"
@filter-change="handleFilterChange"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
@checkbox-range-end="onSelectRangeEnd"
>
<vxe-column type="checkbox" width="60px"></vxe-column>
<vxe-column :title="$t('name')" field="name"></vxe-column>
<vxe-column :title="$t('description')" field="description"></vxe-column>
<vxe-column
field="category"
:title="$t(`oneterm.timeTemplate.category`)"
:filters="categoryFilters"
:filter-multiple="false"
>
<template #default="{row}">
{{ $t(row.categoryName) }}
</template>
</vxe-column>
<vxe-column :title="$t('oneterm.timeTemplate.timeZone')" field="timezone"></vxe-column>
<vxe-column :title="$t('oneterm.isEnable')" field="is_active">
<template #default="{row}">
<EnabledStatus
:status="Boolean(row.is_active)"
@change="changeIsActive(row)"
/>
</template>
</vxe-column>
<vxe-column :title="$t('created_at')" width="170">
<template #default="{row}">
{{ row.createdTimeText }}
</template>
</vxe-column>
<vxe-column :title="$t('operation')" width="100">
<template #default="{row}">
<a-space>
<a @click="openModal(row)"><ops-icon type="icon-xianxing-edit"/></a>
<a-popconfirm :title="$t('confirmDelete')" @confirm="deleteTimeTemplate(row)">
<a style="color:red"><ops-icon type="icon-xianxing-delete"/></a>
</a-popconfirm>
</a-space>
</template>
</vxe-column>
</ops-table>
<div class="time-template-pagination">
<a-pagination
size="small"
show-size-changer
:current="currentPage"
:total="totalResult"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
:page-size="pageSize"
:default-current="1"
@change="pageOrSizeChange"
@showSizeChange="pageOrSizeChange"
/>
</div>
</a-spin>
<TimeTemplateModal ref="timeTemplateModalRef" @submit="updateTableData()" />
</div>
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import { mapState } from 'vuex'
import { getTimeTemplateList, deleteTimeTemplateById, putTimeTemplateById } from '@/modules/oneterm/api/timeTemplate.js'
import { TIME_TEMPLATE_CATEGORY, TIME_TEMPLATE_CATEGORY_NAME } from './constants.js'
import TimeTemplateModal from './timeTemplateModal.vue'
import EnabledStatus from '@/modules/oneterm/components/enabledStatus/index.vue'
export default {
name: 'TimeTemplate',
components: {
TimeTemplateModal,
EnabledStatus
},
data() {
return {
searchValue: '',
currentCategory: [],
tableData: [],
currentPage: 1,
pageSize: 20,
totalResult: 0,
selectedRowKeys: [],
loading: false,
loadTip: '',
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
tableHeight() {
return this.windowHeight - 254
},
categoryFilters() {
return Object.values(TIME_TEMPLATE_CATEGORY).map((value) => {
return {
value,
label: this.$t(TIME_TEMPLATE_CATEGORY_NAME[value])
}
})
},
},
mounted() {
this.updateTableData()
},
methods: {
updateTableData() {
this.loading = true
const category = this?.currentCategory?.length ? this.currentCategory.join(',') : undefined
getTimeTemplateList({
page_index: this.currentPage,
page_size: this.pageSize,
search: this.searchValue,
category
})
.then((res) => {
const tableData = res?.data?.list || []
tableData.forEach((row) => {
row.categoryName = TIME_TEMPLATE_CATEGORY_NAME?.[row.category] ?? '-'
row.createdTimeText = moment(row.created_at).format('YYYY-MM-DD HH:mm:ss')
})
this.tableData = tableData
this.totalResult = res?.data?.count ?? 0
})
.finally(() => {
this.loading = false
})
},
onSelectChange() {
const opsTable = this.$refs.opsTable.getVxetableRef()
const records = [...opsTable.getCheckboxRecords(), ...opsTable.getCheckboxReserveRecords()]
this.selectedRowKeys = records.map((i) => i.id)
},
onSelectRangeEnd({ records }) {
this.selectedRowKeys = records.map((i) => i.id)
},
pageOrSizeChange(currentPage, pageSize) {
this.currentPage = currentPage
this.pageSize = pageSize
this.updateTableData()
},
openModal(data) {
this.$refs.timeTemplateModalRef.open(data)
},
deleteTimeTemplate(row) {
this.loading = true
deleteTimeTemplateById(row.id)
.then((res) => {
this.$message.success(this.$t('deleteSuccess'))
this.updateTableData()
})
.finally(() => {
this.loading = false
})
},
async batchDelete() {
this.$confirm({
title: this.$t('warning'),
content: this.$t('confirmDelete'),
onOk: async () => {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = `${this.$t('deleting')}...`
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await deleteTimeTemplateById(this.selectedRowKeys[i])
.then(() => {
successNum += 1
})
.catch(() => {
errorNum += 1
})
.finally(() => {
this.loadTip = this.$t('deletingTip', { total: this.selectedRowKeys.length, successNum, errorNum })
})
}
this.afterBatch()
},
})
},
batchChangeActive(active) {
this.$confirm({
title: this.$t('warning'),
content: this.$t('oneterm.confirmEnable'),
onOk: async () => {
const opsTable = this.$refs.opsTable.getVxetableRef()
const records = [...opsTable.getCheckboxRecords(), ...opsTable.getCheckboxReserveRecords()]
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = `${this.$t('oneterm.switching')}...`
for (let i = 0; i < records.length; i++) {
const params = _.omit(_.cloneDeep(records[i]), ['categoryName', 'createdTimeText', 'id', 'resource_id'])
params.is_active = active
await putTimeTemplateById(records[i].id, params)
.then(() => {
successNum += 1
})
.catch(() => {
errorNum += 1
})
.finally(() => {
this.loadTip = this.$t('oneterm.switchingTip', { total: records.length, successNum, errorNum })
})
}
this.afterBatch()
},
})
},
afterBatch() {
this.loading = false
this.loadTip = ''
this.selectedRowKeys = []
this.$refs.opsTable.getVxetableRef().clearCheckboxRow()
this.$refs.opsTable.getVxetableRef().clearCheckboxReserve()
this.$nextTick(() => {
this.updateTableData()
})
},
changeIsActive(row) {
const params = _.omit(_.cloneDeep(row), ['categoryName', 'createdTimeText', 'id', 'resource_id'])
params.is_active = !params.is_active
putTimeTemplateById(row.id, params).then(() => {
this.$message.success(this.$t('editSuccess'))
this.updateTableData()
})
},
handleFilterChange(e) {
switch (e.field) {
case 'category':
this.currentCategory = e?.values
this.updateTableData()
break
default:
break
}
}
},
}
</script>
<style lang="less" scoped>
.time-template {
background-color: #fff;
height: 100%;
border-radius: 6px;
padding: 18px;
&-header {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
&-pagination {
text-align: right;
margin-top: 8px;
}
}
</style>

View File

@@ -0,0 +1,51 @@
/**
* merge time range
* @param {*} timeRanges Array<{ day: number, value: Array<'HH:mm'> }>
* @returns Array<{ weekdays: number[], start_time: 'HH:mm', end_time: 'HH:mm' }>
*/
export function mergeTimeRange(timeRanges) {
// 1. Count which weekdays each interval appears on
const intervalMap = {} // key: 'start~end', value: Set(day)
timeRanges.forEach(item => {
item.value.forEach(interval => {
if (!intervalMap[interval]) intervalMap[interval] = new Set()
intervalMap[interval].add(item.day)
})
})
// 2. Group by interval and sort by weekday
const intervalArr = Object.entries(intervalMap).map(([interval, weekSet]) => {
const [start_time, end_time] = interval.split('~')
return {
start_time,
end_time,
weekdays: Array.from(weekSet).sort((a, b) => a - b)
}
})
// 3. Merge consecutive intervals with exactly the same weekdays
intervalArr.sort((a, b) => {
// First by weekdays, then by start_time
const w1 = a.weekdays.join(',')
const w2 = b.weekdays.join(',')
if (w1 !== w2) return w1.localeCompare(w2)
return a.start_time.localeCompare(b.start_time)
})
const result = []
for (let i = 0; i < intervalArr.length; i++) {
const cur = intervalArr[i]
if (
result.length &&
// Same weekdays as previous, and previous end_time equals current start_time
JSON.stringify(result[result.length - 1].weekdays) === JSON.stringify(cur.weekdays) &&
result[result.length - 1].end_time === cur.start_time
) {
// Merge
result[result.length - 1].end_time = cur.end_time
} else {
result.push({ ...cur })
}
}
return result
}

View File

@@ -0,0 +1,59 @@
// Convert time string to minutes
function timeToMinutes(str) {
const [h, m] = str.split(':').map(Number)
return h * 60 + m
}
// Convert minutes to time string
function minutesToTime(mins) {
const h = String(Math.floor(mins / 60)).padStart(2, '0')
const m = String(mins % 60).padStart(2, '0')
return `${h}:${m}`
}
// Split into half-hour intervals
function splitHalfHour(start, end) {
const res = []
let s = timeToMinutes(start)
const e = timeToMinutes(end)
while (s < e) {
const next = Math.min(s + 30, e)
res.push(`${minutesToTime(s)}~${minutesToTime(next)}`)
s = next
}
return res
}
/**
* split time range
* @param {*} mergeData Array<{ weekdays: number[], start_time: 'HH:mm', end_time: 'HH:mm' }>
* @returns Array<{ day: number, value: Array<'HH:mm'> }>
*/
export function splitTimeRange(timeRanges) {
const weekMap = {}
timeRanges.forEach(item => {
item.weekdays.forEach(weekNum => {
if (!weekMap[weekNum]) weekMap[weekNum] = []
weekMap[weekNum].push(...splitHalfHour(item.start_time, item.end_time))
})
})
// Remove duplicates and sort
return Object.keys(weekMap)
.sort((a, b) => a - b)
.map(weekNum => {
// Remove duplicates
const valueSet = new Set(weekMap[weekNum])
// Sort
const value = Array.from(valueSet).sort((a, b) => {
const [aStart] = a.split('~')
const [bStart] = b.split('~')
return timeToMinutes(aStart) - timeToMinutes(bStart)
})
return {
day: Number(weekNum),
value
}
})
}

View File

@@ -0,0 +1,201 @@
<template>
<a-modal
:title="title"
:visible="visible"
:confirmLoading="confirmLoading"
:width="1000"
:bodyStyle="{
maxHeight: '60vh',
overflowY: 'auto'
}"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form-model
ref="timeTemplateFormRef"
:model="form"
:rules="rules"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 16 }"
>
<a-form-model-item :label="$t('name')" prop="name">
<a-input v-model="form.name" :placeholder="$t(`placeholder1`)" />
</a-form-model-item>
<a-form-model-item :label="$t('description')" prop="description">
<a-input v-model="form.description" :placeholder="$t(`placeholder1`)" />
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.timeTemplate.category')" prop="category">
<a-select
v-model="form.category"
:placeholder="$t('placeholder2')"
:options="categorySelectOptions"
/>
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.timeTemplate.timeZone')" prop="timezone">
<a-select
v-model="form.timezone"
showSearch
:placeholder="$t('placeholder2')"
:options="timezoneSelectOptions"
/>
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.timeTemplate.timeRange')" prop="time_ranges">
<DragWeekTime
v-model="form.time_ranges"
:data="weekTimeData"
@onClear="clearWeektime"
/>
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import _ from 'lodash'
import momentTimezone from 'moment-timezone'
import { TIME_TEMPLATE_CATEGORY, TIME_TEMPLATE_CATEGORY_NAME } from './constants.js'
import { postTimeTemplate, putTimeTemplateById } from '@/modules/oneterm/api/timeTemplate.js'
import { mergeTimeRange } from './mergeTimeRange.js'
import { splitTimeRange } from './splitTimeRange.js'
import DragWeekTime from '@/modules/oneterm/components/dragWeektime'
import weekTimeData from '@/modules/oneterm/components/dragWeektime/weektimeData'
const DEFAULT_FORM = {
name: '',
description: '',
category: TIME_TEMPLATE_CATEGORY.WORK,
timezone: momentTimezone.tz.guess(),
time_ranges: [],
is_active: false
}
export default {
name: 'TimeTemplateModal',
components: {
DragWeekTime
},
data() {
return {
visible: false,
timeTemplateId: '',
form: { ...DEFAULT_FORM },
rules: {
name: [{ required: true, message: this.$t(`placeholder1`) }],
timezone: [{ required: true, message: this.$t(`placeholder1`) }],
time_ranges: [{ required: true, message: this.$t(`placeholder2`) }],
},
confirmLoading: false,
weekTimeData: _.cloneDeep(weekTimeData)
}
},
computed: {
title() {
if (this.timeTemplateId) {
return this.$t('oneterm.timeTemplate.editTimeTemplate')
}
return this.$t('oneterm.timeTemplate.createTimeTemplate')
},
categorySelectOptions() {
return Object.values(TIME_TEMPLATE_CATEGORY).map((value) => {
return {
value,
label: this.$t(TIME_TEMPLATE_CATEGORY_NAME[value])
}
})
},
timezoneSelectOptions() {
const names = momentTimezone.tz.names()
return names.map((value) => {
return {
value,
label: value
}
})
}
},
methods: {
open(data) {
this.visible = true
if (data) {
let time_ranges = []
if (data?.time_ranges?.length) {
const timeRanges = splitTimeRange(data.time_ranges)
time_ranges = timeRanges.map((item) => {
const childData = this.weekTimeData?.[item?.day - 1]?.child
if (childData?.length) {
childData.forEach((t) => {
this.$set(t, 'check', Boolean(item?.value?.length) && item.value.includes(t.value))
})
}
return {
id: item.day,
day: item.day,
value: item.value,
}
})
}
this.form = {
name: data?.name ?? '',
description: data?.description ?? '',
category: data?.category ?? TIME_TEMPLATE_CATEGORY.WORK,
timezone: data?.timezone ?? momentTimezone.tz.guess(),
is_active: Boolean(data.is_active),
time_ranges
}
this.timeTemplateId = data?.id ?? ''
}
},
clearWeektime() {
this.weekTimeData.forEach((item) => {
item.child.forEach((t) => {
this.$set(t, 'check', false)
})
})
this.form.time_ranges = []
},
handleCancel() {
this.form = { ...DEFAULT_FORM }
this.clearWeektime()
this.timeTemplateId = ''
this.$refs.timeTemplateFormRef.resetFields()
this.visible = false
},
async handleOk() {
this.$refs.timeTemplateFormRef.validate(async (valid) => {
if (!valid) return
this.confirmLoading = true
try {
let time_ranges = []
if (this?.form?.time_ranges?.length) {
time_ranges = mergeTimeRange(this.form.time_ranges)
}
const params = {
...this.form,
time_ranges
}
if (this.timeTemplateId) {
await putTimeTemplateById(this.timeTemplateId, params)
this.$message.success(this.$t('editSuccess'))
} else {
await postTimeTemplate(params)
this.$message.success(this.$t('createSuccess'))
}
this.$emit('submit')
this.handleCancel()
} catch (e) {
console.error('submit error:', e)
} finally {
this.confirmLoading = false
}
})
},
},
}
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div class="oneterm-layout">
<div class="oneterm-header">{{ $t('oneterm.menu.accounts') }}</div>
<div class="oneterm-header">{{ $t('oneterm.menu.accountManagement') }}</div>
<a-spin :tip="loadTip" :spinning="loading">
<div class="oneterm-layout-container">
<div class="oneterm-layout-container-header">

View File

@@ -1,160 +1,162 @@
<template>
<a-form-model ref="form" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 16 }">
<a-form-model-item>
<span slot="label">
<a-tooltip placement="right" :title="$t('oneterm.assetList.timeTip')">
<a><a-icon type="question-circle"/></a>
</a-tooltip>
{{ $t(`oneterm.assetList.time`) }}
</span>
<a-radio-group v-model="form.allow" style="display:block;margin:8px 0;">
<a-radio :value="true">
{{ $t('oneterm.assetList.allowAccess') }}
</a-radio>
<a-radio :value="false">
{{ $t('oneterm.assetList.prohibitAccess') }}
</a-radio>
</a-radio-group>
<DragWeektime v-model="form.ranges" :data="weektimeData" @onClear="clearWeektime" />
<a-form-model-item :label="$t('oneterm.assetList.time')">
<DragWeektime v-model="form.ranges" :data="weekTimeData" @onClear="clearWeektime" />
</a-form-model-item>
<a-form-model-item :label="$t(`oneterm.assetList.effectiveDate`)" prop="startAndEnd">
<a-range-picker v-model="form.startAndEnd" />
<a-form-model-item :wrapper-col="{ span: 16 }" :label="$t('oneterm.timeTemplate.timeZone')" prop="timezone">
<a-select
v-model="form.timezone"
showSearch
:placeholder="$t('placeholder2')"
:options="timezoneSelectOptions"
/>
</a-form-model-item>
<a-form-model-item :label="$t(`oneterm.assetList.commandIntercept`)" prop="cmd_ids">
<treeselect
class="custom-treeselect custom-treeselect-white"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-multiple-lineHeight': '18px',
}"
<a-select
v-model="form.cmd_ids"
:multiple="true"
:clearable="true"
searchable
:options="cmdList"
:placeholder="`${$t(`placeholder2`)}`"
:normalizer="
(node) => {
return {
id: node.id,
label: node.name,
}
}
"
appendToBody
:z-index="1056"
>
</treeselect>
mode="multiple"
:options="commandSelectOptions"
:placeholder="$t('oneterm.auth.commandTip1')"
/>
<a-select
mode="multiple"
v-model="form.template_ids"
:options="commandTemplateSelectOptions"
:placeholder="$t('oneterm.auth.commandTip2')"
/>
</a-form-model-item>
</a-form-model>
</template>
<script>
import moment from 'moment'
import { getCommandList } from '../../../api/command'
import DragWeektime from '../../../components/dragWeektime'
import weektimeData from '../../../components/dragWeektime/weektimeData'
import _ from 'lodash'
import momentTimezone from 'moment-timezone'
import { getCommandList } from '@/modules/oneterm/api/command'
import { getCommandTemplateList } from '@/modules/oneterm/api/commandTemplate.js'
import { mergeTimeRange } from '@/modules/oneterm/views/access/time/mergeTimeRange.js'
import { splitTimeRange } from '@/modules/oneterm/views/access/time/splitTimeRange.js'
import DragWeektime from '@/modules/oneterm/components/dragWeektime'
import weekTimeData from '@/modules/oneterm/components/dragWeektime/weektimeData'
export default {
name: 'AccessAuth',
components: { DragWeektime },
data() {
return {
weektimeData,
weekMap: {
0: '一',
1: '二',
2: '三',
3: '四',
4: '五',
5: '六',
6: '七',
},
weekTimeData: _.cloneDeep(weekTimeData),
form: {
cmd_ids: undefined,
startAndEnd: [],
ranges: [],
allow: true,
timezone: momentTimezone.tz.guess(),
cmd_ids: undefined,
template_ids: undefined
},
rules: {},
allRoleList: [],
roleList: [],
cmdList: [],
commandSelectOptions: [],
commandTemplateSelectOptions: [],
}
},
created() {
getCommandList({ page_index: 1 }).then((res) => {
this.cmdList = res?.data?.list || []
})
this.form.ranges = this.weektimeData.map((item) => {
return {
id: item.row,
week: item.week,
value: [],
}
})
computed: {
timezoneSelectOptions() {
const names = momentTimezone.tz.names()
return names.map((value) => {
return {
value,
label: value
}
})
}
},
mounted() {
this.getCommandList()
this.getCommandTemplateList()
},
beforeDestroy() {
this.clearWeektime()
},
methods: {
getCommandList() {
getCommandList({
page_index: 1,
page_size: 9999
}).then((res) => {
const list = res?.data?.list || []
this.commandSelectOptions = list.map((item) => ({
value: item.id,
label: item.name
}))
})
},
getCommandTemplateList() {
getCommandTemplateList({
page_index: 1,
page_size: 9999
}).then((res) => {
const list = res?.data?.list || []
this.commandTemplateSelectOptions = list.map((item) => ({
value: item.id,
label: item.name
}))
})
},
clearWeektime() {
this.weektimeData.forEach((item) => {
this.weekTimeData.forEach((item) => {
item.child.forEach((t) => {
this.$set(t, 'check', false)
})
})
this.form.ranges = this.weektimeData.map((item) => {
return {
id: item.row,
week: item.week,
value: [],
}
})
this.form.ranges = []
},
getValues() {
const { cmd_ids, startAndEnd, ranges, allow } = this.form
const { ranges, timezone, cmd_ids, template_ids } = this.form
let time_ranges = []
if (ranges.length) {
time_ranges = mergeTimeRange(ranges)
}
return {
cmd_ids,
start: startAndEnd[0]
? moment(startAndEnd[0])
.startOf('day')
.format()
: null,
end: startAndEnd[1]
? moment(startAndEnd[1])
.endOf('day')
.format()
: null,
ranges: ranges.map((r) => ({
week: r.id,
times: r.value,
})),
allow,
template_ids,
time_ranges,
timezone
}
},
async setValues(access_auth) {
const { cmd_ids = undefined, start, end, ranges = [], allow = true } = access_auth
async setValues({
access_time_control,
asset_command_control
}) {
const { time_ranges = [], timezone = momentTimezone.tz.guess() } = access_time_control
const { cmd_ids = undefined, template_ids = undefined } = asset_command_control
let ranges = []
if (time_ranges?.length) {
const timeRanges = splitTimeRange(time_ranges)
ranges = timeRanges.map((item) => {
const childData = this.weekTimeData?.[item?.day - 1]?.child
if (childData?.length) {
childData.forEach((t) => {
this.$set(t, 'check', Boolean(item?.value?.length) && item.value.includes(t.value))
})
}
return {
id: item.day,
day: item.day,
value: item.value,
}
})
}
this.form = {
cmd_ids,
allow,
startAndEnd: [start ? moment(start) : null, end ? moment(end) : null],
ranges: ranges.map((r, index) => {
this.weektimeData[index].child.forEach((t) => {
this.$set(t, 'check', !!r.times && r.times.includes(t.value))
})
return {
id: index,
week: `星期${this.weekMap[index]}`,
value: r.times,
}
}),
template_ids,
ranges,
timezone
}
if (!ranges.length) {
this.clearWeektime()
}
},
}
},
}
</script>

View File

@@ -1,17 +1,17 @@
<template>
<a-row>
<a-row class="form-account">
<a-col v-bind="colSpan">
<table class="account-table">
<tr>
<th>{{ $t(`oneterm.name`) }}</th>
<th>{{ $t(`oneterm.account`) }}</th>
<th>{{ $t(`oneterm.assetList.grantUser`) }}</th>
<th>{{ $t(`operation`) }}</th>
</tr>
<tr v-for="(item, index) in countList" :key="item.id">
<td>
<vxe-table
ref="xTable"
size="mini"
:data="authList"
:column-config="{ width: 200 }"
:min-height="110"
>
<vxe-column field="account" :title="$t('oneterm.account')" width="190">
<template #default="{ row }">
<a-select
v-model="item.name"
v-model="row.account"
showSearch
:style="{
width: '180px',
@@ -19,7 +19,6 @@
:placeholder="$t('placeholder2')"
optionFilterProp="title"
allowClear
@change="(value) => selectAccount(value, index)"
>
<a-select-option
v-for="(node, nodeIndex) in accountList"
@@ -27,35 +26,18 @@
:value="node.id"
:title="node.name"
>
{{ node.name }}
<a-tooltip :title="node.toolTip">
{{ node.name }}
<span v-if="node.account" class="select-option-name">({{ node.account }})</span>
</a-tooltip>
</a-select-option>
</a-select>
</td>
<td>
<a-select
v-model="item.account"
:style="{
width: '180px',
}"
showSearch
:placeholder="$t('placeholder2')"
optionFilterProp="title"
allowClear
@change="(value) => selectAccount(value, index)"
>
<a-select-option
v-for="(node, nodeIndex) in accountList"
:key="node.id + nodeIndex"
:value="node.id"
:title="node.account"
>
{{ node.account }}
</a-select-option>
</a-select>
</td>
<td>
</template>
</vxe-column>
<vxe-column field="grantUser" :title="$t('oneterm.assetList.grantRole')" width="190">
<template #default="{ row }">
<EmployeeTreeSelect
v-model="item.rids"
v-model="row.rids"
multiple
:idType="2"
departmentKey="acl_rid"
@@ -70,28 +52,50 @@
:limit="1"
:otherOptions="visualRoleList"
/>
</td>
<td>
<a-space :style="{ width: '60px' }">
</template>
</vxe-column>
<vxe-column field="permissions" :title="$t('oneterm.assetList.operationPermissions')" width="230">
<template #default="{ row }">
<PermissionCheckbox
:value="row.permissions"
@change="(key, checked) => row.permissions[key] = checked"
/>
</template>
</vxe-column>
<vxe-column field="operation" :title="$t('operation')" width="55" fixed="right">
<template #default="{ row }">
<a-space>
<a @click="addCount"><a-icon type="plus-circle"/></a>
<a v-if="countList && countList.length > 1" @click="deleteCount(index)"><a-icon type="minus-circle"/></a>
<a v-if="authList && authList.length > 1" @click="deleteCount(row.id)"><a-icon type="minus-circle"/></a>
</a-space>
</td>
</tr>
</table>
</template>
</vxe-column>
</vxe-table>
</a-col>
</a-row>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import { getAccountList } from '../../../api/account'
import { getAccountList } from '@/modules/oneterm/api/account'
import { searchRole } from '@/modules/acl/api/role'
import { getConfig } from '@/modules/oneterm/api/config'
import { PERMISSION_TYPE } from '@/modules/oneterm/views/systemSettings/accessControl/constants.js'
import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue'
import PermissionCheckbox from '@/modules/oneterm/views/systemSettings/accessControl/permissionCheckbox.vue'
const DEFAULT_PERMISSIONS = Object.values(PERMISSION_TYPE).reduce((config, key) => {
config[key] = false
return config
}, {})
export default {
name: 'Account',
components: { EmployeeTreeSelect },
components: {
EmployeeTreeSelect,
PermissionCheckbox
},
props: {
colSpan: {
type: Object,
@@ -104,17 +108,36 @@ export default {
data() {
return {
accountList: [],
countList: [{ id: uuidv4(), name: undefined, account: undefined, rids: undefined }],
authList: [{
id: uuidv4(),
account: undefined,
rids: undefined,
permissions: { ...DEFAULT_PERMISSIONS }
}],
visualRoleList: []
}
},
created() {
this.loadRoles()
getAccountList({ page_index: 1 }).then((res) => {
this.accountList = res?.data?.list || []
})
this.initDefaultPermissions()
this.getRoleList()
this.getAccountList()
},
methods: {
async loadRoles() {
initDefaultPermissions() {
getConfig({
info: true
}).then((res) => {
const default_permissions = res?.data?.default_permissions
Object.keys(DEFAULT_PERMISSIONS).forEach((key) => {
DEFAULT_PERMISSIONS[key] = default_permissions?.[key] ?? DEFAULT_PERMISSIONS[key]
})
this.authList.forEach((item) => {
item.permissions = { ...DEFAULT_PERMISSIONS }
})
})
},
async getRoleList() {
const res = await searchRole({ page_size: 9999, page: 1, app_id: 'oneterm', user_role: 0, user_only: 0, is_all: true })
const visualRoleList = []
@@ -137,35 +160,63 @@ export default {
this.$set(this, 'visualRoleList', visualRoleList)
},
async getAccountList() {
const res = await getAccountList({ page_index: 1 })
const accountList = res?.data?.list || []
accountList.forEach((item) => {
item.toolTip = item.name + (item.account ? '(item.account)' : '')
})
this.accountList = accountList
},
addCount() {
this.countList.push({ id: uuidv4(), name: undefined, account: undefined, rids: undefined })
},
deleteCount(index) {
this.countList.splice(index, 1)
},
selectAccount(id, index) {
this.$nextTick(() => {
this.$set(this.countList[index], 'name', id)
this.$set(this.countList[index], 'account', id)
this.authList.push({
id: uuidv4(),
account: undefined,
rids: undefined,
permissions: { ...DEFAULT_PERMISSIONS }
})
},
deleteCount(id) {
const index = this.authList.findIndex((item) => item.id === id)
if (index !== -1) {
this.authList.splice(index, 1)
}
},
getValues() {
const authorization = {}
this.countList
.filter((count) => count.name)
.forEach((count) => {
authorization[count.name] = count?.rids?.length ? count.rids.map((r) => Number(r.split('-')[1])) : []
this.authList
.filter((auth) => auth.account)
.forEach((auth) => {
const rids = (auth?.rids || []).map((r) => Number(r.split('-')[1]))
authorization[auth.account] = {
rids,
permissions: auth.permissions
}
})
return { authorization }
},
setValues({ authorization = {} }) {
const authorizationList = Object.entries(authorization)
const authorizationList = Object.entries(authorization || {})
if (authorizationList.length) {
this.countList = authorizationList.map(([acc, rids]) => {
return { id: uuidv4(), name: Number(acc), account: Number(acc), rids: rids.map((r) => `employee-${r}`) }
this.authList = authorizationList.map(([key, value]) => {
return {
id: uuidv4(),
account: Number(key),
rids: (value?.rids || []).map((r) => `employee-${r}`),
permissions: value.permissions
}
})
} else {
this.countList = [{ id: uuidv4(), name: undefined, account: undefined, rids: undefined }]
this.authList = [{
id: uuidv4(),
account: undefined,
rids: undefined,
permissions: { ...DEFAULT_PERMISSIONS }
}]
}
},
},
@@ -173,16 +224,14 @@ export default {
</script>
<style lang="less" scoped>
.account-table {
border-collapse: collapse;
border: 1px solid #e4e7ed;
border-spacing: 20px;
th {
background-color: #f0f5ff;
}
th,
td {
padding: 5px 8px;
.form-account {
/deep/ .ant-checkbox-wrapper {
margin-right: 0px;
width: 48%;
}
}
.select-option-name {
font-size: 12px;
color: #A5A9BC;
}
</style>

View File

@@ -43,21 +43,21 @@
<a-menu>
<a-menu-item key="1" v-if="showNodeOperation(node.dataRef, ['write'])" @click="$emit('openNode', { parent_id: node.dataRef.id })">
<a-icon type="plus-circle" />
{{ $t(`oneterm.assetList.createSubCatalog`) }}
{{ $t(`oneterm.assetList.createSubFolder`) }}
</a-menu-item>
<a-menu-item key="2" v-if="showNodeOperation(node.dataRef, ['write'])" @click="$emit('openNode', node.dataRef)">
<ops-icon type="icon-xianxing-edit" />
{{ $t(`oneterm.assetList.editCatalog`) }}
{{ $t(`oneterm.assetList.editFolder`) }}
</a-menu-item>
<a-menu-item key="3" v-if="showNodeOperation(node.dataRef, ['delete'])" @click="deleteNode(node.dataRef)">
<ops-icon type="veops-delete" />
{{ $t(`oneterm.assetList.deleteCatalog`) }}
{{ $t(`oneterm.assetList.deleteFolder`) }}
</a-menu-item>
<template v-if="showNodeOperation(node.dataRef, ['grant'])">
<a-divider style="margin: 4px 0" />
<a-menu-item key="4" @click="openGrantModal(node.dataRef)">
<a-icon type="user-add" />
{{ $t(`oneterm.assetList.grantCatalog`) }}
{{ $t(`oneterm.assetList.grantFolder`) }}
</a-menu-item>
</template>
</a-menu>
@@ -125,7 +125,7 @@
<vxe-column type="checkbox" width="60px"></vxe-column>
<vxe-column :title="$t(`oneterm.name`)" field="name"></vxe-column>
<vxe-column :title="$t(`oneterm.assetList.ip`)" field="ip"> </vxe-column>
<vxe-column :title="$t(`oneterm.assetList.catalogName`)" field="node_chain"> </vxe-column>
<vxe-column :title="$t(`oneterm.assetList.folderName`)" field="node_chain"> </vxe-column>
<vxe-column
:title="$t(`status`)"
field="connectable"
@@ -242,7 +242,7 @@ export default {
searchValue: '',
getRequestParams: {
info: false
info: true
}
}
},
@@ -544,7 +544,7 @@ export default {
authAsset() {
const assetIds = this.selectedRowKeys.map((item) => item.id)
this.$refs.grantModalRef.open({
resourceId: this.selectedRowKeys?.[0]?.resource_id || '',
resourceId: this.selectedRowKeys?.[0]?.resource_id ?? '',
type: 'asset',
ids: assetIds
})

View File

@@ -22,7 +22,7 @@
<a-form-model-item :label="$t('oneterm.assetList.ip')" prop="ip">
<a-input v-model="baseForm.ip" :placeholder="`${$t(`placeholder1`)}`" />
</a-form-model-item>
<a-form-model-item :label="$t(`oneterm.catalog`)" prop="parent_id">
<a-form-model-item :label="$t(`oneterm.folder`)" prop="parent_id">
<treeselect
class="custom-treeselect custom-treeselect-white"
:style="{
@@ -90,8 +90,8 @@
import Protocol from './protocol.vue'
import Account from './account.vue'
import AccessAuth from './accessAuth.vue'
import { getNodeList } from '../../../api/node'
import { postAsset, putAssetById } from '../../../api/asset'
import { getNodeList } from '@/modules/oneterm/api/node'
import { postAsset, putAssetById } from '@/modules/oneterm/api/asset'
export default {
name: 'CreateAsset',
@@ -110,6 +110,7 @@ export default {
},
baseRules: {
name: [{ required: true, message: `${this.$t(`placeholder1`)}` }],
ip: [{ required: true, message: `${this.$t(`placeholder1`)}` }],
parent_id: [{ required: true, message: `${this.$t(`placeholder2`)}` }],
},
nodeList: [],
@@ -141,7 +142,8 @@ export default {
gateway_id = undefined,
protocols = [],
authorization = {},
access_auth = {},
access_time_control = {},
asset_command_control = {}
} = asset ?? {}
this.assetId = id
this.baseForm = {
@@ -152,7 +154,10 @@ export default {
}
this.$refs.protocol.setValues({ gateway_id, protocols })
this.$refs.account.setValues({ authorization })
this.$refs.accessAuth.setValues(access_auth)
this.$refs.accessAuth.setValues({
access_time_control,
asset_command_control
})
})
},
@@ -181,7 +186,7 @@ export default {
const { name, ip, parent_id, comment } = this.baseForm
const { gateway_id, protocols } = this.$refs.protocol.getValues()
const { authorization } = this.$refs.account.getValues()
const access_auth = this.$refs.accessAuth.getValues()
const { cmd_ids, template_ids, time_ranges, timezone } = this.$refs.accessAuth.getValues()
const params = {
name,
ip: ip?.trim?.() ?? '',
@@ -190,8 +195,16 @@ export default {
protocols,
gateway_id,
authorization,
access_auth,
access_time_control: {
time_ranges,
timezone
},
asset_command_control: {
cmd_ids,
template_ids
}
}
this.loading = true
if (this.assetId) {
putAssetById(this.assetId, { ...params, id: this.assetId })

View File

@@ -16,10 +16,10 @@
:label-col="{ span: 5 }"
:wrapper-col="{ span: 16 }"
>
<a-form-model-item :label="$t('oneterm.assetList.catalogName')" prop="name">
<a-form-model-item :label="$t('oneterm.assetList.folderName')" prop="name">
<a-input v-model="baseForm.name" :placeholder="`${$t(`placeholder1`)}`" />
</a-form-model-item>
<a-form-model-item :label="$t(`oneterm.catalog`)" prop="parent_id">
<a-form-model-item :label="$t(`oneterm.folder`)" prop="parent_id">
<treeselect
class="custom-treeselect custom-treeselect-white"
:style="{
@@ -55,77 +55,6 @@
<a-textarea v-model="baseForm.comment" :placeholder="`${$t(`placeholder1`)}`" />
</a-form-model-item>
</a-form-model>
<!-- <p>
<strong>{{ $t(`oneterm.assetList.cmdbSync`) }}</strong>
</p>
<a-form-model ref="syncForm" :model="syncForm" :label-col="{ span: 5 }" :wrapper-col="{ span: 16 }">
<a-form-model-item
:label="$t('oneterm.cmdbType')"
prop="type_id"
:style="{ display: 'flex', alignItems: 'center' }"
>
<CMDBTypeSelect v-model="syncForm.type_id" selectType="ci_type" @change="changeTypeId" />
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.fieldMap')">
<div v-for="item in fieldMap" :key="item.id">
<div class="cmdb-radio-slot-field">
<div class="slot-field1">
<span>*</span>
<a-input disabled size="small" :style="{ width: '200px' }" v-model="item['attribute'].label" />
</div>
<div class="slot-field2" :style="{ marginLeft: '150px' }">
<treeselect
class="custom-treeselect custom-treeselect-white"
:style="{
'--custom-height': '24px',
lineHeight: '24px',
width: '250px',
}"
v-model="item.field_name"
:multiple="false"
:clearable="true"
searchable
:options="attributes"
:placeholder="`${$t(`placeholder2`)}`"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
</div>
</div>
<span v-if="item.error" style="color: red">{{ `${$t(`placeholder2`)}` }}</span>
</div>
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.filter')" class="cmdb-value-filter">
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="attributes"
@setExpFromFilter="setExpFromFilter"
:expression="filterExp ? `q=${filterExp}` : ''"
/>
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.assetList.sync')" prop="enable">
<a-switch v-model="syncForm.enable" />
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.assetList.frequency')" prop="frequency">
<a-input-number :min="0" v-model="syncForm.frequency" />{{ $t('hour') }}
</a-form-model-item>
</a-form-model> -->
<p>
<strong>{{ $t(`oneterm.protocol`) }}</strong>
</p>
@@ -134,10 +63,6 @@
<strong>{{ $t(`oneterm.accountAuthorization`) }}</strong>
</p>
<Account ref="account" />
<p>
<strong>{{ $t(`oneterm.accessRestrictions`) }}</strong>
</p>
<AccessAuth ref="accessAuth" />
<div class="custom-drawer-bottom-action">
<a-button
:loading="loading"
@@ -154,17 +79,17 @@
</template>
<script>
import CMDBTypeSelect from '../../../components/cmdbTypeSelect'
import { getCITypeAttributesById } from '../../../api/otherModules'
import FilterComp from '@/components/CMDBFilterComp'
import { getNodeList, postNode, putNodeById } from '@/modules/oneterm/api/node'
import Protocol from './protocol.vue'
import Account from './account.vue'
import AccessAuth from './accessAuth.vue'
import { getNodeList, postNode, putNodeById } from '../../../api/node'
export default {
name: 'CreateNode',
components: { CMDBTypeSelect, FilterComp, Protocol, Account, AccessAuth },
components: {
Protocol,
Account
},
data() {
return {
visible: false,
@@ -179,37 +104,15 @@ export default {
baseRules: {
name: [{ required: true, message: `${this.$t(`placeholder1`)}` }],
},
syncForm: {
type_id: undefined,
enable: true,
frequency: undefined,
},
// fieldMap
fieldMap: [
{
field_name: undefined,
attribute: { value: 'name', label: this.$t('oneterm.name') },
},
{
field_name: undefined,
attribute: { value: 'ip', label: this.$t('oneterm.assetList.ip') },
},
],
fieldMapObj: {
ip: 'IP',
name: this.$t('oneterm.name'),
},
attributes: [],
filterExp: '',
nodeList: [],
}
},
computed: {
title() {
if (this.type === 'create') {
return this.$t(`oneterm.assetList.createCatalog`)
return this.$t(`oneterm.assetList.createFolder`)
}
return this.$t(`oneterm.assetList.editCatalog`)
return this.$t(`oneterm.assetList.editFolder`)
},
},
mounted() {},
@@ -225,128 +128,42 @@ export default {
getNodeList(params).then((res) => {
this.nodeList = res?.data?.list || []
})
console.log(node)
const {
id = null,
name = '',
comment = '',
parent_id,
sync = {},
gateway_id = undefined,
protocols = [],
authorization = {},
access_auth = {},
authorization = {}
} = node ?? {}
const { type_id = undefined, enable = true, frequency = undefined, filters = '', mapping = {} } = sync
this.nodeId = id
this.baseForm = {
name,
parent_id: parent_id || undefined,
comment,
}
this.syncForm = {
type_id,
enable,
frequency,
}
this.$nextTick(() => {
this.fieldMap =
JSON.stringify(mapping) === '{}'
? [
{
field_name: undefined,
attribute: { value: 'name', label: this.$t('oneterm.name') },
},
{
field_name: undefined,
attribute: { value: 'ip', label: this.$t('oneterm.assetList.ip') },
},
]
: Object.keys(mapping).map((key) => {
return {
field_name: mapping[key],
attribute: { value: key, label: this.fieldMapObj[key] },
}
})
})
this.filterExp = filters
// this.$nextTick(() => {
// this.$refs.filterComp.visibleChange(true, false)
// })
this.$refs.protocol.setValues({ gateway_id, protocols })
this.$refs.account.setValues({ authorization })
this.$refs.accessAuth.setValues(access_auth)
})
},
async changeTypeId(id) {
this.fieldMap = [
{
field_name: undefined,
attribute: { value: 'name', label: this.$t('oneterm.name') },
},
{
field_name: undefined,
attribute: { value: 'ip', label: this.$t('oneterm.assetList.ip') },
},
]
if (id) {
await getCITypeAttributesById(id).then((res) => {
const { attributes } = res
this.attributes = attributes
})
} else {
this.attributes = []
}
},
// setExpFromFilter(filterExp) {
// if (filterExp) {
// this.filterExp = `${filterExp}`
// } else {
// this.filterExp = ''
// }
// },
handleSubmit() {
this.$refs.baseForm.validate((valid) => {
if (valid) {
const { name, parent_id, comment } = this.baseForm
// const { type_id, enable, frequency } = this.syncForm
// this.$refs.filterComp.handleSubmit()
// const mapping = {}
// let flag = true
// this.fieldMap.forEach((field) => {
// if (!field.field_name) {
// this.$set(field, 'error', true)
// field.error = true
// flag = false
// } else {
// this.$set(field, 'error', false)
// field.error = false
// mapping[field.attribute.value] = field.field_name
// }
// })
// if (type_id && !flag) {
// return
// }
const { gateway_id, protocols } = this.$refs.protocol.getValues()
const { authorization } = this.$refs.account.getValues()
const access_auth = this.$refs.accessAuth.getValues()
const params = {
name,
comment,
parent_id: parent_id ?? 0,
// sync: {
// enable,
// filters: this.filterExp,
// frequency,
// mapping,
// type_id,
// },
protocols,
gateway_id,
authorization,
access_auth,
authorization
}
console.log(params)
this.loading = true
if (this.nodeId) {
putNodeById(this.nodeId, { ...params, id: this.nodeId })

View File

@@ -8,7 +8,7 @@
<template #title>
<div class="asset-list-title">
<div class="asset-list-title-text">
{{ $t('oneterm.assetList.assetList') }}
{{ $t('oneterm.menu.assetManagement') }}
</div>
<div
@@ -19,7 +19,7 @@
<a-icon type="plus"/>
</span>
<span class="asset-list-title-create-text">
{{ $t(`oneterm.assetList.createCatalog`) }}
{{ $t(`oneterm.assetList.createFolder`) }}
</span>
</div>
</div>
@@ -52,11 +52,6 @@ export default {
allTreeDepAndEmp: [],
}
},
computed: {
title() {
return this.$t(`oneterm.assetList.assetList`)
},
},
mounted() {
getAllDepAndEmployee({ block: 0 }).then((res) => {
this.allTreeDepAndEmp = res

View File

@@ -181,9 +181,9 @@ export default {
const _protocols = this.protocols.map((pro) => `${pro.value}:${pro.label}`)
return { gateway_id, protocols: _protocols }
},
setValues({ gateway_id = undefined, protocols = [] }) {
setValues({ gateway_id = undefined, protocols }) {
this.form = { gateway_id: gateway_id || undefined }
this.protocols = protocols.length
this.protocols = protocols?.length
? protocols.map((p) => ({
id: uuidv4(),
value: p.split(':')[0],

View File

@@ -2,7 +2,7 @@
<div class="oneterm-layout">
<div class="oneterm-header">
<a-space>
<span>{{ $t('oneterm.menu.gateways') }}</span>
<span>{{ $t('oneterm.menu.gatewayManagement') }}</span>
<a-tooltip placement="right" :title="$t('oneterm.assetList.gatewayTip')">
<a><a-icon type="question-circle"/></a>
</a-tooltip>

View File

@@ -15,7 +15,7 @@
</div>
<div class="file-management-title-right">
<a-tooltip
v-if="selectedRows.length"
v-if="selectedRows.length && showDownload"
:title="$t('oneterm.fileManagement.batchDownloadFiles')"
>
<a-icon
@@ -24,6 +24,7 @@
/>
</a-tooltip>
<UploadFile
v-if="showUpload"
:sessionId="sessionId"
:pathStr="pathStr"
:connectType="connectType"
@@ -87,7 +88,11 @@
@checkbox-all="onSelectChange"
@checkbox-range-end="onSelectChange"
>
<vxe-column type="checkbox" width="40px"></vxe-column>
<vxe-column
v-if="showDownload"
type="checkbox"
width="40px"
></vxe-column>
<vxe-column
:title="$t('name')"
field="name"
@@ -144,6 +149,7 @@ import moment from 'moment'
import { mapState } from 'vuex'
import { getFileListBySessionId } from '@/modules/oneterm/api/file.js'
import { getRDPFileList } from '@/modules/oneterm/api/rdp.js'
import { PERMISSION_TYPE } from '@/modules/oneterm/views/systemSettings/accessControl/constants.js'
import UploadFile from './uploadFile.vue'
@@ -160,6 +166,10 @@ export default {
connectType: {
type: String,
default: 'ssh' // 'ssh' | 'rdp'
},
assetPermissions: {
type: Object,
default: () => {}
}
},
data() {
@@ -179,6 +189,12 @@ export default {
}),
pathStr() {
return `/${this.pathList.join('/')}`
},
showDownload() {
return this.assetPermissions?.[PERMISSION_TYPE.FILE_DOWNLOAD] || false
},
showUpload() {
return this.assetPermissions?.[PERMISSION_TYPE.FILE_UPLOAD] || false
}
},
methods: {

View File

@@ -23,6 +23,7 @@
ref="fileManagementDrawerRef"
connectType="rdp"
:sessionId="sessionId"
:assetPermissions="assetPermissions"
/>
</div>
</template>
@@ -32,6 +33,7 @@ import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import Guacamole from 'guacamole-common-js'
import { pageBeforeUnload } from '@/modules/oneterm/utils/index.js'
import { PERMISSION_TYPE } from '@/modules/oneterm/views/systemSettings/accessControl/constants.js'
import ClipboardModal from './clipboardModal.vue'
import ResolutionModal from './resolutionModal.vue'
@@ -76,7 +78,7 @@ export default {
type: [Object, null],
default: null
},
controlConfig: {
assetPermissions: {
type: Object,
default: () => {}
}
@@ -154,7 +156,7 @@ export default {
// clipboard contents received by remote desktop
client.onclipboard = this.handleClipboardReceived
if (this?.controlConfig?.[`${queryProtocol?.split?.(':')?.[0]}_config`]?.copy) {
if (this?.assetPermissions?.[PERMISSION_TYPE.COPY]) {
// handle the clipboard content received from the remote desktop.
client.onclipboard = this.handleClipboardReceived
}

View File

@@ -22,6 +22,7 @@
ref="fileManagementDrawerRef"
connectType="ssh"
:sessionId="connectData.sessionId"
:assetPermissions="assetPermissions"
/>
</div>
</template>
@@ -71,6 +72,10 @@ export default {
preferenceSetting: {
type: [Object, null],
default: null
},
assetPermissions: {
type: Object,
default: () => {}
}
},
data() {

View File

@@ -0,0 +1,17 @@
export const PERMISSION_TYPE = {
CONNECT: 'connect',
SHARE: 'share',
FILE_UPLOAD: 'file_upload',
FILE_DOWNLOAD: 'file_download',
COPY: 'copy',
PASTE: 'paste'
}
export const PERMISSION_TYPE_NAME = {
[PERMISSION_TYPE.CONNECT]: 'oneterm.accessControl.connect',
[PERMISSION_TYPE.FILE_UPLOAD]: 'oneterm.accessControl.upload',
[PERMISSION_TYPE.FILE_DOWNLOAD]: 'oneterm.accessControl.download',
[PERMISSION_TYPE.COPY]: 'oneterm.accessControl.copy',
[PERMISSION_TYPE.PASTE]: 'oneterm.accessControl.paste',
[PERMISSION_TYPE.SHARE]: 'oneterm.accessControl.share',
}

View File

@@ -0,0 +1,111 @@
<template>
<div class="access-control">
<a-form-model ref="configForm" :model="form" :rules="rules" :label-col="{ span: 7 }" :wrapper-col="{ span: 14 }">
<a-form-model-item :label="$t('oneterm.accessControl.timeout')" prop="timeout">
<a-input-number
:min="0"
:max="7200"
v-model="form.timeout"
:formatter="(value) => `${value}s`"
:parser="(value) => value.replace('s', '')"
/>
</a-form-model-item>
<a-form-model-item
prop="default_permissions"
:label="$t('oneterm.accessControl.permissionConfig')"
:extra="$t('oneterm.accessControl.permissionConfigTip')"
>
<PermissionCheckbox
:value="form.default_permissions"
@change="handlePermissionChange"
/>
</a-form-model-item>
<a-form-model-item label=" " :colon="false">
<a-space>
<a-button :loading="loading" @click="getConfig()">{{ $t('reset') }}</a-button>
<a-button :loading="loading" type="primary" @click="handleSave">{{ $t('save') }}</a-button>
</a-space>
</a-form-model-item>
</a-form-model>
</div>
</template>
<script>
import { getConfig, postConfig } from '@/modules/oneterm/api/config'
import { PERMISSION_TYPE } from './constants.js'
import PermissionCheckbox from './permissionCheckbox.vue'
const DEFAULT_PERMISSIONS = Object.values(PERMISSION_TYPE).reduce((config, key) => {
config[key] = false
return config
}, {})
export default {
name: 'AccessControl',
components: {
PermissionCheckbox
},
data() {
return {
loading: false,
rules: {},
form: {
timeout: 5,
default_permissions: { ...DEFAULT_PERMISSIONS }
}
}
},
mounted() {
this.getConfig()
},
methods: {
getConfig() {
getConfig({
info: true
}).then((res) => {
const { timeout = 5, default_permissions } = res?.data
this.form = {
timeout,
default_permissions: default_permissions || { ...DEFAULT_PERMISSIONS }
}
})
},
handlePermissionChange(key, checked) {
this.form.default_permissions[key] = checked
},
handleSave() {
this.loading = true
postConfig({ ...this.form })
.then(() => {
this.$message.success(this.$t('saveSuccess'))
})
.finally(async () => {
this.getConfig()
this.loading = false
})
},
},
}
</script>
<style lang="less" scoped>
.access-control {
background-color: #fff;
height: 100%;
padding: 18px 0px;
border-radius: 6px;
&-label {
display: inline-flex;
align-items: center;
&-icon {
margin-left: 6px;
color: @text-color_3;
}
}
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<div>
<a-checkbox
v-for="(item) in permissionCheckboxOptions"
:key="item.value"
:checked="value[item.value]"
@change="(e) => $emit('change', item.value, e.target.checked)"
>
{{ $t(item.label) }}
<a-tooltip v-if="item.value === PERMISSION_TYPE.COPY">
<p slot="title">{{ $t('oneterm.accessControl.copyTip') }}</p>
<a-icon type="info-circle" class="terminal-control-label-icon"/>
</a-tooltip>
<a-tooltip v-if="item.value === PERMISSION_TYPE.PASTE">
<p slot="title">{{ $t('oneterm.accessControl.pasteTip') }}</p>
<a-icon type="info-circle" class="terminal-control-label-icon"/>
</a-tooltip>
</a-checkbox>
</div>
</template>
<script>
import { PERMISSION_TYPE, PERMISSION_TYPE_NAME } from './constants.js'
export default {
name: 'PermissionCheckbox',
props: {
value: {
type: Object,
default: () => {}
}
},
data() {
return {
PERMISSION_TYPE,
permissionCheckboxOptions: Object.values(PERMISSION_TYPE).map((key) => ({
value: key,
label: PERMISSION_TYPE_NAME[key]
}))
}
}
}
</script>
<style lang="less" scoped>
.ant-checkbox-wrapper + .ant-checkbox-wrapper {
margin-left: 0px;
margin-right: 8px;
}
</style>

View File

@@ -1,100 +0,0 @@
<template>
<a-modal :title="title" :visible="visible" @cancel="handleCancel" @ok="handleOk" :confirmLoading="loading">
<a-form-model ref="commandForm" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 16 }">
<a-form-model-item :label="$t(`oneterm.name`)" prop="name">
<a-input v-model="form.name" :placeholder="`${$t(`placeholder1`)}`" />
</a-form-model-item>
<a-form-model-item :label="$t(`oneterm.command`)" prop="cmd">
<a-input v-model="form.cmd" :placeholder="`${$t(`placeholder1`)}`" />
</a-form-model-item>
<a-form-model-item :label="$t(`oneterm.commandIntercept.enable`)" prop="enable">
<a-switch v-model="form.enable" />
</a-form-model-item>
<a-form-model-item :label="$t(`oneterm.commandIntercept.regexp`)" prop="enable">
<a-switch v-model="form.is_re" />
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import { postCommand, putCommandById } from '../../../api/command'
export default {
name: 'CommandModal',
data() {
return {
visible: false,
form: {
name: '',
cmd: '',
enable: true,
is_re: true,
},
rules: {
name: [{ required: true, message: `${this.$t(`placeholder1`)}` }],
},
loading: false,
}
},
computed: {
title() {
if (this.form.id) {
return this.$t('oneterm.commandIntercept.editCommand')
}
return this.$t('oneterm.commandIntercept.createCommand')
},
},
methods: {
open(data) {
this.visible = true
if (data) {
this.form = {
...data,
enable: Boolean(data.enable),
is_re: Boolean(data.is_re),
cmd: data.cmd ?? ''
}
}
},
handleCancel() {
this.$refs.commandForm.resetFields()
this.form = {
name: '',
cmd: '',
enable: true,
is_re: true,
}
this.visible = false
},
async handleOk() {
this.$refs.commandForm.validate(async (valid) => {
if (valid) {
this.loading = true
if (this.form.id) {
await putCommandById(this.form.id, { ...this.form })
.then(() => {
this.$message.success(this.$t('editSuccess'))
})
.finally(() => {
this.loading = false
})
} else {
await postCommand({ ...this.form })
.then(() => {
this.$message.success(this.$t('createSuccess'))
})
.finally(() => {
this.loading = false
})
}
this.$emit('submit')
this.handleCancel()
}
})
},
},
}
</script>
<style></style>

View File

@@ -24,8 +24,7 @@
</template>
<script>
import CommandIntercept from './commandIntercept/index.vue'
import TerminalControl from './terminalControl/index.vue'
import AccessControl from './accessControl/index.vue'
import PublicKey from './publicKey/index.vue'
import QuickCommand from './quickCommand/index.vue'
import TerminalDisplay from './terminalDisplay/index.vue'
@@ -36,8 +35,7 @@ const systemSettingTabStorageKey = 'ops_oneterm_system_setting_tab_key'
export default {
name: 'SystemSettings',
components: {
CommandIntercept,
TerminalControl,
AccessControl,
PublicKey,
QuickCommand,
TerminalDisplay,
@@ -70,16 +68,10 @@ export default {
component: 'TerminalDisplay'
},
{
label: 'oneterm.systemSettings.terminalControl',
label: 'oneterm.systemSettings.accessControl',
icon: 'basic_settings',
key: 'terminalControl',
component: 'TerminalControl',
},
{
label: 'oneterm.systemSettings.commandIntercept',
icon: 'a-command_interception1',
key: 'commandIntercept',
component: 'CommandIntercept',
key: 'accessControl',
component: 'AccessControl',
},
{
label: 'oneterm.systemSettings.storageConfig',

View File

@@ -41,7 +41,7 @@
<a-form-model-item :label="$t('oneterm.storageConfig.isPrimary')" prop="is_primary">
<a-switch v-model="form.is_primary" />
</a-form-model-item>
<a-form-model-item :label="$t('oneterm.storageConfig.isEnable')" prop="enabled">
<a-form-model-item :label="$t('oneterm.isEnable')" prop="enabled">
<a-switch v-model="form.enabled" />
</a-form-model-item>

View File

@@ -82,10 +82,10 @@
</a-popconfirm>
</template>
</vxe-column>
<vxe-column :title="$t('oneterm.storageConfig.isEnable')" field="enabled" min-width="70">
<vxe-column :title="$t('oneterm.isEnable')" field="enabled" min-width="70">
<template #default="{row}">
<a-popconfirm
:title="$t('oneterm.storageConfig.confirmEnable')"
:title="$t('oneterm.confirmEnable')"
@confirm="toggleEnabled(row)"
>
<a-switch :checked="row.enabled" />

View File

@@ -1,143 +0,0 @@
<template>
<div class="terminal-control">
<a-form-model ref="configForm" :model="form" :rules="rules" :label-col="{ span: 7 }" :wrapper-col="{ span: 14 }">
<a-form-model-item :label="$t('oneterm.terminalControl.timeout')" prop="timeout">
<a-input-number
:min="0"
:max="7200"
v-model="form.timeout"
:formatter="(value) => `${value}s`"
:parser="(value) => value.replace('s', '')"
/>
</a-form-model-item>
<a-form-model-item>
<div class="terminal-control-label" slot="label">
<span>RDP</span>
<a-tooltip>
<div slot="title">
<p>{{ $t('oneterm.terminalControl.copyTip') }}</p>
<p>{{ $t('oneterm.terminalControl.pasteTip') }}</p>
</div>
<a-icon
type="info-circle"
class="terminal-control-label-icon"
/>
</a-tooltip>
</div>
<a-checkbox v-model="form.rdp_config.copy">
{{ $t('oneterm.terminalControl.allowCopy') }}
</a-checkbox>
<a-checkbox v-model="form.rdp_config.paste">
{{ $t('oneterm.terminalControl.allowPaste') }}
</a-checkbox>
</a-form-model-item>
<a-form-model-item>
<div class="terminal-control-label" slot="label">
<span>VNC</span>
<a-tooltip>
<div slot="title">
<p>{{ $t('oneterm.terminalControl.copyTip') }}</p>
<p>{{ $t('oneterm.terminalControl.pasteTip') }}</p>
</div>
<a-icon
type="info-circle"
class="terminal-control-label-icon"
/>
</a-tooltip>
</div>
<a-checkbox v-model="form.vnc_config.copy">
{{ $t('oneterm.terminalControl.allowCopy') }}
</a-checkbox>
<a-checkbox v-model="form.vnc_config.paste">
{{ $t('oneterm.terminalControl.allowPaste') }}
</a-checkbox>
</a-form-model-item>
<a-form-model-item label=" " :colon="false">
<a-space>
<a-button :loading="loading" @click="getConfig()">{{ $t('reset') }}</a-button>
<a-button :loading="loading" type="primary" @click="handleSave">{{ $t('save') }}</a-button>
</a-space>
</a-form-model-item>
</a-form-model>
</div>
</template>
<script>
import { getConfig, postConfig } from '@/modules/oneterm/api/config'
export default {
name: 'TerminalControl',
data() {
return {
loading: false,
form: {
timeout: 5,
rdp_config: {
copy: false,
paste: false,
},
vnc_config: {
copy: false,
paste: false,
}
},
rules: {},
}
},
mounted() {
this.getConfig()
},
methods: {
async getConfig() {
await getConfig({
info: true
}).then((res) => {
const { timeout = 5, rdp_config, vnc_config } = res?.data
this.form = {
timeout,
vnc_config: {
copy: vnc_config?.copy || false,
paste: vnc_config?.paste || false
},
rdp_config: {
copy: rdp_config?.copy || false,
paste: rdp_config?.paste || false
}
}
})
},
handleSave() {
this.loading = true
postConfig({ ...this.form })
.then(() => {
this.$message.success(this.$t('saveSuccess'))
})
.finally(async () => {
this.getConfig()
this.loading = false
})
},
},
}
</script>
<style lang="less" scoped>
.terminal-control {
background-color: #fff;
height: 100%;
padding: 18px 0px;
border-radius: 6px;
&-label {
display: inline-flex;
align-items: center;
&-icon {
margin-left: 6px;
color: @text-color_3;
}
}
}
</style>

View File

@@ -114,7 +114,7 @@ export default {
searchValue: '',
getRequestParams: {
info: true
info: false
}
}
},

View File

@@ -53,7 +53,7 @@
</template>
</vxe-column>
<vxe-column :title="$t(`oneterm.assetList.ip`)" field="ip"> </vxe-column>
<vxe-column :title="$t(`oneterm.assetList.catalogName`)" field="node_chain"> </vxe-column>
<vxe-column :title="$t(`oneterm.assetList.folderName`)" field="node_chain"> </vxe-column>
<vxe-column
:title="$t(`status`)"
field="connectable"

View File

@@ -89,6 +89,7 @@
:assetId="item.assetId"
:accountId="item.accountId"
:protocol="item.protocol"
:assetPermissions="item.permissions"
:isFullScreen="false"
:preferenceSetting="preferenceSetting"
@close="handleTerminalError(item)"
@@ -103,9 +104,9 @@
:assetId="item.assetId"
:accountId="item.accountId"
:protocol="item.protocol"
:assetPermissions="item.permissions"
:isFullScreen="false"
:preferenceSetting="preferenceSetting"
:controlConfig="controlConfig"
@close="handleTerminalError(item)"
@open="getOfUserStat()"
@updatePreferenceSetting="getPreference"
@@ -128,7 +129,6 @@
:openFullScreen="openFullScreen"
:accountList="accountList"
:currentTabData="currentTabData"
:controlConfig="controlConfig"
@toggleFullScreen="toggleFullScreen"
@openRecentSession="openRecentSession"
@openBatchExecution="openBatchExecution"
@@ -154,6 +154,7 @@ import { getOfUserStat } from '@/modules/oneterm/api/stat'
import { getPreference } from '@/modules/oneterm/api/preference.js'
import { getAccountList } from '@/modules/oneterm/api/account'
import { getConfig } from '@/modules/oneterm/api/config'
import { getAssetPermissions } from '@/modules/oneterm/api/asset'
import { defaultPreferenceSetting } from '../systemSettings/terminalDisplay/constants.js'
import { WORKSTATION_TAB_TYPE } from './constants.js'
import FullScreenMixin from '@/modules/oneterm/mixins/fullScreenMixin'
@@ -289,23 +290,27 @@ export default {
this.selectedKeys = keys
},
openTerminal(data) {
async openTerminal(data) {
const id = uuidv4()
const accountName = this.getAccountName(data.accountId)
const name = accountName ? `${accountName}@${data.assetName}` : data.assetName
const permissions = await this.getAssetPermissions(data.assetId, data.accountId)
this.terminalList.push({
...data,
socketStatus: true,
id,
name,
type: this.getConnectType(data.protocolType)
type: this.getConnectType(data.protocolType),
permissions: permissions?.[data.accountId] || {}
})
this.tabActiveKey = id
},
openTerminalList(data) {
async openTerminalList(data) {
const permissions = await this.getAssetPermissions(data.assetId, data.accountList.map((id) => id).join(','))
const newList = data.accountList.map((id) => {
const accountName = this.getAccountName(id)
const name = accountName ? `${accountName}@${data.assetName}` : data.assetName
@@ -318,13 +323,35 @@ export default {
accountId: id,
socketStatus: true,
id: uuidv4(),
type: this.getConnectType(data.protocolType)
type: this.getConnectType(data.protocolType),
permissions: permissions?.[id] || {}
}
})
this.tabActiveKey = newList[0].id
this.terminalList.push(...newList)
},
async getAssetPermissions(assetId, account_ids) {
const defaultPermissions = this.controlConfig?.default_permissions
const permissions = {}
try {
const res = await getAssetPermissions(assetId, { account_ids })
const data = res?.data?.results || {}
Object.keys(data).forEach((accountId) => {
const permissionData = data?.[accountId]?.results || {}
permissions[accountId] = {}
Object.keys(defaultPermissions).forEach((permissionType) => {
permissions[accountId][permissionType] = permissionData?.[permissionType]?.allowed ?? defaultPermissions?.[permissionType] ?? false
})
})
} catch (error) {
console.error('getAssetPermissions error', error)
}
return permissions
},
closeTerminal(item, index) {
if (item.id === this.tabActiveKey) {
this.tabActiveKey = index === 0 ? WORKSTATION_TAB_TYPE.MY_ASSETS : this.terminalList[index - 1].id

View File

@@ -7,5 +7,6 @@ export const OPERATION_MENU_TYPE = {
QUICK_COMMAND: 'quickCommand',
FILE_MANAGEMENT: 'fileManagement',
CLIPBOARD: 'clipboard',
RESOLUTION: 'resolution'
RESOLUTION: 'resolution',
SHARE: 'share'
}

View File

@@ -60,6 +60,17 @@
class="workstation-operation-menu-divider"
></a-divider>
<a-tooltip
v-if="controlDisplayList.includes(OPERATION_MENU_TYPE.SHARE)"
:title="$t('oneterm.workStation.assetShare')"
placement="left"
>
<ops-icon
type="veops-share"
@click="shareAsset"
/>
</a-tooltip>
<a-tooltip
v-if="controlDisplayList.includes(OPERATION_MENU_TYPE.QUICK_COMMAND)"
:title="$t('oneterm.quickCommand.name')"
@@ -109,19 +120,26 @@
:accountList="accountList"
@ok="openBatchExecution"
/>
<ShareAssetModal
ref="shareAssetModalRef"
:assetData="currentTabData"
/>
</div>
</template>
<script>
import { WORKSTATION_TAB_TYPE } from '@/modules/oneterm/views/workStation/constants.js'
import { OPERATION_MENU_TYPE } from './constants.js'
import { PERMISSION_TYPE } from '@/modules/oneterm/views/systemSettings/accessControl/constants.js'
import ChooseAssetsModal from '../batchExecution/chooseAssetsModal.vue'
import ShareAssetModal from './shareAssetModal.vue'
export default {
name: 'OperationMenu',
components: {
ChooseAssetsModal
ChooseAssetsModal,
ShareAssetModal
},
props: {
openFullScreen: {
@@ -135,10 +153,6 @@ export default {
currentTabData: {
type: Object,
default: () => {}
},
controlConfig: {
type: Object,
default: () => {}
}
},
data() {
@@ -155,6 +169,8 @@ export default {
return this.currentTabData?.type === WORKSTATION_TAB_TYPE.GUACAMOLE
},
controlDisplayList() {
const assetPermissions = this.currentTabData?.permissions || {}
const controlDisplayList = [
OPERATION_MENU_TYPE.FULL_SCREEN,
OPERATION_MENU_TYPE.RECENT_SESSION,
@@ -167,15 +183,22 @@ export default {
controlDisplayList.push(OPERATION_MENU_TYPE.RESOLUTION)
}
const showShare = assetPermissions?.[PERMISSION_TYPE.SHARE] || false
if (showShare && (this.isGuacamole || this.isTerminal)) {
controlDisplayList.push(OPERATION_MENU_TYPE.SHARE)
}
if (this.isTerminal) {
controlDisplayList.push(OPERATION_MENU_TYPE.QUICK_COMMAND)
}
if (['ssh', 'rdp'].includes(this.currentTabData?.protocolType)) {
const showUpload = assetPermissions?.[PERMISSION_TYPE.FILE_UPLOAD] || false
const showDownload = assetPermissions?.[PERMISSION_TYPE.FILE_DOWNLOAD] || false
if (['ssh', 'rdp'].includes(this.currentTabData?.protocolType) && (showUpload || showDownload)) {
controlDisplayList.push(OPERATION_MENU_TYPE.FILE_MANAGEMENT)
}
const showClipboard = this?.controlConfig?.[`${this.currentTabData?.protocolType}_config`]?.paste
const showClipboard = assetPermissions?.[PERMISSION_TYPE.PASTE] || false
if (this.isGuacamole && showClipboard) {
controlDisplayList.push(OPERATION_MENU_TYPE.CLIPBOARD)
}
@@ -211,6 +234,9 @@ export default {
},
callComponentFn(name) {
this.$emit('callComponentFn', name)
},
shareAsset() {
this.$refs.shareAssetModalRef.open()
}
}
}

View File

@@ -0,0 +1,184 @@
<template>
<a-modal
:title="$t('oneterm.assetList.createTempLink')"
:visible="visible"
:confirmLoading="confirmLoading"
:okText="linkText ? $t('confirm') : $t('create')"
@cancel="handleCancel"
@ok="handleOk"
>
<div
v-if="linkText"
class="temp-link-content"
>
<span class="temp-link-content-label">{{ $t('oneterm.assetList.tempLink') }}: </span>
<span class="temp-link-content-text">
{{ linkText }}
<a @click="copyLink">
<ops-icon type="veops-copy"/>
</a>
</span>
</div>
<a-form-model
v-else
ref="createTempLinkForm"
:model="form"
:rules="rules"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 19 }"
>
<a-form-model-item :label="$t(`oneterm.assetList.validTime`)" prop="validTime">
<a-range-picker
v-model="form.validTime"
:show-time="{ format: 'HH:mm' }"
format="YYYY-MM-DD HH:mm"
>
<a-icon slot="suffixIcon" type="calendar" />
</a-range-picker>
</a-form-model-item>
<a-form-model-item :label="$t(`oneterm.assetList.times`)" prop="timesRadio">
<a-radio-group v-model="form.timesRadio">
<a-radio class="temp-link-times" value="fixed">
<div class="temp-link-times-fixed">
<span>{{ $t('oneterm.assetList.fixed') }}</span>
<a-input-number
v-model="form.times"
:min="1"
:max="9999"
/>
<span>{{ $t('oneterm.assetList.times2') }}</span>
</div>
</a-radio>
<a-radio class="temp-link-times" value="any">
{{ $t('oneterm.assetList.any') }}
</a-radio>
</a-radio-group>
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import moment from 'moment'
import { postShareLink } from '@/modules/oneterm/api/connect'
export default {
name: 'ShareAssetModal',
props: {
assetData: {
type: Object,
default: () => {}
}
},
data() {
return {
visible: false,
confirmLoading: false,
linkText: '',
form: {
validTime: [moment(), moment().add(1, 'day')],
times: 1,
timesRadio: 'fixed'
},
rules: {
validTime: [
{
required: true,
message: this.$t('placeholder2')
}
],
timesRadio: [
{
required: true,
message: this.$t('placeholder2')
}
]
}
}
},
methods: {
open() {
this.visible = true
this.form = {
validTime: [moment(), moment().add(1, 'day')],
times: 1,
timesRadio: 'fixed'
}
},
copyLink() {
this.$copyText(this.linkText)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
},
handleCancel() {
this.confirmLoading = false
this.linkText = ''
this.visible = false
},
handleOk() {
if (this.linkText) {
this.handleCancel()
return
}
this.$refs.createTempLinkForm.validate(async (valid) => {
if (!valid) {
return
}
this.confirmLoading = true
const start = this.form.validTime[0].format()
const end = this.form.validTime[1].format()
const times = this.form.timesRadio === 'fixed' ? this.form.times : 0
const protocol = this.assetData.protocolType
const params = [
{
account_id: Number(this.assetData.accountId),
asset_id: this.assetData.assetId,
protocol,
start,
end,
times,
no_limit: this.form.timesRadio === 'any'
}
]
const res = await postShareLink(params)
const shareId = res?.list?.[0]
if (shareId) {
this.$message.success(this.$t('createSuccess'))
this.linkText = `${document.location.origin}/oneterm/share/${protocol}/${shareId}`
}
this.confirmLoading = false
})
}
}
}
</script>
<style lang="less" scoped>
.temp-link-content {
display: flex;
&-label {
flex-shrink: 0;
margin-right: 12px;
}
&-text {
font-weight: 600;
}
}
.temp-link-times {
display: flex;
align-items: center;
&-fixed {
display: flex;
align-items: center;
gap: 12px;
}
}
</style>