Merge pull request #835 from jumpserver/dev

v2.11.0 rc1
This commit is contained in:
Jiangjie.Bai 2021-06-10 14:01:52 +08:00 committed by GitHub
commit 00cf2c34b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1327 additions and 781 deletions

BIN
dump.rdb Normal file

Binary file not shown.

View File

@ -46,6 +46,7 @@
"lodash.set": "^4.3.2",
"lodash.topairs": "^4.3.0",
"lodash.values": "^4.3.0",
"moment": "^2.29.1",
"moment-parseformat": "^3.0.0",
"normalize.css": "7.0.0",
"npm": "^7.8.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -8,7 +8,6 @@
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title>
</head>
<body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,7 +1,7 @@
<template>
<div>
<div>
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
<ListTable ref="ListTable" :table-config="iTableConfig" :header-actions="headerActions" />
<Dialog v-if="showMFADialog" width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true">
<div v-if="MFAConfirmed">
<el-form label-position="right" label-width="80px" :model="MFAInfo">
@ -93,6 +93,10 @@ export default {
hasClone: {
type: Boolean,
default: true
},
tableConfig: {
type: Object,
default: () => ({})
}
},
data() {
@ -114,36 +118,31 @@ export default {
password: '',
private_key: ''
},
tableConfig: {
defaultTableConfig: {
url: this.url,
columns: [
{
prop: 'hostname',
columns: ['hostname', 'ip', 'username', 'version', 'date_created', 'actions'],
columnsMeta: {
'hostname': {
label: this.$t('assets.Hostname'),
showOverflowTooltip: true
},
{
prop: 'ip',
'ip': {
label: this.$t('assets.ip'),
width: '120px'
},
{
prop: 'username',
'username': {
label: this.$t('assets.Username'),
showOverflowTooltip: true
},
{
prop: 'version',
'version': {
label: this.$t('assets.Version'),
width: '70px'
},
{
prop: 'date_created',
'date_created': {
label: this.$t('assets.date_joined'),
formatter: DateFormatter
},
{
prop: 'id',
'actions': {
label: this.$t('common.Action'),
align: 'center',
width: 150,
@ -211,7 +210,7 @@ export default {
]
}
}
],
},
extraQuery: {
latest: 1
}
@ -256,16 +255,22 @@ export default {
const ttl = this.publicSettings.SECURITY_MFA_VERIFY_TTL
const now = new Date()
return !(this.MFAVerifyAt && (now - this.MFAVerifyAt) < ttl * 1000)
},
iTableConfig() {
const columnsMeta = Object.assign({}, this.defaultTableConfig.columnsMeta, this.tableConfig.columnsMeta || {})
const config = Object.assign(this.defaultTableConfig, this.tableConfig)
config.columnsMeta = columnsMeta
return config
}
},
watch: {
url(iNew) {
this.$set(this.tableConfig, 'url', iNew)
this.$set(this.iTableConfig, 'url', iNew)
}
},
mounted() {
if (this.otherActions) {
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
const actionColumn = this.iTableConfig.columns[this.iTableConfig.columns.length - 1]
for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item)
}

View File

@ -37,12 +37,11 @@
class="action-item"
@click="handleClick(action)"
>
<el-tooltip v-if="action.tip" effect="dark" :content="action.tip" placement="top">
<i v-if="action.fa" :class="'fa ' + action.fa" />{{ action.title }}
<el-tooltip :disabled="!action.tip" :content="action.tip" placement="top">
<span>
<i v-if="action.fa" :class="'fa ' + action.fa" />{{ action.title }}
</span>
</el-tooltip>
<span v-else>
<i v-if="action.fa" :class="'fa ' + action.fa" />{{ action.title }}
</span>
</el-button>
</template>

View File

@ -74,7 +74,7 @@ export default {
}
.dialog-footer {
padding-right: 50px;
padding-right: 20px;
}
</style>

View File

@ -207,7 +207,7 @@ export default {
if (!d) {
return 0
}
if (!itemColData || !itemColData.length) {
if (typeof itemColData !== 'number' && (!itemColData || !itemColData.length)) {
return 0
}
return itemColData.length

View File

@ -144,10 +144,11 @@ export default {
let actions = [...this.defaultActions, ...this.extraActions]
actions = _.cloneDeep(actions)
actions = actions.map((v) => {
v.has = this.cleanBoolean(v, 'has')
v.can = this.cleanBoolean(v, 'can')
v.callback = this.cleanCallback(v)
v.has = this.cleanBoolean(v, 'has', true)
v.can = this.cleanBoolean(v, 'can', true)
v.callback = this.cleanCallback(v, 'callback')
v.order = v.order || 100
v.tip = this.cleanValue(v, 'tip')
return v
})
actions = actions.filter((v) => v.has)
@ -168,15 +169,15 @@ export default {
}
},
methods: {
cleanBoolean(item, attr) {
cleanBoolean(item, attr, defaults) {
const ok = item[attr]
if (typeof ok !== 'function') {
return ok === undefined ? true : ok
return ok === undefined ? defaults : ok
}
return ok(this.row, this.cellValue)
return this.cleanValue(item, attr)
},
cleanCallback(item) {
const callback = item.callback
cleanCallback(item, attr) {
const callback = item[attr]
const attrs = {
reload: this.reload,
row: this.row,
@ -185,6 +186,20 @@ export default {
tableData: this.tableData
}
return () => { return callback.bind(this)(attrs) }
},
cleanValue(item, attr) {
const value = item[attr]
if (!value || typeof value !== 'function') {
return value
}
const attrs = {
reload: this.reload,
row: this.row,
col: this.col,
cellValue: this.cellValue,
tableData: this.tableData
}
return value(attrs)
}
}
}

View File

@ -522,6 +522,9 @@
},
"route": {
"": "",
"Accounts": "账号管理",
"AssetAccount": "资产账号",
"ApplicationAccount": "应用账号",
"Ticket":"工单",
"CommandConfirm": "命令复核",
"AdminUserCreate": "创建管理用户",
@ -646,7 +649,8 @@
"Users": "用户管理",
"WebFTP": "文件管理",
"WebTerminal": "Web终端",
"Notifications": "通知"
"Notifications": "通知",
"SiteMessageList": "站内信"
},
"sessions": {
"StorageConfiguration": "存储配置",
@ -709,6 +713,7 @@
"common": "普通"
},
"Monitor": "监控",
"XRDPNotSupport": "RDP 客户端会话, 暂不支持监控",
"sessionMonitor": "监控",
"TerminateTaskSendSuccessMsg": "终断任务已下发,请稍后刷新查看",
"helpText": {
@ -1031,7 +1036,15 @@
"MessageType": "消息类型",
"Receivers": "接收人",
"Subscription": "消息订阅",
"ChangeReceiver": "修改消息接收人"
"ChangeReceiver": "修改消息接收人",
"Subject": "主题",
"Message": "消息",
"DeliveryTime": "发送时间",
"HasRead": "是否已读",
"Sender": "发送人",
"MarkAsRead": "标记已读",
"NoUnreadMsg": "暂无未读消息",
"SiteMessage": "站内信"
},
"xpack": {
"Admin": "管理员",

View File

@ -520,6 +520,9 @@
},
"route": {
"": "",
"Accounts": "Accounts",
"AssetAccount": "Asset account",
"ApplicationAccount": "Application account",
"Ticket": "Tickets",
"CommandConfirm": "Command confirm",
"AdminUserCreate": "Admin user create",
@ -644,7 +647,8 @@
"Users": "Users",
"WebFTP": "WebFTP",
"WebTerminal": "Web Terminal",
"Notifications": "Notifications"
"Notifications": "Notifications",
"SiteMessageList": "Site message"
},
"sessions": {
"StorageConfiguration": "Storage configuration",
@ -708,6 +712,7 @@
},
"Monitor": "Monitor",
"sessionMonitor": "Session Monitor",
"XRDPNotSupport": "RDP Client session not support now",
"TerminateTaskSendSuccessMsg": "Terminate task has been send, Please check later",
"helpText": {
"esUrl": "Tip: If you have multiple hosts, use comma (,) to split (eg: http://www.jumpserver.a.com,http://www.jumpserver.b.com)",
@ -1025,7 +1030,15 @@
"MessageType": "Message Type",
"Receivers": "Receivers",
"Subscription": "Subscription",
"ChangeReceiver": "Change Receivers"
"ChangeReceiver": "Change Receivers",
"Subject": "Subject",
"Message": "Message",
"DeliveryTime": "Delivery time",
"HasRead": "Has read",
"Sender": "Sender",
"MarkAsRead": "Mark as read",
"NoUnreadMsg": "No unread messages",
"SiteMessage": "Site messages"
},
"xpack": {
"Admin": "Admin",

View File

@ -2,13 +2,10 @@
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
<transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo">
<h1 v-else class="sidebar-title">{{ title }}</h1>
<img :src="logoSrc" class="sidebar-logo">
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img :src="logoSrc" class="sidebar-logo-text">
<!-- <img v-else-if="logoText" :src="logoText" class="sidebar-logo-text">-->
<!-- <h1 class="sidebar-title">{{ title }}</h1>-->
<img :src="logoTextSrc" class="sidebar-logo-text">
</router-link>
</transition>
</div>
@ -26,10 +23,6 @@ export default {
},
data() {
return {
title: 'JumpServer',
logoText: require('@/assets/img/logo-text.png'),
logo: require('@/assets/img/logo.png'),
xpackData: {}
}
},
computed: {
@ -37,16 +30,14 @@ export default {
'publicSettings'
]),
// eslint-disable-next-line vue/return-in-computed-property
logoTextSrc() {
return this.publicSettings.LOGO_URLS.logo_index
},
logoSrc() {
if (this.publicSettings.LOGO_URLS.logo_index !== '/static/img/logo_text.png') {
return this.publicSettings.LOGO_URLS.logo_index
} else {
return this.logoText
}
return this.publicSettings.LOGO_URLS.logo_logout
}
},
created() {
}
}
</script>

View File

@ -10,7 +10,6 @@
</template>
<script>
export default {
name: 'Language',
data() {
@ -47,11 +46,22 @@ export default {
}
},
mounted() {
if (this.currentLang.code !== this.$i18n.locale) {
this.changeLangTo(this.currentLang)
}
this.changeLang()
this.changeMomentLang()
},
methods: {
changeLang() {
if (this.currentLang.code !== this.$i18n.locale) {
this.changeLangTo(this.currentLang)
}
},
changeMomentLang() {
if (this.currentLang.code.indexOf('en') > -1) {
this.$moment.locale('en')
} else {
this.$moment.locale('zh-cn')
}
},
changeLangTo(item) {
this.$i18n.locale = item.code
localStorage.setItem('lang', item.code)

View File

@ -0,0 +1,262 @@
<template>
<div>
<el-badge :value="unreadMsgCount" :hidden="unreadMsgCount === 0" :max="99" size="mini" type="primary">
<a style="color: #606266 !important; width: 30px" @click="toggleDrawer">
<i class="el-icon-message" style="font-size: 18px" />
</a>
</el-badge>
<el-drawer
:visible.sync="show"
:before-close="handleClose"
:modal="false"
:title="$t('notifications.SiteMessage')"
custom-class="site-msg"
size="25%"
@open="getMessages"
>
<div v-if="unreadMsgCount !== 0" class="msg-list">
<div
v-for="msg of messages"
:key="msg.id"
class="msg-item"
:class="msg.has_read ? 'msg-read' : 'msg-unread'"
@mouseover="hoverMsgId = msg.id"
@mouseleave="hoverMsgId = ''"
@click="showMsgDetail(msg)"
>
<div class="msg-item-head">
<span class="msg-item-head-type">
<i :class="msg.has_read ? 'fa-envelope-open-o' : 'fa-envelope'" class="fa msg-icon" />
{{ msg.subject }}
</span>
<span v-if="hoverMsgId !== msg.id || msg.has_read" class="msg-item-head-time">
{{ formatDate(msg.date_created) }}
</span>
<div v-else class="msg-item-read-btn" @click.stop="markAsRead(msg)">
<a>{{ $t('notifications.MarkAsRead') }}</a>
</div>
</div>
<div class="msg-item-txt">
<span v-html="msg.message" />
</div>
</div>
</div>
<div v-else class="no-msg">
{{ $t('notifications.NoUnreadMsg') }}
</div>
</el-drawer>
<Dialog
v-if="msgDetailVisible"
:visible.sync="msgDetailVisible"
:title="''"
:close-on-click-modal="false"
:confirm-title="$t('notifications.MarkAsRead')"
@confirm="markAsRead(currentMsg)"
@cancel="cancelRead"
>
<div class="msg-detail">
<div class="msg-detail-head">
<h3>{{ currentMsg.subject }}</h3>
<h5>
<span class="msg-detail-time">{{ formatDate(currentMsg.date_created) }}</span>
</h5>
</div>
<div class="msg-detail-txt">
<span v-html="currentMsg.message" />
</div>
</div>
</Dialog>
</div>
</template>
<script>
import { toSafeLocalDateStr } from '@/utils/common'
import Dialog from '@/components/Dialog'
export default {
name: 'SiteMessages',
components: { Dialog },
data() {
return {
show: false,
messages: [],
hoverMsgId: '',
msgDetailVisible: false,
currentMsg: null,
unreadMsgCount: 0
}
},
mounted() {
this.enablePullMsgCount()
},
methods: {
handleClose() {
this.show = false
},
toggleDrawer() {
this.show = !this.show
},
showMsgDetail(msg) {
this.currentMsg = msg
this.msgDetailVisible = true
},
getMessages() {
const url = '/api/v1/notifications/site-message/?offset=0&limit=15&has_read=false'
this.$axios.get(url).then(resp => {
this.messages = [...resp.results]
this.unreadMsgCount = resp.count
})
},
formatDate(s) {
if (!s) {
return ''
}
const d = new Date(s)
const now = new Date()
if (now.getTime() - d.getTime() > (3600 * 24 * 7) * 1000) {
return toSafeLocalDateStr(s)
} else {
return this.$moment(d).fromNow()
}
},
markAsRead(msg) {
console.log(`${msg}`)
const url = `/api/v1/notifications/site-message/mark-as-read/`
this.$axios.patch(url, { ids: [msg.id] }).then(res => {
this.msgDetailVisible = false
this.getMessages()
}).catch(err => {
this.$message(err.detail)
})
},
cancelRead() {
this.msgDetailVisible = false
},
pullMsgCount() {
const url = '/api/v1/notifications/site-message/unread-total/'
this.$axios.get(url).then(res => {
this.unreadMsgCount = res.total
}).catch(err => {
this.$message(err.detail)
})
},
enablePullMsgCount() {
this.pullMsgCount()
setInterval(() => {
this.pullMsgCount()
}, 10000)
}
}
}
</script>
<style lang="scss" scoped>
.el-badge ::v-deep .el-badge__content.is-fixed{
top:10px;
}
.msg-list {
padding: 0 25px 20px;
}
>>> .site-msg {
.el-drawer__header {
border-bottom: solid 1px rgb(231, 234, 239);
margin-bottom: 0;
padding-top: 10px;
font-size: 16px;
}
.el-drawer__body {
overflow-y: auto;
}
}
.msg-item {
border-bottom: solid 1px rgb(231, 234, 239);
padding: 15px 0 10px;
position: relative;
border-bottom: 1px solid #ddd;
cursor: pointer;
&:hover {
background-color: #f2f2f2;
padding: 15px 20px 10px;
margin: 0 -20px;
border-bottom: 1px solid #fff;
}
.msg-icon {
font-size: 13px;
line-height: 13px;
}
&.msg-unread {
.msg-item-txt {
font-weight: bolder;
}
}
}
.msg-item-head {
line-height: 20px;
color: #888;
font-size: 12px;
&:after {
clear: both;
content: ".";
display: block;
height: 0;
overflow: hidden;
}
.msg-item-head-type {
float: left;
width: 120px;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
}
.msg-item-head-time {
float: right;
}
.msg-item-read-btn {
float: right;
}
}
.msg-item-txt {
overflow: hidden;
color: #000;
padding: 4px 0 0;
line-height: 21px;
max-height: 21px;
display: -webkit-box;
font-size: 12px;
display: block;
}
.msg-detail {
padding-left: 20px;
.msg-detail-time {
font-weight: 400;
font-size: 12px;
line-height: 1.1;
}
.msg-detail-txt {
margin-bottom: 20px;
line-height: 25px;
}
}
.no-msg {
padding-top: 20px;
text-align: center;
}
>>> :focus{ outline:0; }
</style>

View File

@ -3,30 +3,26 @@
<div class="navbar-header">
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
</div>
<div class="navbar-right">
<div class="header-item">
<ul class="navbar-right">
<li class="header-item header-icon">
<SiteMessages />
</li>
<li class="header-item" style="margin-left: 10px">
<Help />
</div>
<div class="header-item">
</li>
<li class="header-item">
<Language />
</div>
<div
v-if="
publicSettings.TICKETS_ENABLED
&& publicSettings.XPACK_LICENSE_IS_VALID
&& !isOrgAuditor
"
class="header-item"
>
</li>
<li v-if="showTickets" class="header-item">
<Tickets />
</div>
<div class="header-item">
</li>
<li class="header-item">
<WebTerminal />
</div>
<div class="header-item header-profile">
</li>
<li class="header-item header-profile">
<AccountDropdown />
</div>
</div>
</li>
</ul>
</div>
</template>
@ -34,6 +30,7 @@
import { mapGetters } from 'vuex'
import Hamburger from '@/components/Hamburger'
import AccountDropdown from './AccountDropdown'
import SiteMessages from './SiteMessages'
import Help from './Help'
import Language from './Language'
import WebTerminal from './WebTerminal'
@ -48,7 +45,8 @@ export default {
Language,
Help,
Tickets,
WebTerminal
WebTerminal,
SiteMessages
},
data() {
return {
@ -60,13 +58,17 @@ export default {
]),
isOrgAuditor() {
return rolc.getRolesDisplay(this.currentOrgRoles).includes('OrgAuditor') || rolc.getRolesDisplay(this.currentOrgRoles).includes('Auditor')
},
showTickets() {
return this.publicSettings.TICKETS_ENABLED &&
this.publicSettings.XPACK_LICENSE_IS_VALID &&
!this.isOrgAuditor
}
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
}
}
}
</script>
@ -91,12 +93,19 @@ export default {
.navbar-right {
float: right;
margin-right: 10px;
}
.header-item {
line-height: 50px;
display: inline-block;
padding-right: 20px;
.header-item {
line-height: 50px;
display: inline-block;
padding-right: 10px;
padding-left: 10px;
}
.header-icon {
&:hover {
background-color: #e6e6e6;
}
}
}
.breadcrumb-container {
@ -108,5 +117,9 @@ export default {
.el-header {
background-color: #ffffff;
}
ul {
margin: 0;
}
</style>

View File

@ -40,8 +40,13 @@ Vue.config.productionTip = false
import VueCookie from 'vue-cookie'
Vue.use(VueCookie)
window.$cookie = VueCookie
import VueMoment from 'vue-moment'
Vue.use(VueMoment)
const moment = require('moment')
require('moment/locale/zh-cn')
Vue.use(require('vue-moment'), {
moment
})
// logger
import VueLogger from 'vuejs-logger'
import loggerOptions from './utils/logger'

120
src/router/accounts.js Normal file
View File

@ -0,0 +1,120 @@
import i18n from '@/i18n/i18n'
import empty from '@/layout/empty'
export default [
{
path: 'asset-accounts',
component: empty,
meta: { title: i18n.t('route.AssetAccount') },
children: [
{
path: '',
name: 'AssetAccountList',
component: () => import('@/views/accounts/AssetAccount/AssetAccountList'),
meta: { title: i18n.t('route.AssetAccount') }
}
]
},
{
path: 'application-accounts',
component: empty,
meta: { title: i18n.t('route.AssetAccount') },
children: [
{
path: '',
name: 'ApplicationAccountList',
component: () => import('@/views/accounts/ApplicationAccount/ApplicationAccountList'),
meta: { title: i18n.t('route.ApplicationAccount') }
}
]
},
{
path: 'gathered-user',
component: empty,
redirect: '',
meta: { title: i18n.t('xpack.GatherUser.GatherUserList') },
children: [
{
path: '',
component: () => import('@/views/accounts/GatheredUser/index'),
name: 'GatherUserListIndex',
meta: { title: i18n.t('xpack.GatherUser.GatherUser'), activeMenu: '/accounts/gathered-user' }
},
{
path: '',
component: () => import('@/views/accounts/GatheredUser/GatheredUserList'),
name: 'GatherUserList',
hidden: true,
meta: { title: i18n.t('xpack.GatherUser.GatherUserList'), activeMenu: '/accounts/gathered-user' }
},
{
path: 'tasks',
component: () => import('@/views/accounts/GatheredUser/TaskList'),
name: 'GatherUserTaskList',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskList'), activeMenu: '/accounts/gathered-user' },
hidden: true
},
{
path: 'tasks/:id',
component: () => import('@/views/accounts/GatheredUser/TaskDetail/index'),
name: 'GatherUserTaskDetail',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskDetail'), activeMenu: '/accounts/gathered-user' },
hidden: true
},
{
path: 'tasks/create',
component: () => import('@/views/accounts/GatheredUser/TaskCreateUpdate'),
name: 'GatherUserTaskCreate',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskCreate'), activeMenu: '/accounts/gathered-user' },
hidden: true
},
{
path: 'tasks/:id/update',
component: () => import('@/views/accounts/GatheredUser/TaskCreateUpdate'),
name: 'GatherUserTaskUpdate',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskUpdate'), action: 'update', activeMenu: '/accounts/gathered-user' },
hidden: true
}
]
},
{
path: 'change-auth-plan',
component: empty,
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' },
children: [
{
path: 'plan',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanList.vue'),
name: 'ChangeAuthPlanList',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' }
},
{
path: 'plan/create',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'ChangeAuthPlanCreate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanCreate'), activeMenu: '/accounts/change-auth-plan/plan', action: 'create' },
hidden: true
},
{
path: 'plan/:id/update',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'ChangeAuthPlanUpdate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanUpdate'), activeMenu: '/accounts/change-auth-plan/plan', action: 'update' },
hidden: true
},
{
path: 'plan/:id',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanDetail/index.vue'),
name: 'ChangeAuthPlanDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' },
hidden: true
},
{
path: 'plan-execution/:id',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanDetail/ChangeAuthPlanExecution/ChangeAuthPlanExecutionDetail/index.vue'),
name: 'ChangeAuthPlanExecutionDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ExecutionDetail'), activeMenu: '/accounts/change-auth-plan/plan' },
hidden: true
}
]
}
]

View File

@ -37,6 +37,7 @@ import TicketsRoutes from './tickets'
import AuditsRoutes from './audits'
import commonRoutes from './common'
import aclRoutes from './acl'
import AccountRoutes from './accounts'
/**
* constantRoutes
@ -110,6 +111,18 @@ export const allRoleRoutes = [
meta: { title: i18n.t('route.Applications'), icon: 'th' },
children: ApplicationsRoute
},
{
path: '/accounts',
component: Layout,
redirect: '/accounts/asset-accounts/',
name: 'Accounts',
meta: {
licenseRequired: true,
title: i18n.t('route.Accounts'),
icon: 'address-book'
},
children: AccountRoutes
},
{
path: '/perms/',
component: Layout,

View File

@ -1,6 +1,5 @@
module.exports = {
title: 'JumpServer',
title: '.',
/**
* @type {boolean} true | false

View File

@ -35,15 +35,18 @@ const actions = {
getPublicSettings({ commit, state }) {
return new Promise((resolve, reject) => {
getPublicSettings().then(response => {
const link = document.querySelector("link[rel*='icon']") || document.createElement('link')
link.type = 'image/x-icon'
link.rel = 'shortcut icon'
link.href = response.data.LOGO_URLS.favicon
document.getElementsByTagName('head')[0].appendChild(link)
const faviconURL = response.data.LOGO_URLS.favicon
let link = document.querySelector("link[rel*='icon']")
if (!link) {
link = document.createElement('link')
link.type = 'image/x-icon'
link.rel = 'shortcut icon'
document.getElementsByTagName('head')[0].appendChild(link)
}
link.href = faviconURL
// 动态修改Title
if (response.data.LOGIN_TITLE) { document.title = response.data.LOGIN_TITLE }
document.title = response.data.LOGIN_TITLE
commit('SET_PUBLIC_SETTINGS', response.data)
resolve(response)
}).catch(error => {

View File

@ -117,7 +117,7 @@ export default {
name: 'connect',
fa: 'fa-terminal',
type: 'primary',
can: (row, cellValue) => {
can: ({ row, cellValue }) => {
return row.is_active
},
callback: function({ row, col, cellValue, reload }) {

View File

@ -88,11 +88,14 @@ export function toSafeLocalDateStr(d) {
}
export function getApiPath(that) {
const pagePath = that.$route.path
const isOrgPath = pagePath.split('/').indexOf('orgs') !== -1
if (isOrgPath) {
return `/api/v1/orgs/orgs/${pagePath.split('/').pop()}/`
let pagePath = that.$route.path
const pagePathArray = pagePath.split('/')
if (pagePathArray.indexOf('orgs') !== -1) {
pagePathArray[pagePathArray.indexOf('xpack')] = 'orgs'
} else if (pagePathArray.indexOf('gathered-user') !== -1 || pagePathArray.indexOf('change-auth-plan') !== -1) {
pagePathArray[pagePathArray.indexOf('accounts')] = 'xpack'
}
pagePath = pagePathArray.join('/')
return `/api/v1${pagePath}/`
}

View File

@ -1,4 +1,4 @@
import getPageTitle from '@/utils/get-page-title'
// import getPageTitle from '@/utils/get-page-title'
import store from '@/store'
import router from '@/router'
import { Message } from 'element-ui'
@ -15,9 +15,9 @@ function reject(msg) {
return new Promise((resolve, reject) => reject(msg))
}
function setHeadTitle({ to, from, next }) {
document.title = getPageTitle(to.meta.title)
}
// function setHeadTitle({ to, from, next }) {
// document.title = getPageTitle(to.meta.title)
// }
async function checkLogin({ to, from, next }) {
if (whiteList.indexOf(to.path) !== -1) {
@ -145,7 +145,7 @@ export async function startup({ to, from, next }) {
// set page title
await getPublicSetting({ to, from, next })
await setHeadTitle({ to, from, next })
// await setHeadTitle({ to, from, next })
await checkLogin({ to, from, next })
await changeCurrentOrgIfNeed({ to, from, next })
await changeCurrentRoleIfNeed({ to, from, next })

View File

@ -0,0 +1,247 @@
<template>
<Page>
<el-row>
<el-col :span="11">
<GenericListTable
ref="LeftTable"
class="application-table"
:header-actions="leftTable.headerActions"
:table-config="leftTable.tableConfig"
@row-click="leftTable.tableConfig.rowClick"
/>
</el-col>
<el-col :span="13">
<GenericListTable
ref="RightTable"
class="application-user-table"
:header-actions="rightTable.headerActions"
:table-config="rightTable.tableConfig"
/>
</el-col>
</el-row>
<Dialog v-if="showMFADialog" width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true">
<div v-if="MFAConfirmed">
<el-form label-position="right" label-width="80px" :model="MFAInfo">
<el-form-item :label="this.$t('assets.Applications')">
<el-input v-model="MFAInfo.application" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="MFAInfo.username" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="MFAInfo.password" type="password" show-password />
</el-form-item>
</el-form>
</div>
<el-row v-else :gutter="20">
<el-col :span="4">
<div style="line-height: 34px;text-align: center">MFA</div>
</el-col>
<el-col :span="14">
<el-input v-model="MFAInput" />
<span class="help-tips help-block">{{ $t('common.MFARequireForSecurity') }}</span>
</el-col>
<el-col :span="4">
<el-button size="mini" type="primary" style="line-height:20px " @click="MFAConfirm">{{ this.$t('common.Confirm') }}</el-button>
</el-col>
</el-row>
</Dialog>
</Page>
</template>
<script>
import Page from '@/layout/components/Page'
import GenericListTable from '@/layout/components/GenericListTable'
import { ActionsFormatter, DetailFormatter } from '@/components/TableFormatters'
import Dialog from '@/components/Dialog'
import { mapGetters } from 'vuex'
export default {
name: 'AssetAccountList',
components: {
GenericListTable, Page, Dialog
},
data() {
const vm = this
return {
showMFADialog: false,
MFAConfirmed: false,
MFAInput: '',
MFAInfo: {
systemUser: '',
application: '',
username: '',
password: ''
},
clickedRow: {},
leftTable: {
tableConfig: {
url: '/api/v1/applications/applications/',
columns: [
'name', 'category_display', 'type_display', 'comment'
],
columnsShow: {
min: ['name'],
default: ['name', 'category_display', 'type_display']
},
columnsMeta: {
name: {
formatter: DetailFormatter,
formatterArgs: {
getRoute({ row, col, cellValue }) {
return {
'db': 'DatabaseAppDetail', 'remote_app': 'RemoteAppDetail', 'cloud': 'KubernetesAppDetail'
}[row.category]
}
},
showOverflowTooltip: true,
sortable: false
}
},
tableAttrs: {
stripe: false, //
border: true, //
fit: true, // ,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
if (row === vm.clickedRow) {
return 'row-clicked'
}
return ''
}
},
rowClick: function(row, column, event) {
vm.rightTable.tableConfig.url = `/api/v1/applications/application-users/?application_id=${row.id}`
vm.clickedRow = row
vm.MFAInfo.application = row.name
}
},
headerActions: {
hasLeftActions: false,
hasCreate: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasBulkUpdate: false
}
},
rightTable: {
tableConfig: {
url: `/api/v1/applications/application-users/?application_id=`,
columns: [
'name', 'username', 'username_same_with_user', 'protocol', 'login_mode', 'priority', 'comment', 'actions'
],
columnsShow: {
min: ['name', 'username', 'actions'],
default: ['name', 'username', 'date_created', 'actions']
},
columnsMeta: {
name: {
formatter: DetailFormatter,
formatterArgs: {
route: 'SystemUserDetail'
},
showOverflowTooltip: true,
sortable: false
},
protocol: {
sortable: false
},
login_mode: {
sortable: false
},
actions: {
label: this.$t('common.Action'),
align: 'center',
width: 150,
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: false,
moreActionsTitle: this.$t('common.More'),
extraActions: [
{
name: 'View',
title: this.$t('common.View'),
type: 'primary',
callback: function(val) {
this.MFAInfo.systemUser = val.row
if (!this.needMFAVerify) {
this.showMFADialog = true
this.MFAConfirmed = true
this.$axios.get(`/api/v1/assets/system-users/${this.MFAInfo.systemUser.id}/auth-info/`).then(res => {
this.MFAConfirmed = true
this.MFAInfo.username = res.username
this.MFAInfo.password = res.password
})
} else {
this.showMFADialog = true
}
}.bind(this)
}
]
}
},
tableAttrs: {
stripe: false, //
border: true, //
fit: true, // ,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
return 'row-background-color'
}
}
}
},
headerActions: {
hasLeftActions: false,
hasImport: false
}
}
}
},
computed: {
...mapGetters([
'MFA_TTl',
'MFAVerifyAt',
'publicSettings'
]),
needMFAVerify() {
if (!this.publicSettings.SECURITY_VIEW_AUTH_NEED_MFA) {
return false
}
const ttl = this.publicSettings.SECURITY_MFA_VERIFY_TTL
const now = new Date()
return !(this.MFAVerifyAt && (now - this.MFAVerifyAt) < ttl * 1000)
}
},
methods: {
MFAConfirm() {
if (this.MFAInput.length !== 6) {
return this.$message.error(this.$t('common.MFAErrorMsg'))
}
this.$axios.post(
`/api/v1/authentication/otp/verify/`, {
code: this.MFAInput
}
).then(
res => {
this.$store.dispatch('users/setMFAVerify')
this.$axios.get(`/api/v1/assets/system-users/${this.MFAInfo.systemUser.id}/auth-info/`).then(res => {
this.MFAConfirmed = true
this.MFAInfo.username = res.username
this.MFAInfo.password = res.password
})
}
)
}
}
}
</script>
<style lang="scss" scoped>
.application-table ::v-deep .row-clicked, .application-user-table ::v-deep .row-background-color {
background-color: #f5f7fa;
}
</style>

View File

@ -0,0 +1,176 @@
<template>
<Page>
<el-row>
<el-col v-show="iShowTree" :span="iShowTree?4:0">
<AutoDataZTree
ref="AUtoDataZTree"
:setting="treeSetting"
/>
</el-col>
<el-col :span="iShowTree?9:11">
<div class="mini">
<div style="display:block" class="mini-button" @click="iShowTree=!iShowTree">
<i v-show="iShowTree" class="fa fa-angle-left fa-x" />
<i v-show="!iShowTree" class="fa fa-angle-right fa-x" />
</div>
</div>
<GenericListTable
ref="LeftTable"
class="asset-table"
:header-actions="leftTable.headerActions"
:table-config="leftTable.tableConfig"
@row-click="leftTable.tableConfig.rowClick"
/>
</el-col>
<el-col :span="iShowTree?11:13">
<AssetUserTable
ref="RightTable"
class="asset-user-table"
:url="rightTable.url"
:has-left-actions="rightTable.hasLeftActions"
:table-config="rightTable.tableConfig"
:has-clone="false"
:has-import="false"
/>
</el-col>
</el-row>
</Page>
</template>
<script>
import Page from '@/layout/components/Page'
import GenericListTable from '@/layout/components/GenericListTable'
import AutoDataZTree from '@/components/AutoDataZTree/index'
import { AssetUserTable } from '@/components'
import { DetailFormatter } from '@/components/TableFormatters'
export default {
name: 'AssetAccountList',
components: {
AutoDataZTree, GenericListTable, Page, AssetUserTable
},
data() {
const vm = this
return {
clickedRow: {},
iShowTree: true,
treeSetting: {
showMenu: false,
showRefresh: false,
showAssets: false,
url: '',
treeUrl: '/api/v1/assets/nodes/children/tree/',
callback: {
onSelected: function(event, treeNode) {
vm.leftTable.tableConfig.url = `/api/v1/assets/assets/?node_id=${treeNode.meta.node.id}`
}
}
},
leftTable: {
tableConfig: {
url: '/api/v1/assets/assets/',
columns: [
'hostname', 'ip', 'protocols', 'platform', 'comment'
],
columnsShow: {
min: ['hostname', 'ip'],
default: ['hostname', 'ip', 'platform']
},
columnsMeta: {
hostname: {
formatter: DetailFormatter,
formatterArgs: {
route: 'AssetDetail',
routeQuery: {
activeTab: 'Detail'
}
},
showOverflowTooltip: true
},
ip: {
showOverflowTooltip: true
}
},
tableAttrs: {
stripe: false, //
border: true, //
fit: true, // ,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
if (row === vm.clickedRow) {
return 'row-clicked'
}
return ''
}
},
rowClick: function(row, column, event) {
vm.rightTable.url = `/api/v1/assets/asset-users/?asset_id=${row.id}&latest=1`
vm.clickedRow = row
}
},
headerActions: {
hasLeftActions: false,
hasCreate: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasColumnSetting: true,
hasRefresh: true,
hasBulkUpdate: false
}
},
rightTable: {
url: `/api/v1/assets/asset-users/?hostname=ShowFirstAssetRelated&latest=1`,
tableConfig: {
columns: ['name', 'username', 'version', 'backend_display', 'date_created', 'actions'],
columnsShow: {
min: ['username', 'actions'],
default: ['name', 'username', 'version', 'backend_display', 'date_created', 'actions']
},
columnsMeta: {
name: {
formatter: null,
showOverflowTooltip: true,
sortable: false
}
},
tableAttrs: {
stripe: true, //
border: true, //
fit: true, // ,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
return 'row-background-color'
}
}
},
hasLeftActions: false
}
}
}
}
</script>
<style lang="scss" scoped>
.asset-table ::v-deep .row-clicked, .asset-user-table ::v-deep .row-background-color {
background-color: #f5f7fa;
}
.asset-table {
& >>> .table-content {
margin-left: 21px;
}
}
.mini-button{
width: 12px;
float: left;
margin-right: 10px;
text-align: center;
padding: 9px 0;
background-color: #1ab394;
border-color: #1ab394;
color: #FFFFFF;
border-radius: 5px;
line-height: 1.428;
cursor:pointer;
}
</style>

View File

@ -0,0 +1,203 @@
<template>
<div>
<el-row>
<el-col v-show="iShowTree" :span="iShowTree?4:0">
<AutoDataZTree
ref="AUtoDataZTree"
:setting="treeSetting"
/>
</el-col>
<el-col :span="iShowTree?9:11">
<div class="mini">
<div style="display:block" class="mini-button" @click="iShowTree=!iShowTree">
<i v-show="iShowTree" class="fa fa-angle-left fa-x" />
<i v-show="!iShowTree" class="fa fa-angle-right fa-x" />
</div>
</div>
<GenericListTable
ref="LeftTable"
class="asset-table"
:header-actions="leftTable.headerActions"
:table-config="leftTable.tableConfig"
@row-click="leftTable.tableConfig.rowClick"
/>
</el-col>
<el-col :span="iShowTree?11:13">
<GenericListTable
ref="RightTable"
class="asset-user-table"
:header-actions="rightTable.headerActions"
:table-config="rightTable.tableConfig"
/>
</el-col>
</el-row>
</div>
</template>
<script>
import GenericListTable from '@/layout/components/GenericListTable'
import AutoDataZTree from '@/components/AutoDataZTree/index'
import { ChoicesFormatter, DetailFormatter } from '@/components/TableFormatters'
export default {
name: 'AssetAccountList',
components: {
AutoDataZTree, GenericListTable
},
data() {
const vm = this
return {
clickedRow: {},
iShowTree: true,
treeSetting: {
showMenu: false,
showRefresh: false,
showAssets: false,
url: '',
treeUrl: '/api/v1/assets/nodes/children/tree/',
callback: {
onSelected: function(event, treeNode) {
vm.leftTable.tableConfig.url = `/api/v1/assets/assets/?node_id=${treeNode.meta.node.id}`
}
}
},
leftTable: {
tableConfig: {
url: '/api/v1/assets/assets/',
columns: [
'hostname', 'ip', 'public_ip', 'admin_user_display',
'protocols', 'platform', 'connectivity',
'created_by', 'date_created', 'comment', 'org_name'
],
columnsShow: {
min: ['hostname', 'ip', 'platform'],
default: ['hostname', 'ip', 'connectivity', 'platform']
},
columnsMeta: {
hostname: {
formatter: DetailFormatter,
formatterArgs: {
route: 'AssetDetail',
routeQuery: {
activeTab: 'Detail'
}
},
showOverflowTooltip: true
},
connectivity: {
label: this.$t('assets.Reachable'),
formatter: ChoicesFormatter,
formatterArgs: {
iconChoices: {
0: 'fa-times text-danger',
1: 'fa-check text-primary',
2: 'fa-circle text-warning'
},
typeChange: function(val) {
if (!val) {
return 2
}
return val.status
},
hasTips: true
},
width: '90px',
align: 'center'
}
},
tableAttrs: {
stripe: false, //
border: true, //
fit: true, // ,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
if (row === vm.clickedRow) {
return 'row-clicked'
}
return ''
}
},
rowClick: function(row, column, event) {
vm.rightTable.tableConfig.url = `/api/v1/assets/gathered-users/?asset_id=${row.id}`
vm.clickedRow = row
}
},
headerActions: {
hasLeftActions: false,
hasCreate: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasBulkUpdate: false
}
},
rightTable: {
tableConfig: {
url: `/api/v1/assets/gathered-users/?asset__hostname=ShowFirstAssetRelated`,
columns: [
'username', 'date_last_login', 'present', 'ip_last_login', 'date_updated'
],
columnsShow: {
min: ['username'],
default: [
'username', 'date_last_login', 'present', 'ip_last_login', 'date_updated'
]
},
columnsMeta: {
username: {
showOverflowTooltip: true
},
present: {
width: 80
},
ip_last_login: {
width: 120
}
},
tableAttrs: {
stripe: true, //
border: true, //
fit: true, // ,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
return 'row-background-color'
}
}
},
headerActions: {
hasLeftActions: false,
hasCreate: false,
hasExport: true,
hasImport: false,
hasBulkDelete: false,
hasBulkUpdate: false
}
}
}
}
}
</script>
<style lang="scss" scoped>
.asset-table ::v-deep .row-clicked, .asset-user-table ::v-deep .row-background-color {
background-color: #f5f7fa;
}
.asset-table {
& >>> .table-content {
margin-left: 21px;
}
}
.mini-button{
width: 12px;
float: left;
margin-right: 10px;
text-align: center;
padding: 9px 0;
background-color: #1ab394;
border-color: #1ab394;
color: #FFFFFF;
border-radius: 5px;
line-height: 1.428;
cursor:pointer;
}
</style>

View File

@ -1,16 +1,16 @@
<template>
<div v-loading="loading">
<GenericTreeListPage
ref="GenericTreeListPage"
:table-config="tableConfig"
:header-actions="headerActions"
:tree-setting="treeSetting"
@TreeInitFinish="checkFirstNode"
@TagSearch="handleTagChange"
@TagFilter="handleFilterChange"
@TagDateChange="handleDateChange"
/>
</div>
<GenericTreeListPage
ref="GenericTreeListPage"
v-loading="loading"
:table-config="tableConfig"
:header-actions="headerActions"
:tree-setting="treeSetting"
class="command-list-table"
@TreeInitFinish="checkFirstNode"
@TagSearch="handleTagChange"
@TagFilter="handleFilterChange"
@TagDateChange="handleDateChange"
/>
</template>
<script>
@ -39,6 +39,14 @@ export default {
loading: true,
tableConfig: {
url: '',
tableAttrs: {
rowClassName: ({ row }) => {
if (row.risk_level === 5) {
return 'risk-command'
}
return 'command'
}
},
columns: [
'expandCol', 'input', 'risk_level', 'user',
'asset', 'system_user', 'session', 'timestamp'
@ -51,7 +59,7 @@ export default {
expandCol: {
type: 'expand',
prop: 'output',
label: '>',
label: '',
formatter: OutputExpandFormatter
},
risk_level: {
@ -151,7 +159,6 @@ export default {
}
},
watch: {
},
methods: {
checkFirstNode(obj) {
@ -203,6 +210,13 @@ export default {
}
</script>
<style>
<style lang="scss" scoped>
.command-list-table >>> .risk-command {
background-color: oldlace;
tr {
color: white;
}
}
</style>

View File

@ -17,7 +17,7 @@ export default {
name: 'replay',
title: this.$t('sessions.replay'),
type: 'warning',
can: (row, cellValue) => {
can: ({ row, cellValue }) => {
return row.can_replay
},
callback: function({ row, tableData }) {
@ -30,7 +30,7 @@ export default {
name: 'download',
title: this.$t('sessions.download'),
type: 'primary',
can: (row, cellValue) => {
can: ({ row, cellValue }) => {
return row.can_replay
},
callback: function({ row, tableData }) {

View File

@ -19,7 +19,7 @@ export default {
name: 'terminate',
title: this.$t('sessions.terminate'),
type: 'danger',
can: (row, cellValue) => {
can: ({ row, cellValue }) => {
return row.can_terminate
},
callback: function({ reload, row }) {
@ -39,13 +39,15 @@ export default {
name: 'join',
title: this.$t('sessions.Monitor'),
type: 'primary',
can: (row, cellValue) => {
if (row.protocol === 'rdp' ||
row.protocol === 'vnc') {
return true
}
can: ({ row, cellValue }) => {
return row.can_join
},
tip: ({ row }) => {
if (row.login_from === 'RT') {
return this.$t('sessions.XRDPNotSupport')
}
return ''
},
callback: function({ row, tableData }) {
// luna
if (row.protocol === 'rdp' || row.protocol === 'vnc') {

View File

@ -27,7 +27,8 @@ export default {
fields: [
[
this.$t('common.BasicInfo'), [
'SITE_URL', 'USER_GUIDE_URL', 'FORGOT_PASSWORD_URL', globalOrgName
'SITE_URL', 'USER_GUIDE_URL',
'FORGOT_PASSWORD_URL', globalOrgName
]
]
],

View File

@ -64,7 +64,7 @@ export default {
const selectedUsers = this.selectedUsers.map(item => {
return {
id: item.id,
label: `${item.name}(${item.username})`
label: item.name
}
})
setTimeout(() => {

View File

@ -67,7 +67,7 @@ export default {
}
this.$axios.patch(
`/api/v1/notifications/system/subscriptions/${sub.id}/`,
`/api/v1/notifications/system-msg-subscription/${sub.id}/`,
{ receive_backends: backends }
).catch(err => {
this.$log.error(err)
@ -78,11 +78,10 @@ export default {
onDialogSelectSubmit(userIds) {
this.dialogVisible = false
this.$axios.patch(
`/api/v1/notifications/system/subscriptions/${this.currentEditSub.id}/`,
`/api/v1/notifications/system-msg-subscription/${this.currentEditSub.id}/`,
{ users: userIds }
).then(newSub => {
const msgType = this.idMessageTypeMapper[newSub.id]
msgType.users = newSub.users
const msgType = this.idMessageTypeMapper[newSub.message_type]
msgType.receivers = newSub.receivers
}).catch(err => {
console.log(err)
@ -97,40 +96,37 @@ export default {
this.receiveBackends = await this.$axios.get('/api/v1/notifications/backends/')
},
async initSubscriptions() {
const subscriptions = await this.$axios.get('/api/v1/notifications/system/subscriptions/')
const subscriptions = await this.$axios.get('/api/v1/notifications/system-msg-subscription/')
// app
const appMessageTypesMapper = {}
subscriptions.forEach(sub => {
if (!(sub.message_category in appMessageTypesMapper)) {
appMessageTypesMapper[sub.message_category] = {
id: sub.message_category,
value: sub.message_category_label,
children: [sub]
}
} else {
appMessageTypesMapper[sub.message_category].children.push(sub)
}
})
const trans_subscriptions = []
// sub
for (const app of Object.values(appMessageTypesMapper)) {
app.children = app.children.map(sub => {
for (const category of subscriptions) {
const children = []
trans_subscriptions.push({
id: category.category,
value: category.category_label,
children: children
})
for (const sub of category.children) {
const backendsChecked = {}
this.receiveBackends.forEach(backend => {
backendsChecked[backend.name] = sub.receive_backends.indexOf(backend.name) > -1
})
const subObj = {
id: sub.id,
value: sub.message_label,
const trans_sub = {
id: sub.message_type,
value: sub.message_type_label,
receivers: sub.receivers,
receive_backends: backendsChecked
}
this.idMessageTypeMapper[sub.id] = subObj
return subObj
})
children.push(trans_sub)
this.idMessageTypeMapper[trans_sub.id] = trans_sub
}
}
this.tableData = Object.values(appMessageTypesMapper)
this.tableData = trans_subscriptions
}
}
}

View File

@ -43,11 +43,22 @@ export default {
url: '/api/v1/settings/setting/?category=terminal'
}
},
mounted() {
if (this.$store.getters.hasValidLicense) {
const xRDPFields = [
'XRDP', [
'TERMINAL_RDP_ADDR'
]
]
this.selectFields.splice(1, 0, xRDPFields)
}
},
methods: {
getMethod() {
return 'put'
}
}
}
</script>

View File

@ -69,6 +69,10 @@ export default {
title: this.$t('setting.DingTalk'),
name: 'DingTalk'
},
{
title: this.$t('setting.SystemMessageSubscription'),
name: 'SystemMessageSubscription'
},
{
title: this.$t('setting.Terminal'),
name: 'Terminal'
@ -119,6 +123,9 @@ export default {
case 'License':
this.activeMenu = 'License'
break
case 'SystemMessageSubscription':
this.activeMenu = 'SystemMessageSubscription'
break
default:
this.activeMenu = 'Basic'
break

View File

@ -59,7 +59,7 @@ export default {
password: {
component: UserPassword,
hidden: (formValue) => {
if (formValue.password_strategy) {
if (formValue.password_strategy === 'custom') {
return false
}
return !formValue.update_password
@ -81,7 +81,7 @@ export default {
}
],
hidden: (formValue) => {
if (formValue.password_strategy) {
if (formValue.password_strategy === 'custom') {
return false
}
return !formValue.update_password || !this.user.can_public_key_auth
@ -148,7 +148,7 @@ export default {
methods: {
cleanFormValue(value) {
const method = this.getMethod()
if (method === 'post' && !value.password_strategy) {
if (method === 'post' && value.password_strategy === 'email') {
delete value['password']
if (this.currentOrgIsRoot) {
delete value['groups']

View File

@ -92,7 +92,7 @@ export default {
name: 'remove',
type: 'warning',
has: hasRemove,
can: function(row, cellValue) {
can: ({ row }) => {
return row.can_delete
},
callback: this.removeUserFromOrg

View File

@ -1,78 +0,0 @@
<template>
<TreeTable :table-config="tableConfig" :tree-setting="treeSetting" :header-actions="headerActions" />
</template>
<script>
import TreeTable from '@/components/TreeTable'
export default {
components: {
TreeTable
},
data() {
return {
treeSetting: {
showMenu: false,
showRefresh: true,
showAssets: true,
url: '/api/v1/assets/gathered-users/',
nodeUrl: '/api/v1/assets/nodes/',
// ?assets=0. =1
treeUrl: '/api/v1/assets/nodes/children/tree/?assets=1'
},
tableConfig: {
url: '/api/v1/assets/gathered-users/',
hasTree: true,
columns: [
'hostname', 'ip', 'username', 'date_last_login', 'present',
'ip_last_login', 'date_updated'
],
columnsMeta: {
hostname: {
showOverflowTooltip: true
},
ip: {
width: 120
},
username: {
showOverflowTooltip: true
},
present: {
width: 80
},
ip_last_login: {
width: 120
}
}
},
headerActions: {
hasCreate: false,
hasLeftActions: false,
hasImport: false,
searchConfig: {
exclude: ['asset'],
options: [
{
label: this.$t('assets.Hostname'),
value: 'asset__hostname'
},
{
label: 'IP',
value: 'asset__ip'
}
]
}
}
}
},
methods: {
onGatherUserTasks() {
this.$router.push({ name: 'GatherUserTaskList' })
}
}
}
</script>
<style>
</style>

View File

@ -85,9 +85,10 @@ export default {
componentName() {
const nameMapper = {
koko: 'KoKo',
guacamole: 'Guacamole',
omnidb: 'OmniDB',
lion: 'Lion'
guacamole: 'Guacamole',
lion: 'Lion',
xrdp: 'XRDP'
}
return nameMapper[this.componentMetric.type]
}

View File

@ -1,60 +0,0 @@
<template>
<GenericCreateUpdatePage :fields="fields" :initial="initial" :fields-meta="fieldsMeta" :url="url" />
</template>
<script>
import GenericCreateUpdatePage from '@/layout/components/GenericCreateUpdatePage'
import { UploadKey, Select2 } from '@/components'
export default {
name: 'VaultCreate',
components: {
GenericCreateUpdatePage
},
data() {
return {
url: '/api/v1/assets/asset-users/',
initial: {
},
fields: [
[this.$t('common.Basic'), ['asset', 'username', 'password', 'private_key', 'comment']]
],
fieldsMeta: {
asset: {
label: this.$t('perms.Asset'),
component: Select2,
el: {
multiple: false,
ajax: {
url: '/api/v1/assets/assets/',
transformOption: (item) => {
return { label: `${item.hostname}(${item.ip})`, value: item.id }
}
}
}
},
name: {
el: {
placeholder: this.$t('common.Name')
}
},
username: {
el: {
placeholder: this.$t('common.Username')
}
},
password: {
helpText: this.$t('common.passwordOrPassphrase')
},
private_key: {
component: UploadKey
}
}
}
}
}
</script>
<style>
</style>

View File

@ -1,340 +0,0 @@
<template>
<div>
<GenericTreeListPage ref="TreeTablePage" :tree-setting="treeSetting">
<template #table>
<AssetUserTable ref="table" v-bind="assetUserConfig" />
</template>
</GenericTreeListPage>
<Dialog width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true">
<el-row :gutter="20">
<el-col :span="4">
<div style="line-height: 34px;text-align: center">MFA</div>
</el-col>
<el-col :span="14">
<el-input v-model="MFAInput" />
<span class="help-tips help-block">{{ $t('common.MFARequireForSecurity') }}</span>
</el-col>
<el-col :span="4">
<el-button size="mini" type="primary" style="line-height:20px " @click="MFAConfirm">{{ this.$t('common.Confirm') }}</el-button>
</el-col>
</el-row>
</Dialog>
<Dialog :title="$t('common.Export')" :visible.sync="showExportDialog" @confirm="handleExportConfirm()" @cancel="handleExportCancel()">
<el-form label-position="left" style="padding-left: 50px">
<el-form-item :label="this.$t('common.imExport.ExportRange')" :label-width="'100px'">
<el-radio v-model="exportOption" class="export-item" label="1">{{ this.$t('common.imExport.ExportAll') }}</el-radio>
<br>
<el-radio v-model="exportOption" :disabled="selectedRows.length===0" class="export-item" label="2">{{ this.$t('common.imExport.ExportOnlySelectedItems') }}</el-radio>
<br>
<!-- 去掉导出搜索项-->
<!-- <el-radio v-model="exportOption" disabled class="export-item" label="3">{{ this.$t('common.imExport.ExportOnlyFiltered') }}</el-radio>-->
</el-form-item>
</el-form>
</Dialog>
<Dialog :title="$t('common.Import')" :visible.sync="showImportDialog" @confirm="handleImportConfirm()" @cancel="handleImportCancel()">
<el-form label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.Import' )" :label-width="'100px'">
<el-radio v-model="importOption" class="export-item" label="1">{{ this.$t('common.Create') }}</el-radio>
<el-radio v-model="importOption" disabled class="export-item" label="2">{{ this.$t('common.Update') }}</el-radio>
<div style="line-height: 1.5">
<span v-if="importOption==='1'" class="el-upload__tip">
{{ this.$t('common.imExport.downloadImportTemplateMsg') }}
<el-link type="success" :underline="false" :href="downloadImportTempUrl">{{ this.$t('common.Download') }}</el-link>
</span>
<span v-else class="el-upload__tip">`
{{ this.$t('common.imExport.downloadUpdateTemplateMsg') }}
<el-link type="success" :underline="false" @click="downloadUpdateTempUrl">{{ this.$t('common.Download') }}</el-link>
</span>
</div>
</el-form-item>
<el-form-item :label="$t('common.Upload' )" :label-width="'100px'">
<el-upload
ref="upload"
action="string"
:http-request="handleImport"
list-type="text/csv"
:limit="1"
:auto-upload="false"
:before-upload="beforeUpload"
>
<el-button size="mini" type="default">{{ this.$t('common.SelectFile') }}</el-button>
<div slot="tip" :class="uploadHelpTextClass" style="line-height: 1.5">{{ this.$t('common.imExport.onlyCSVFilesTips') }}</div>
</el-upload>
</el-form-item>
</el-form>
<div v-if="errorMsg" class="error-msg error-results">
<ul v-if="typeof errorMsg === 'object'">
<li v-for="(item, index) in errorMsg" :key="item + '-' + index"> {{ item }}</li>
</ul>
<span v-else>{{ errorMsg }}</span>
</div>
</Dialog>
</div>
</template>
<script>
import GenericTreeListPage from '@/layout/components/GenericTreeListPage'
import { AssetUserTable } from '@/components'
import Dialog from '@/components/Dialog'
import { setUrlParam } from '@/utils/common'
import { createSourceIdCache } from '@/api/common'
import * as queryUtil from '@/components/DataTable/compenents/el-data-table/utils/query'
import { mapGetters } from 'vuex'
export default {
name: 'VaultList',
components: {
GenericTreeListPage,
AssetUserTable,
Dialog
},
data() {
const vm = this
return {
showImportDialog: false,
importOption: '1',
isCsv: true,
errorMsg: '',
showExportDialog: false,
exportOption: '1',
meta: {},
MfaExpired: 0,
showMFADialog: false,
MFAInput: '',
selectedRows: '',
assetUserConfig: {
hasLeftActions: true,
hasCreate: true,
hasClone: false,
url: '/api/v1/assets/asset-users/',
handleImport: function({ selectedRows }) {
this.selectedRows = selectedRows
this.dialogStatus = 'import'
if (!this.needMFAVerify) {
this.showMFADialog = false
this.showImportDialog = true
} else {
this.showMFADialog = true
}
}.bind(this),
handleExport: function({ selectedRows }) {
this.selectedRows = selectedRows
this.dialogStatus = 'export'
if (!this.needMFAVerify) {
this.showMFADialog = false
this.showExportDialog = true
} else {
this.showMFADialog = true
}
}.bind(this)
},
treeSetting: {
showMenu: false,
showRefresh: false,
showAssets: true,
url: '/api/v1/assets/asset-users/',
treeUrl: '/api/v1/assets/nodes/children/tree/?assets=1',
callback: {
onSelected: function(event, treeNode) {
let url = vm.assetUserConfig.url
if (treeNode.meta.type === 'node') {
const nodeId = treeNode.meta.node.id
url = setUrlParam(url, 'asset_id', '')
url = setUrlParam(url, 'node_id', nodeId)
} else if (treeNode.meta.type === 'asset') {
const assetId = treeNode.meta.asset.id
url = setUrlParam(url, 'node_id', '')
url = setUrlParam(url, 'asset_id', assetId)
}
setTimeout(() => {
vm.assetUserConfig.url = url
}, 100)
}
}
}
}
},
computed: {
...mapGetters([
'MFA_TTl',
'MFAVerifyAt',
'publicSettings'
]),
needMFAVerify() {
if (!this.publicSettings.SECURITY_VIEW_AUTH_NEED_MFA) {
return false
}
const ttl = this.publicSettings.SECURITY_MFA_VERIFY_TTL
const now = new Date()
return !(this.MFAVerifyAt && (now - this.MFAVerifyAt) < ttl * 1000)
},
hasSelected() {
return this.selectedRows.length > 0
},
upLoadUrl() {
return this.url
},
downloadImportTempUrl() {
const baseUrl = `/api/v1/assets/asset-user-auth-infos/`
return baseUrl + '?format=csv&template=import&limit=1'
},
uploadHelpTextClass() {
const cls = ['el-upload__tip']
if (!this.isCsv) {
cls.push('error-msg')
}
return cls
},
...mapGetters([
'MFAVerifyAt',
'MFA_TTl'
])
},
methods: {
performUpdate(item) {
this.$axios.put(
`/api/v1/assets/asset-users/`,
item.file,
{ headers: { 'Content-Type': 'text/csv' }, disableFlashErrorMsg: true }
).then((data) => {
const msg = this.$t('common.imExport.updateSuccessMsg', { count: data.length })
this.onSuccess(msg)
}).catch(error => {
this.catchError(error)
})
},
performCreate(item) {
this.$axios.post(
`/api/v1/assets/asset-users/`,
item.file,
{ headers: { 'Content-Type': 'text/csv' }, disableFlashErrorMsg: true }
).then((data) => {
const msg = this.$t('common.imExport.createSuccessMsg', { count: data.length })
this.onSuccess(msg)
}).catch(error => {
this.$message.error(this.$t('common.updateErrorMsg') + ' ' + error)
this.catchError(error)
})
},
catchError(error) {
this.$refs.upload.clearFiles()
if (error.response && error.response.status === 400) {
const errorData = error.response.data
const totalErrorMsg = []
errorData.forEach((value, index) => {
if (typeof value === 'string') {
totalErrorMsg.push(`line ${index}. ${value}`)
} else {
const errorMsg = [`line ${index}. `]
for (const [k, v] of Object.entries(value)) {
if (v) {
errorMsg.push(`${k}: ${v}`)
}
}
if (errorMsg.length > 1) {
totalErrorMsg.push(errorMsg.join(' '))
}
}
})
this.errorMsg = totalErrorMsg
}
},
onSuccess(msg) {
this.errorMsg = ''
this.$message.success(msg)
},
handleImport(item) {
if (this.importOption === '1') {
this.performCreate(item)
} else {
this.performUpdate(item)
}
},
async downloadUpdateTempUrl() {
var resources = []
const data = this.selectedRows
for (let index = 0; index < data.length; index++) {
resources.push(data[index].id)
}
const spm = await createSourceIdCache(resources)
const baseUrl = `/api/v1/assets/asset-user-auth-infos/`
const url = `${baseUrl}?format=csv&template=update&spm=` + spm.spm
return this.downloadCsv(url)
},
async handleImportConfirm() {
this.$refs.upload.submit()
this.showImportDialog = false
},
handleImportCancel() {
this.showImportDialog = false
},
beforeUpload(file) {
this.isCsv = _.endsWith(file.name, 'csv')
return this.isCsv
},
downloadCsv(url) {
const a = document.createElement('a')
a.href = url
a.click()
window.URL.revokeObjectURL(url)
},
async handleExport() {
const url = `/api/v1/assets/asset-user-auth-infos/`
let query = {}
if (this.exportOption === '2') {
const resources = []
const data = this.selectedRows
for (let index = 0; index < data.length; index++) {
resources.push(data[index].id)
}
const spm = await createSourceIdCache(resources)
query['spm'] = spm.spm
} else if (this.exportOption === '3') {
const listTableRef = this.$parent.$parent.$parent.$parent
// console.log(listTableRef)
// console.log(listTableRef.dataTable)
query = listTableRef.dataTable.getQuery()
delete query['limit']
delete query['offset']
}
query['format'] = 'csv'
const queryStr =
(url.indexOf('?') > -1 ? '&' : '?') +
queryUtil.stringify(query, '=', '&')
return this.downloadCsv(url + queryStr)
},
async handleExportConfirm() {
await this.handleExport()
this.showExportDialog = false
},
handleExportCancel() {
this.showExportDialog = false
},
MFAConfirm() {
if (this.MFAInput.length !== 6) {
return this.$message.error(this.$t('common.MFAErrorMsg'))
}
this.$axios.post(
`/api/v1/authentication/otp/verify/`, {
code: this.MFAInput
}
).then(
res => {
this.$store.dispatch('users/setMFAVerify')
if (this.dialogStatus === 'import') {
this.showMFADialog = false
this.showImportDialog = true
} else {
this.showMFADialog = false
this.showExportDialog = true
}
}
)
}
}
}
</script>
<style scoped>
</style>

View File

@ -10,47 +10,6 @@ export default {
name: 'Xpack',
meta: { title: 'X-Pack', icon: 'sitemap', licenseRequired: true },
children: [
{
path: 'change-auth-plan',
component: empty,
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/xpack/change-auth-plan/plan' },
children: [
{
path: 'plan',
component: () => import('@/views/xpack/ChangeAuthPlan/ChangeAuthPlanList.vue'),
name: 'ChangeAuthPlanList',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/xpack/change-auth-plan/plan' }
},
{
path: 'plan/create',
component: () => import('@/views/xpack/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'ChangeAuthPlanCreate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanCreate'), activeMenu: '/xpack/change-auth-plan/plan', action: 'create' },
hidden: true
},
{
path: 'plan/:id/update',
component: () => import('@/views/xpack/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'ChangeAuthPlanUpdate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanUpdate'), activeMenu: '/xpack/change-auth-plan/plan', action: 'update' },
hidden: true
},
{
path: 'plan/:id',
component: () => import('@/views/xpack/ChangeAuthPlan/ChangeAuthPlanDetail/index.vue'),
name: 'ChangeAuthPlanDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/xpack/change-auth-plan/plan' },
hidden: true
},
{
path: 'plan-execution/:id',
component: () => import('@/views/xpack/ChangeAuthPlan/ChangeAuthPlanDetail/ChangeAuthPlanExecution/ChangeAuthPlanExecutionDetail/index.vue'),
name: 'ChangeAuthPlanExecutionDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ExecutionDetail'), activeMenu: '/xpack/change-auth-plan/plan' },
hidden: true
}
]
},
{
path: 'cloud',
component: empty,
@ -127,55 +86,6 @@ export default {
name: 'InterfaceSetting',
meta: { title: i18n.t('xpack.InterfaceSettings'), permissions: [rolec.PERM_SUPER] }
},
{
path: 'gathered-user',
component: empty,
redirect: '',
meta: { title: i18n.t('xpack.GatherUser.GatherUserList') },
children: [
{
path: '',
component: () => import('@/views/xpack/GatheredUser/index'),
name: 'GatherUserListIndex',
meta: { title: i18n.t('xpack.GatherUser.GatherUser'), activeMenu: '/xpack/gathered-user' }
},
{
path: '',
component: () => import('@/views/xpack/GatheredUser/GatheredUserList'),
name: 'GatherUserList',
hidden: true,
meta: { title: i18n.t('xpack.GatherUser.GatherUserList'), activeMenu: '/xpack/gathered-user' }
},
{
path: 'tasks',
component: () => import('@/views/xpack/GatheredUser/TaskList'),
name: 'GatherUserTaskList',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskList'), activeMenu: '/xpack/gathered-user' },
hidden: true
},
{
path: 'tasks/:id',
component: () => import('@/views/xpack/GatheredUser/TaskDetail/index'),
name: 'GatherUserTaskDetail',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskDetail'), activeMenu: '/xpack/gathered-user' },
hidden: true
},
{
path: 'tasks/create',
component: () => import('@/views/xpack/GatheredUser/TaskCreateUpdate'),
name: 'GatherUserTaskCreate',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskCreate'), activeMenu: '/xpack/gathered-user' },
hidden: true
},
{
path: 'tasks/:id/update',
component: () => import('@/views/xpack/GatheredUser/TaskCreateUpdate'),
name: 'GatherUserTaskUpdate',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskUpdate'), action: 'update', activeMenu: '/xpack/gathered-user' },
hidden: true
}
]
},
{
path: 'orgs',
component: empty,
@ -211,27 +121,6 @@ export default {
}
]
},
{
path: 'vault',
component: empty,
redirect: '',
meta: { },
children: [
{
path: '',
component: () => import('@/views/xpack/Vault/VaultList.vue'),
name: 'VaultList',
meta: { title: i18n.t('xpack.Vault.Vault'), activeMenu: '/xpack/vault' }
},
{
path: 'create',
component: () => import('@/views/xpack/Vault/VaultCreate'),
name: 'VaultCreate',
meta: { title: i18n.t('xpack.Vault.Create'), activeMenu: '/xpack/vault' },
hidden: true
}
]
},
{
path: 'system-monitor',
component: () => import('@/views/xpack/SystemMonitor/index.vue'),

View File

@ -4,12 +4,11 @@ const defaultSettings = require('./src/settings.js')
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const productionGzipExtensions = /\.(js|css|json|txt|ico|svg)(\?.*)?$/i
function resolve(dir) {
return path.join(__dirname, dir)
}
const name = defaultSettings.title || 'JumpServer' // page title
const name = '' // page title
// If your port is set to 80,
// use administrator privileges to execute the command line.

View File

@ -2357,13 +2357,15 @@ browserify-zlib@^0.2.0:
pako "~1.0.5"
browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.5.4, browserslist@^4.8.3:
version "4.10.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.10.0.tgz#f179737913eaf0d2b98e4926ac1ca6a15cbcc6a9"
version "4.16.6"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2"
integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==
dependencies:
caniuse-lite "^1.0.30001035"
electron-to-chromium "^1.3.378"
node-releases "^1.1.52"
pkg-up "^3.1.0"
caniuse-lite "^1.0.30001219"
colorette "^1.2.2"
electron-to-chromium "^1.3.723"
escalade "^3.1.1"
node-releases "^1.1.71"
bser@2.1.1:
version "2.1.1"
@ -2556,9 +2558,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001035:
version "1.0.30001035"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001035.tgz#2bb53b8aa4716b2ed08e088d4dc816a5fe089a1e"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001219:
version "1.0.30001230"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz#8135c57459854b2240b57a4a6786044bdc5a9f71"
integrity sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==
capture-exit@^1.2.0:
version "1.2.0"
@ -2874,6 +2877,11 @@ color@^3.0.0:
color-convert "^1.9.1"
color-string "^1.5.2"
colorette@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
colors@^1.1.2:
version "1.4.0"
resolved "https://registry.npm.taobao.org/colors/download/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
@ -3605,8 +3613,9 @@ dns-equal@^1.0.0:
resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
dns-packet@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a"
version "1.3.4"
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f"
integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==
dependencies:
ip "^1.1.0"
safe-buffer "^5.0.1"
@ -3758,9 +3767,10 @@ ejs@^2.6.1:
version "2.7.4"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
electron-to-chromium@^1.3.378:
version "1.3.379"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.379.tgz#81dc5e82a3e72bbb830d93e15bc35eda2bbc910e"
electron-to-chromium@^1.3.723:
version "1.3.739"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.739.tgz#f07756aa92cabd5a6eec6f491525a64fe62f98b9"
integrity sha512-+LPJVRsN7hGZ9EIUUiWCpO7l4E3qBYHNadazlucBfsXBbccDFNKUBAgzE68FnkWGJPwD/AfKhSzL+G+Iqb8A4A==
elegant-spinner@^1.0.1:
version "1.0.1"
@ -3903,6 +3913,11 @@ es6-object-assign@1.1.0:
resolved "https://registry.npm.taobao.org/es6-object-assign/download/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@ -7324,6 +7339,11 @@ moment@^2.19.2:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
moment@^2.29.1:
version "2.29.1"
resolved "https://registry.npm.taobao.org/moment/download/moment-2.29.1.tgz?cache=0&sync_timestamp=1601983320283&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmoment%2Fdownload%2Fmoment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha1-sr52n6MZQL6e7qZGnAdeNQBvo9M=
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@ -7495,11 +7515,10 @@ node-notifier@^5.2.1:
shellwords "^0.1.1"
which "^1.3.0"
node-releases@^1.1.52:
version "1.1.52"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.52.tgz#bcffee3e0a758e92e44ecfaecd0a47554b0bcba9"
dependencies:
semver "^6.3.0"
node-releases@^1.1.71:
version "1.1.72"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe"
integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==
nopt@^5.0.0:
version "5.0.0"
@ -8307,12 +8326,6 @@ pkg-up@^2.0.0:
dependencies:
find-up "^2.1.0"
pkg-up@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5"
dependencies:
find-up "^3.0.0"
please-upgrade-node@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
@ -9372,8 +9385,9 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-regex@^1.1.0:
version "1.1.0"