Compare commits

..

41 Commits
v2.25 ... v2.26

Author SHA1 Message Date
“huailei000”
02d1ab25a0 fix: jQuery undefined 2022-10-13 17:41:29 +08:00
“huailei000”
5408057c05 perf: update jquery 2022-10-13 11:20:02 +08:00
feng626
bb640f63a3 Merge pull request #2096 from jumpserver/pr@v2.26@task_log
fix: 修复任务列表无法查看日志信息bug
2022-10-13 10:55:09 +08:00
feng626
7f0df48bb0 fix: 修复任务列表无法查看日志信息bug 2022-10-13 10:52:48 +08:00
“huailei000”
0225ea95fb pref: 作业中心任务详情添加查看输出快捷键 2022-10-12 19:13:28 +08:00
“huailei000”
c599a6aa21 fix: 修复全局组织下更新、删除:用户、端点、端点规则、组织权限 2022-09-26 14:51:57 +08:00
“huailei000”
7f34195ae4 fix: 实例同步列表不显示多选框;设置云同步详情页面的菜单高亮显示;云同步进入详情不主动激活detail页卡 2022-09-26 14:50:47 +08:00
“huailei000”
b23950aefd fix: 修复账号信息内容更新不及时问题 2022-09-26 14:49:34 +08:00
Jiangjie.Bai
49880f6739 Merge pull request #2059 from jumpserver/dev
v2.26.0
2022-09-15 17:49:44 +08:00
“huailei000”
82faf0f99e perf: 资产详情-授权用户没有数据时显示暂无数据 2022-09-15 17:48:41 +08:00
Jiangjie.Bai
e6f98d58c4 Merge pull request #2057 from jumpserver/dev
v2.26.0-rc4
2022-09-15 16:18:03 +08:00
“huailei000”
3536a94976 fix: 修复全局组织下命令过滤不能创建 2022-09-15 15:59:53 +08:00
“huailei000”
45276010e0 fix: 创建数据库端口设置为必填项 2022-09-15 15:34:00 +08:00
“huailei000”
2e47f42366 fix: 修复批量命令列表可点击链接颜色 2022-09-15 15:33:41 +08:00
“huailei000”
dd6e9a1512 fix: 修复批量命令列表可点击链接颜色 2022-09-15 15:33:41 +08:00
Jiangjie.Bai
fd1f16d43c Merge pull request #2050 from jumpserver/dev
v2.26.0-rc2
2022-09-13 17:41:39 +08:00
feng626
24931a9f5a perf: 工单新增相关过滤 2022-09-13 15:33:22 +08:00
ibuler
f51924bf1d perf: 优化密码加密,如果没有key就不加密了 2022-09-13 15:32:53 +08:00
“huailei000”
f82257edb8 fix: 修复创建、更新用户后点击邀请用户接口404问题 2022-09-13 15:31:53 +08:00
jiangweidong
286e9894c0 perf: 屏蔽--secure参数,目前redis-cli版本为6.0,暂时用不到 2022-09-13 11:50:06 +08:00
Jiangjie.Bai
968b2415b1 Merge pull request #2043 from jumpserver/dev
v2.26.0-rc1
2022-09-08 15:46:44 +08:00
“huailei000”
e0f6fb305d perf: cas用户属性映射不能为空,至少设置username 2022-09-08 15:46:08 +08:00
jiangweidong
e0fd33f376 fix: 修复数据库创建页面有多个mongdob的问题 (#2041) 2022-09-08 15:16:54 +08:00
jiangweidong
7ad86062b4 perf: 支持连接开启ssl且自签证书的Redis/MongoDB (#2039)
* perf: 支持连接开启ssl且自签证书的Redis/MongoDB

* 修改字段文案

* 修改字段名称

* 修改变量名
2022-09-07 16:09:07 +08:00
“huailei000”
1fce7561db fix: 修复用户首次登录页面翻译 2022-09-06 19:38:49 +08:00
“huailei000”
3b17235b6e fix: 批量更新不请求接口 2022-09-06 19:06:14 +08:00
jiangweidong
52c9b9503b feat: 支持MFA可配置华为云平台短信对接 2022-09-06 17:33:42 +08:00
Jiangjie.Bai
48a2b20320 Merge pull request #2025 from jumpserver/pr@dev@feat_cloud_support_ctyun_private
feat: 云同步支持同步天翼私有云平台资产
2022-09-06 17:31:01 +08:00
Jiangjie.Bai
e6cc8cd2e8 Merge branch 'dev' into pr@dev@feat_cloud_support_ctyun_private 2022-09-06 17:30:47 +08:00
jiangweidong
3a183ddf53 feat: 支持OAuth2协议自定义注销功能 2022-09-06 17:27:11 +08:00
jiangweidong
7eb77487d1 feat: feat: 支持连接开启了ssl的Redis数据库 2022-09-06 16:55:37 +08:00
jiangweidong
a5da581317 feat: 云同步支持资产同步腾讯云(轻量应用服务器) 2022-09-06 16:19:58 +08:00
“huailei000”
6bda9a372e fix: moment.js 插件升级修复官方漏洞 2022-09-05 13:55:52 +08:00
jiangweidong
a8de087137 feat: 云同步支持同步天翼私有云平台资产 2022-09-02 17:37:27 +08:00
jiangweidong
34d9790a04 feat: MongoDB支持连接SSL类型 2022-08-24 15:00:56 +08:00
“huailei000”
5bfe2497fd fix: 修复新建用户个人信息确认后跳转路由还会返回个人信息确认页问题 2022-08-24 14:48:08 +08:00
jiangweidong
e16a775037 feat: 改密计划支持MongoDB改密 2022-08-24 14:47:46 +08:00
“huailei000”
42fab92237 fix: 调整按钮大小 2022-08-19 16:24:29 +08:00
Jiangjie.Bai
e2eac83615 fix: 修复上传json文件上传问题 2022-08-19 13:36:22 +08:00
“huailei000”
04465a1da3 fix: 修复LDAP用户导入失败弹出提示 2022-08-19 11:05:31 +08:00
“huailei000”
2adb1ee980 fix: 升级lodash 2022-08-19 11:04:43 +08:00
43 changed files with 503 additions and 157 deletions

View File

@@ -32,7 +32,7 @@
"element-ui": "2.13.2",
"eslint-plugin-html": "^6.0.0",
"install": "^0.13.0",
"jquery": "^3.5.0",
"jquery": "^3.6.1",
"js-cookie": "2.2.0",
"jsencrypt": "^3.2.1",
"krry-transfer": "^1.7.3",

View File

@@ -247,6 +247,15 @@ td .el-button.el-button--mini {
border-top-color: #676a6c;
}
.text-link {
color: info!important;
}
.text-link:hover {
color: info!important;
filter: opacity(65%)!important;
}
.text-danger {
color: danger;
}

View File

@@ -45,3 +45,20 @@ export const JsonRequired = {
}
}
}
export const JsonRequiredUserNameMapped = {
required: true,
trigger: 'change',
validator: (rule, value, callback) => {
try {
JSON.parse(value)
const hasUserName = _.map(JSON.parse(value), (value) => value)
if (!hasUserName.includes('username')) {
callback(new Error(i18n.t('common.requiredHasUserNameMapped')))
}
callback()
} catch (e) {
callback(new Error(i18n.t('common.InvalidJson')))
}
}
}

View File

@@ -10,7 +10,6 @@
"name": "Name",
"username": "Username",
"ip_group": "IP group",
"ip_group_help_text": "IP segments should be separated by comma, with * indicating a match all. Such as: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64",
"action": "Action",
"priority": "Priority",
"date_created": "Date created",
@@ -268,6 +267,12 @@
"IPLoginLimit": "IP login limit",
"Setting": "Setting",
"Certificate": "Certificate",
"CACertificate": "CA Certificate",
"ClientCertificate": "Client certificate",
"CertificateKey": "Certificate key file",
"AllowInvalidCert": "Allow invalid cert",
"UseSSL": "Use SSL/TLS",
"SecretKey": "Secret key",
"Scope": "Type",
"Builtin": "Builtin",
"DateCreated": "Date created",
@@ -409,6 +414,7 @@
"disableSelected": "Disable selected",
"disableSuccessMsg": "Disable success",
"fieldRequiredError": "This field is required",
"requiredHasUserNameMapped": "The mapping of the username field must be included, such as {'uid': 'username'}",
"getErrorMsg": "Get failed",
"fileType": "File type",
"Status": "Status",
@@ -773,7 +779,7 @@
"OperateLog": "Operation Logs",
"PasswordChangeLog": "Password Update Logs",
"Perms": "Permissions",
"PersonalInformationImprovement": "PersonalInformationImprovement",
"PersonalInformationImprovement": "Personal information improvement",
"PlatformCreate": "Platform create",
"PlatformDetail": "Platform detail",
"PlatformList": "Platforms",
@@ -927,6 +933,9 @@
"SMS": "SMS",
"AlibabaCloud": "Alibaba cloud",
"TencentCloud": "Tencent cloud",
"HuaweiCloud": "Huawei cloud",
"SignChannelNum": "Signature Channel Number",
"AppEndpoint": "App access address",
"CMPP2": "CMPP v2.0",
"VerifySignTmpl": "Verification code template",
"Radius": "Radius",
@@ -1141,6 +1150,10 @@
"reply": "Reply",
"status": "Status",
"title": "Title",
"RelevantApp": "App",
"RelevantAsset": "Asset",
"RelevantCommand": "Command",
"RelevantSystemUser": "System user",
"type": "Type",
"user": "User",
"Status": "Status",
@@ -1305,10 +1318,6 @@
"Receivers": "Receivers",
"Subscription": "Subscription",
"ChangeReceiver": "Change Receivers",
"Constituency": "Available Fields",
"Selected": "Selected Fields",
"PrePage": "Previous",
"NextPage": "Next",
"Subject": "Subject",
"Message": "Message",
"DeliveryTime": "Delivery time",
@@ -1413,9 +1422,11 @@
"IPNetworkSegment": "Ip Network Segment",
"Aliyun": "Ali Cloud",
"Qcloud": "Tencent Cloud",
"QcloudLighthouse": "Tencent Cloud(Lighthouse)",
"QingyunPrivatecloud": "Qingyun Private Cloud",
"HuaweiPrivatecloud": "Huawei Private Cloud",
"OpenStack": "OpenStack",
"CTYunPrivate": "CTYun Private Cloud",
"GCP": "Google Cloud Platform",
"FC": "Fusion Compute",
"LAN": "LAN",

View File

@@ -272,6 +272,12 @@
"IPLoginLimit": "IPログイン制限",
"Setting": "設定",
"Certificate": "証明書",
"CACertificate": "CA 証明書",
"ClientCertificate": "クライアント証明書",
"CertificateKey": "証明書秘密鍵ファイル",
"AllowInvalidCert": "証明書チェックを無視する",
"UseSSL": "使う SSL/TLS",
"SecretKey": "鍵",
"Scope": "カテゴリ",
"Builtin": "内蔵",
"DateCreated": "作成日",
@@ -417,6 +423,7 @@
"disableSelected": "選択した無効",
"disableSuccessMsg": "成功を無効にする",
"fieldRequiredError": "このフィールドは必須項目です",
"requiredHasUserNameMapped": "usernameフィールドのマッピングを含める必要があります, {'uid':'username'}など",
"getErrorMsg": "の取得に失敗しました",
"MFAErrorMsg": "MFAエラーです。チェックしてください",
"Total": "合計",
@@ -945,6 +952,9 @@
"Feature": "機能",
"AlibabaCloud": "Alibaba cloud",
"TencentCloud": "テンセント雲",
"HuaweiCloud": "ファーウェイ雲",
"SignChannelNum": "サインパス番号",
"AppEndpoint": "アクセスアドレスを適用する",
"CMPP2": "CMPP v2.0",
"Radius": "Radius",
"VerifySignTmpl": "認証コードメールテンプレート",
@@ -1172,6 +1182,10 @@
"reply": "返信",
"status": "ステータス",
"title": "タイトル",
"RelevantApp": "するアプリケーション",
"RelevantAsset": "する資産",
"RelevantCommand": "するコマンド",
"RelevantSystemUser": "するシステムユーザー",
"action": "アクション",
"type": "タイプ",
"user": "ユーザー",
@@ -1344,10 +1358,6 @@
"Receivers": "受取人",
"Subscription": "メッセージ購読",
"ChangeReceiver": "メッセージ受信者の修正",
"Constituency": "未選択",
"Selected": "選択済み",
"PrePage": "前のページ",
"NextPage": "次のページ",
"Subject": "テーマ",
"Message": "メッセージ",
"DeliveryTime": "送信時間",
@@ -1455,9 +1465,11 @@
"IPNetworkSegment": "IPネットワークセグメント",
"Aliyun": "Alibaba cloud",
"Qcloud": "テンセント雲",
"QcloudLighthouse": "テンセント雲(軽量アプリケーションサーバー)",
"QingyunPrivatecloud": "青雲プライベートクラウド",
"HuaweiPrivatecloud": "ファーウェイプライベートクラウド",
"OpenStack": "OpenStack",
"CTYunPrivate": "天翼プライベート・クラウド",
"GCP": "Googleクラウド",
"FC": "Fusion Compute",
"LAN": "ローカルエリアネットワーク",

View File

@@ -272,6 +272,12 @@
"IPLoginLimit": "IP 登录限制",
"Setting": "设置",
"Certificate": "证书",
"CACertificate": "CA 证书",
"ClientCertificate": "客户端证书",
"CertificateKey": "证书秘钥文件",
"AllowInvalidCert": "忽略证书检查",
"UseSSL": "使用 SSL/TLS",
"SecretKey": "密钥",
"Scope": "类别",
"Builtin": "内置",
"DateCreated": "创建日期",
@@ -417,6 +423,7 @@
"disableSelected": "禁用所选",
"disableSuccessMsg": "禁用成功",
"fieldRequiredError": "这个字段是必填项",
"requiredHasUserNameMapped": "必须包含 username 字段的映射,如 { 'uid': 'username' }",
"getErrorMsg": "获取失败",
"MFAErrorMsg": "MFA错误请检查",
"Total": "总共",
@@ -946,6 +953,9 @@
"Feature": "功能",
"AlibabaCloud": "阿里云",
"TencentCloud": "腾讯云",
"HuaweiCloud": "华为云",
"SignChannelNum": "签名通道号",
"AppEndpoint": "应用接入地址",
"CMPP2": "CMPP v2.0",
"Radius": "Radius",
"VerifySignTmpl": "验证码短信模板",
@@ -1173,6 +1183,10 @@
"reply": "回复",
"status": "状态",
"title": "标题",
"RelevantApp": "应用",
"RelevantAsset": "资产",
"RelevantCommand": "命令",
"RelevantSystemUser": "系统用户",
"action": "动作",
"type": "类型",
"user": "用户",
@@ -1345,10 +1359,6 @@
"Receivers": "接收人",
"Subscription": "消息订阅",
"ChangeReceiver": "修改消息接收人",
"Constituency": "待选区",
"Selected": "已选中",
"PrePage": "上一页",
"NextPage": "下一页",
"Subject": "主题",
"Message": "消息",
"DeliveryTime": "发送时间",
@@ -1456,8 +1466,10 @@
"IPNetworkSegment": "IP网段",
"Aliyun": "阿里云",
"Qcloud": "腾讯云",
"QcloudLighthouse": "腾讯云(轻量应用服务器)",
"QingyunPrivatecloud": "青云私有云",
"HuaweiPrivatecloud": "华为私有云",
"CTYunPrivate": "天翼私有云",
"OpenStack": "OpenStack",
"GCP": "谷歌云",
"FC": "Fusion Compute",
@@ -1514,8 +1526,8 @@
"DeleteReleasedAssets": "删除已释放资产"
},
"Template": {
"Template": "模版管理"
},
"Template": "模版管理"
},
"Corporation": "公司",
"Edition": "版本",
"Execute": "执行",

View File

@@ -15,6 +15,16 @@ export default [
permissions: []
}
},
{
path: '/ops/ansible/task/:id/log/',
component: () => import('@/views/ops/CeleryTaskLog'),
name: 'AnsibleTaskLog',
hidden: true,
meta: {
title: i18n.t('route.CeleryTaskLog'),
permissions: []
}
},
{
path: '/ops/task/task/:id/log/',
component: () => import('@/views/ops/CeleryTaskLog'),

View File

@@ -1,6 +1,8 @@
import empty from '@/layout/empty'
import i18n from '@/i18n/i18n'
const activateMenu = '/console/assets/assets'
export default [
{
path: 'cloud',
@@ -20,7 +22,7 @@ export default [
hidden: true,
meta: {
title: i18n.t('xpack.Cloud.CloudSync'),
activeMenu: '/console/assets/assets'
activeMenu: activateMenu
}
},
{
@@ -71,6 +73,7 @@ export default [
hidden: true,
meta: {
title: i18n.t('xpack.Cloud.AccountDetail'),
activeMenu: activateMenu,
permissions: ['xpack.view_account']
}
}
@@ -121,7 +124,8 @@ export default [
name: 'SyncInstanceTaskDetail',
hidden: true,
meta: {
title: i18n.t('xpack.Cloud.SyncInstanceTaskDetail')
title: i18n.t('xpack.Cloud.SyncInstanceTaskDetail'),
activeMenu: activateMenu
}
}
]

View File

@@ -72,6 +72,9 @@ const mutations = {
},
ADD_WORKBENCH_ORGS(state, org) {
state.workbenchOrgs.push(org)
},
SET_IS_FIRST_LOGIN(state, flag) {
state.profile.is_first_login = flag
}
}
@@ -140,6 +143,9 @@ const actions = {
const usingOrgs = mapper[viewName] || state.consoleOrgs
Vue.$log.debug('Set using orgs: ', viewName, usingOrgs)
commit('SET_USING_ORGS', usingOrgs)
},
ifFirstLogin({ commit }, flag) {
commit('SET_IS_FIRST_LOGIN', flag)
}
}

View File

@@ -302,3 +302,8 @@ export function groupedDropdownToCascader(group) {
export { BASE_URL }
export function openWindow(url, name = '', iWidth = 900, iHeight = 600) {
var iTop = (window.screen.height - 30 - iHeight) / 2
var iLeft = (window.screen.width - 10 - iWidth) / 2
window.open(url, name, 'height=' + iHeight + ',width=' + iWidth + ',top=' + iTop + ',left=' + iLeft)
}

View File

@@ -37,10 +37,13 @@ export function encryptPassword(password) {
if (!password) {
return ''
}
let rsaPublicKeyText = getCookie('jms_public_key')
if (!rsaPublicKeyText) {
return password
}
const aesKey = (Math.random() + 1).toString(36).substring(2)
// public key 是 base64 存储的
const rsaPublicKeyText = getCookie('jms_public_key')
.replaceAll('"', '')
rsaPublicKeyText = rsaPublicKeyText.replaceAll('"', '')
const rsaPublicKey = atob(rsaPublicKeyText)
const keyCipher = rsaEncrypt(aesKey, rsaPublicKey)
const passwordCipher = aesEncrypt(password, aesKey)

View File

@@ -1,8 +1,10 @@
import store from '@/store'
import { constantRoutes } from '@/router'
import { openWindow } from './common'
export function openTaskPage(taskId) {
window.open(`/#/ops/celery/task/${taskId}/log/`, '', 'width=900,height=600')
export function openTaskPage(taskId, taskType) {
taskType = taskType || 'celery'
openWindow(`/#/ops/${taskType}/task/${taskId}/log/?type=${taskType}`)
}
export function checkPermission(permsRequired, permsAll) {

View File

@@ -1,4 +1,4 @@
import $ from 'jquery'
import $ from 'jquery/dist/jquery.min.js'
window.$ = $
window.jQuery = $
export default $

View File

@@ -5,6 +5,9 @@
<script>
import { GenericCreateUpdatePage } from '@/layout/components'
import { getDatabaseTypeFieldsMap } from '@/views/applications/DatabaseApp/const'
import { UploadKey } from '@/components'
import { Required } from '@/components/DataForm/rules'
export default {
components: {
GenericCreateUpdatePage
@@ -36,6 +39,32 @@ export default {
fieldsMeta: {
host: {
type: 'input'
},
port: {
rules: [Required]
},
use_ssl: {
label: this.$t('common.UseSSL'),
component: 'el-switch'
},
allow_invalid_cert: {
label: this.$t('common.AllowInvalidCert'),
hidden: (form) => { return !form.use_ssl }
},
ca_cert: {
label: this.$t('common.CACertificate'),
hidden: (form) => { return !form.use_ssl },
component: UploadKey
},
client_cert: {
label: this.$t('common.ClientCertificate'),
hidden: (form) => { return !form.use_ssl },
component: UploadKey
},
cert_key: {
label: this.$t('common.CertificateKey'),
hidden: (form) => { return !form.use_ssl },
component: UploadKey
}
}
}

View File

@@ -1,10 +1,16 @@
import { ORACLE } from '../const'
import { ORACLE, MONGODB, REDIS } from '../const'
export function getDatabaseTypeFieldsMap(type) {
const baseParams = ['host', 'port', 'database']
const tlsParams = ['use_ssl', 'ca_cert']
switch (type) {
case ORACLE:
return ['host', 'port', 'database', 'version']
return baseParams.concat(['version'])
case REDIS:
return baseParams.concat(tlsParams.concat(['client_cert', 'cert_key']))
case MONGODB:
return baseParams.concat(tlsParams.concat(['cert_key', 'allow_invalid_cert']))
default:
return ['host', 'port', 'database']
return baseParams
}
}

View File

@@ -92,24 +92,27 @@ export const DATABASE = [
}
]
const MONGODB_ITEM = {
name: MONGODB,
title: i18n.t(`applications.applicationsType.${MONGODB}`),
type: 'primary',
category: DATABASE_CATEGORY,
group: i18n.t('applications.NoSQLProtocol')
}
const REDIS_ITEM = {
name: REDIS,
title: i18n.t(`applications.applicationsType.${REDIS}`),
type: 'primary',
category: DATABASE_CATEGORY,
has: true
}
export const KV_DATABASE = [
{
name: REDIS,
title: i18n.t(`applications.applicationsType.${REDIS}`),
type: 'primary',
category: DATABASE_CATEGORY,
has: true,
group: i18n.t('applications.NoSQLProtocol')
},
{
name: MONGODB,
title: i18n.t(`applications.applicationsType.${MONGODB}`),
type: 'primary',
category: DATABASE_CATEGORY
}
MONGODB_ITEM, REDIS_ITEM
]
export const AppPlanDatabase = DATABASE
export const AppPlanDatabase = [...DATABASE, MONGODB_ITEM]
export const KUBERNETES = 'k8s'
export const CLOUD_CATEGORY = 'cloud'

View File

@@ -1,18 +1,40 @@
<template>
<IBox :fa="icon" :type="type" :title="title" v-bind="$attrs">
<table style="width: 100%;table-layout:fixed;" class="CardTable">
<tr v-for="obj of iObjects" :key="obj.value" class="item">
<td style="overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">
<el-tooltip style="margin: 4px;" effect="dark" :content="obj.label" placement="left">
<el-link class="detail" @click="goDetail(obj)">{{ obj.label }}</el-link>
</el-tooltip>
</td>
<td>
<el-button size="mini" type="primary" style="float: right" @click="buttonClickCallback(obj)">
{{ buttonTitle }}
</el-button>
</td>
</tr>
<IBox
:fa="icon"
:type="type"
:title="title"
v-bind="$attrs"
>
<table class="card-table">
<div v-if="iObjects.length > 0" v-cloak>
<tr v-for="obj of iObjects" :key="obj.value" class="item">
<td>
<el-tooltip
style="margin: 4px;"
effect="dark"
:content="obj.label"
placement="left"
>
<el-link class="detail" @click="goDetail(obj)">
{{ obj.label }}
</el-link>
</el-tooltip>
</td>
<td>
<el-button
size="mini"
type="primary"
style="float: right"
@click="buttonClickCallback(obj)"
>
{{ buttonTitle }}
</el-button>
</td>
</tr>
</div>
<div v-else v-cloak style="text-align: center;">
{{ $t('common.NoData') }}
</div>
</table>
</IBox>
</template>
@@ -71,9 +93,9 @@ export default {
methods: {
async loadObjects() {
const data = await this.$axios.get(this.url)
data.forEach((v) => {
for (const v of data) {
v['label'] = v['name']
})
}
this.objects = data
},
goDetail(obj) {
@@ -84,18 +106,26 @@ export default {
</script>
<style lang="scss" scoped>
.card-table {
width: 100%;
table-layout:fixed;
}
[v-cloak]{
display: none!important;
}
b, strong {
font-weight: 700;
font-size: 13px;
}
tr td {
line-height: 1.42857;
padding: 8px;
vertical-align: top;
display: inline;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
tr.item {
border-bottom: 1px solid #e7eaec;
padding: 8px;

View File

@@ -22,7 +22,7 @@ export default {
const accountProviderAttrs = ACCOUNT_PROVIDER_ATTRS_MAP[accountProvider]
function setFieldAttrs() {
const fieldsObject = {}
const updateNotRequiredFields = ['access_key_secret', 'client_secret', 'password', 'sc_password', 'oc_password']
const updateNotRequiredFields = ['access_key_secret', 'client_secret', 'password', 'sc_password', 'oc_password', 'cert_file', 'key_file']
for (const item of accountProviderAttrs?.attrs) {
fieldsObject[item] = {
rules: updateNotRequiredFields.includes(item) && vm.$route.params.id ? [] : [Required]
@@ -61,6 +61,20 @@ export default {
toFormat: 'object'
}
},
cert_file: {
label: this.$t('common.Certificate'),
component: UploadKey,
el: {
toFormat: 'object'
}
},
key_file: {
label: this.$t('common.SecretKey'),
component: UploadKey,
el: {
toFormat: 'object'
}
},
password: {
rules: this.$route.params.id ? [] : [Required]
}

View File

@@ -4,7 +4,7 @@
<script type="text/jsx">
import GenericListTable from '@/layout/components/GenericListTable'
import { ACCOUNT_PROVIDER_ATTRS_MAP, aliyun, aws_china, aws_international, huaweicloud, qcloud, azure, azure_international, vmware, nutanix, qingcloud_private, huaweicloud_private, openstack, gcp, baiducloud, jdcloud, fc, lan } from '../const'
import { ACCOUNT_PROVIDER_ATTRS_MAP, aliyun, aws_china, aws_international, huaweicloud, qcloud, qcloud_lighthouse, azure, azure_international, vmware, nutanix, qingcloud_private, huaweicloud_private, ctyun_private, openstack, gcp, baiducloud, jdcloud, fc, lan } from '../const'
export default {
name: 'AccountList',
@@ -82,6 +82,10 @@ export default {
type: 'primary',
can: true
},
{
name: qcloud_lighthouse,
title: ACCOUNT_PROVIDER_ATTRS_MAP[qcloud_lighthouse].title
},
{
name: huaweicloud,
title: ACCOUNT_PROVIDER_ATTRS_MAP[huaweicloud].title
@@ -127,6 +131,10 @@ export default {
name: huaweicloud_private,
title: ACCOUNT_PROVIDER_ATTRS_MAP[huaweicloud_private].title
},
{
name: ctyun_private,
title: ACCOUNT_PROVIDER_ATTRS_MAP[ctyun_private].title
},
{
name: openstack,
title: ACCOUNT_PROVIDER_ATTRS_MAP[openstack].title

View File

@@ -92,6 +92,7 @@ export default {
component: Select2,
el: {
multiple: true,
allowCreate: true,
value: [],
ajax: {
url: '/api/v1/xpack/cloud/regions/',

View File

@@ -38,6 +38,7 @@ export default {
},
tableConfig: {
url: `/api/v1/xpack/cloud/sync-instance-tasks/${this.object.id}/instances/`,
hasSelection: false,
columns: [
'instance_id',
{

View File

@@ -58,10 +58,7 @@ export default {
formatter: DetailFormatter,
formatterArgs: {
permissions: 'xpack.view_syncinstancedetail',
route: 'SyncInstanceTaskDetail',
routeQuery: {
activeTab: 'detail'
}
route: 'SyncInstanceTaskDetail'
}
},
history_count: {

View File

@@ -1,21 +1,24 @@
import i18n from '@/i18n/i18n'
export const gcp = 'gcp'
export const aliyun = 'aliyun'
export const baiducloud = 'baiducloud'
export const jdcloud = 'jdcloud'
export const aws_international = 'aws_international'
export const aws_china = 'aws_china'
export const huaweicloud = 'huaweicloud'
export const qcloud = 'qcloud'
export const qcloud_lighthouse = 'qcloud_lighthouse'
export const azure = 'azure'
export const azure_international = 'azure_international'
export const vmware = 'vmware'
export const nutanix = 'nutanix'
export const qingcloud_private = 'qingcloud_private'
export const huaweicloud_private = 'huaweicloud_private'
export const ctyun_private = 'ctyun_private'
export const openstack = 'openstack'
export const gcp = 'gcp'
export const nutanix = 'nutanix'
export const vmware = 'vmware'
export const fc = 'fc'
export const baiducloud = 'baiducloud'
export const jdcloud = 'jdcloud'
export const lan = 'lan'
export const ACCOUNT_PROVIDER_ATTRS_MAP = {
@@ -54,6 +57,11 @@ export const ACCOUNT_PROVIDER_ATTRS_MAP = {
title: i18n.t('xpack.Cloud.Qcloud'),
attrs: ['access_key_id', 'access_key_secret']
},
[qcloud_lighthouse]: {
name: qcloud_lighthouse,
title: i18n.t('xpack.Cloud.QcloudLighthouse'),
attrs: ['access_key_id', 'access_key_secret']
},
[azure]: {
name: azure,
title: i18n.t('xpack.Cloud.Azure'),
@@ -64,6 +72,11 @@ export const ACCOUNT_PROVIDER_ATTRS_MAP = {
title: i18n.t('xpack.Cloud.Azure_Int'),
attrs: ['client_id', 'client_secret', 'tenant_id', 'subscription_id']
},
[gcp]: {
name: gcp,
title: i18n.t('xpack.Cloud.GCP'),
attrs: ['service_account_key']
},
[vmware]: {
name: vmware,
title: 'VMware',
@@ -89,16 +102,16 @@ export const ACCOUNT_PROVIDER_ATTRS_MAP = {
title: i18n.t('xpack.Cloud.OpenStack'),
attrs: ['auth_url', 'user_domain_name', 'username', 'password']
},
[gcp]: {
name: gcp,
title: i18n.t('xpack.Cloud.GCP'),
attrs: ['service_account_key']
},
[fc]: {
name: fc,
title: i18n.t('xpack.Cloud.FC'),
attrs: ['api_endpoint', 'username', 'password']
},
[ctyun_private]: {
name: ctyun_private,
title: i18n.t('xpack.Cloud.CTYunPrivate'),
attrs: ['access_key_id', 'access_key_secret', 'api_endpoint', 'cert_file', 'key_file']
},
[lan]: {
name: lan,
title: i18n.t('xpack.Cloud.LAN'),

View File

@@ -81,10 +81,7 @@ export default {
hasRefresh: true,
hasSearch: true,
hasMoreActions: false,
createRoute: 'CommandFilterCreate',
canCreate: () => {
return this.$hasPerm('assets.add_commandfilter')
}
createRoute: 'CommandFilterCreate'
}
}
},

View File

@@ -42,7 +42,7 @@ export default {
vm.relationDialog.tableConfig.url = setUrlParam(vm.relationDialog.tableConfig.url, 'commandexecution', row.id)
vm.relationDialog.show = true
}
return <el-link onClick={onClick}>{ cellValue.length }</el-link>
return <el-link class='text-link' onClick={onClick}>{ cellValue.length }</el-link>
}
},
command: {
@@ -67,7 +67,7 @@ export default {
formatter: (row) => {
const label = this.$t('audits.View')
const route = { to: { name: 'CeleryTaskLog', params: { id: row.id }}}
return <router-link {...{ attrs: route }} target='_blank'>{ label }</router-link>
return <router-link class='text-link' {...{ attrs: route }} target='_blank'>{ label }</router-link>
}
},
date_start: {
@@ -138,5 +138,4 @@ export default {
</script>
<style>
</style>

View File

@@ -6,6 +6,7 @@
import ListTable from '@/components/ListTable'
import { ActionsFormatter } from '@/components/TableFormatters'
import { toSafeLocalDateStr } from '@/utils/common'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'AdhocExecutionHistory',
@@ -86,6 +87,14 @@ export default {
callback: function({ row, tableData }) {
return this.$router.push({ name: 'HistoryExecutionDetail', params: { id: row.id }})
}
},
{
name: 'log',
title: this.$t('ops.output'),
type: 'info',
callback: function({ row }) {
openTaskPage(row.id, 'ansible')
}
}
]
}

View File

@@ -15,7 +15,6 @@ import DetailCard from '@/components/DetailCard'
import { toSafeLocalDateStr } from '@/utils/common'
import RunInfoCard from '../../RunInfoCard'
import { toLastFailureDisplay, toLastSucessDisplay } from '../business'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'HistoryExecutionDetail',
@@ -72,17 +71,6 @@ export default {
{
key: this.$t('ops.isSuccess'),
value: this.object.is_success
},
{
key: this.$t('ops.output'),
value: this.object.id,
formatter: function(row, value) {
const onClick = function() {
openTaskPage(value, 'ansible')
}
const title = this.$t('common.View')
return <a onClick={onClick} >{ title }</a>
}
}
]
}

View File

@@ -108,7 +108,7 @@ export default {
openTaskPage(value, 'ansible')
}
const title = this.$t('common.View')
return <a onClick={onClick} >{ title }</a>
return <a class='text-link' onClick={onClick} >{ title }</a>
}
}
]

View File

@@ -6,6 +6,7 @@
import ListTable from '@/components/ListTable'
import { DetailFormatter } from '@/components/TableFormatters'
import { toSafeLocalDateStr } from '@/utils/common'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'TaskHistory',
@@ -95,6 +96,14 @@ export default {
callback: function({ row, tableData }) {
return this.$router.push({ name: 'HistoryExecutionDetail', params: { id: row.id }})
}
},
{
name: 'log',
title: this.$t('ops.output'),
type: 'info',
callback: function({ row }) {
openTaskPage(row.id, 'ansible')
}
}
]
}

View File

@@ -78,6 +78,7 @@ export default {
},
onPerformSuccess() {
this.$message.success(this.$t('common.updateSuccessMsg'))
this.$store.dispatch('users/ifFirstLogin', false)
setTimeout(() => this.$router.push({ name: 'ProfileInfo' }), 100)
},
submitMethod() {

View File

@@ -37,7 +37,8 @@ import DetailCard from '@/components/DetailCard'
import QuickActions from '@/components/QuickActions'
import UserConfirmDialog from '@/components/UserConfirmDialog'
import { toSafeLocalDateStr } from '@/utils/common'
import store from '@/store'
import { getProfile } from '@/api/users'
import { mapState } from 'vuex'
export default {
name: 'ProfileInfo',
@@ -47,14 +48,9 @@ export default {
QuickActions,
UserConfirmDialog
},
props: {
object: {
type: Object,
default: () => store.state.users.profile
}
},
data() {
return {
object: this.userProfile || {},
url: `/api/v1/users/profile/`,
showPasswordDialog: false,
currentEdit: '',
@@ -160,7 +156,7 @@ export default {
attrs: {
disabled: true,
name: 'site_msg',
model: this.object.receive_backends.indexOf('site_msg') !== -1
model: this.object?.receive_backends.indexOf('site_msg') !== -1
},
callbacks: {
change: this.updateUserReceiveBackends
@@ -171,7 +167,7 @@ export default {
type: 'switcher',
attrs: {
name: 'email',
model: this.object.receive_backends.indexOf('email') !== -1
model: this.object?.receive_backends.indexOf('email') !== -1
},
callbacks: {
change: this.updateUserReceiveBackends
@@ -182,7 +178,7 @@ export default {
type: 'switcher',
attrs: {
name: 'wecom',
model: this.object.receive_backends.indexOf('wecom') !== -1
model: this.object?.receive_backends.indexOf('wecom') !== -1
},
has: this.$store.getters.publicSettings.AUTH_WECOM,
callbacks: {
@@ -194,7 +190,7 @@ export default {
type: 'switcher',
attrs: {
name: 'dingtalk',
model: this.object.receive_backends.indexOf('dingtalk') !== -1
model: this.object?.receive_backends.indexOf('dingtalk') !== -1
},
has: this.$store.getters.publicSettings.AUTH_DINGTALK,
callbacks: {
@@ -206,7 +202,7 @@ export default {
type: 'switcher',
attrs: {
name: 'feishu',
model: this.object.receive_backends.indexOf('feishu') !== -1
model: this.object?.receive_backends.indexOf('feishu') !== -1
},
has: this.$store.getters.publicSettings.AUTH_FEISHU,
callbacks: {
@@ -217,6 +213,9 @@ export default {
}
},
computed: {
...mapState({
userProfile: state => state.users.profile
}),
detailCardItems() {
return [
{
@@ -299,6 +298,12 @@ export default {
return url
}
},
created() {
getProfile().then(res => {
this.object = res
this.$store.commit('users/SET_PROFILE', res)
})
},
methods: {
updateUserReceiveBackends(val) {
this.$axios.patch(

View File

@@ -8,7 +8,7 @@
<script>
import BaseAuth from './Base'
import { JsonRequired } from '@/components/DataForm/rules'
import { JsonRequiredUserNameMapped } from '@/components/DataForm/rules'
import { JsonEditor } from '@/components/FormFields'
export default {
@@ -25,16 +25,14 @@ export default {
'AUTH_CAS', 'CAS_SERVER_URL', 'CAS_ROOT_PROXIED_AS', 'CAS_VERSION'
]],
[this.$t('common.Other'), [
'CAS_LOGOUT_COMPLETELY', 'CAS_USERNAME_ATTRIBUTE',
'CAS_APPLY_ATTRIBUTES_TO_USER', 'CAS_RENAME_ATTRIBUTES',
'CAS_CREATE_USER'
'CAS_LOGOUT_COMPLETELY', 'CAS_RENAME_ATTRIBUTES', 'CAS_CREATE_USER'
]]
],
fieldsMeta: {
CAS_RENAME_ATTRIBUTES: {
component: JsonEditor,
label: this.$t('setting.authUserAttrMap'),
rules: [JsonRequired]
rules: [JsonRequiredUserNameMapped]
}
},
submitMethod: () => 'patch',
@@ -43,9 +41,18 @@ export default {
return obj
},
cleanFormValue(data) {
if (data['CAS_RENAME_ATTRIBUTES']) {
data['CAS_RENAME_ATTRIBUTES'] = JSON.parse(data['CAS_RENAME_ATTRIBUTES'])
let userNameAttribute = ''
const renameAttributes = JSON.parse(data['CAS_RENAME_ATTRIBUTES'])
if (renameAttributes) {
data['CAS_RENAME_ATTRIBUTES'] = renameAttributes
}
for (const key in renameAttributes) {
if (renameAttributes[key] === 'username') {
userNameAttribute = key
}
}
data['CAS_USERNAME_ATTRIBUTE'] = userNameAttribute
data['CAS_APPLY_ATTRIBUTES_TO_USER'] = true
return data
}
}

View File

@@ -36,9 +36,11 @@ export default {
'AUTH_OAUTH2_SCOPE',
'AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT',
'AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT',
'AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT'
'AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT',
'AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT'
]],
[this.$t('common.Other'), [
'AUTH_OAUTH2_LOGOUT_COMPLETELY',
'AUTH_OAUTH2_ALWAYS_UPDATE_USER',
'AUTH_OAUTH2_USER_ATTR_MAP'
]]

View File

@@ -67,8 +67,6 @@ export default {
return results
},
selectedData: [],
boxTitle: [this.$t('notifications.Constituency'), this.$t('notifications.Selected')],
pageTexts: [this.$t('notifications.PrePage'), this.$t('notifications.NextPage')],
showClearBtn: true
}
}

View File

@@ -60,6 +60,7 @@ export default {
actions: {
prop: 'id',
formatterArgs: {
canUpdate: this.$hasPerm('orgs.change_organization'),
canDelete: function({ row }) {
return !row.is_default && vm.$hasPerm('orgs.delete_organization')
},

View File

@@ -0,0 +1,87 @@
<template>
<BaseSMS :title="$t('setting.HuaweiCloud')" :config="$data" />
</template>
<script>
import BaseSMS from './Base'
import { UpdateToken } from '@/components/FormFields'
export default {
name: 'SMSHuawei',
components: {
BaseSMS
},
data() {
const vm = this
return {
url: `/api/v1/settings/setting/?category=huawei`,
hasDetailInMsg: false,
visible: false,
moreButtons: [
{
title: this.$t('common.Test'),
loading: false,
callback: function(value, form, btn) {
btn.loading = true
vm.$axios.post(
`/api/v1/settings/sms/huawei/testing/`,
value
).then(res => {
vm.$message.success(res['msg'])
}).catch(() => {
vm.$log.error('err occur')
}).finally(() => { btn.loading = false })
}
}
],
fields: [
[
this.$t('common.BasicInfo'),
[
'HUAWEI_APP_KEY', 'HUAWEI_APP_SECRET', 'HUAWEI_SMS_ENDPOINT'
]
],
[
this.$t('setting.VerifySignTmpl'),
[
'HUAWEI_SIGN_CHANNEL_NUM', 'HUAWEI_VERIFY_SIGN_NAME', 'HUAWEI_VERIFY_TEMPLATE_CODE'
]
],
[
this.$t('common.Other'),
[
'SMS_TEST_PHONE'
]
]
],
fieldsMeta: {
HUAWEI_VERIFY_SIGN_TMPL: {
fields: ['SIGN_NAME', 'TEMPLATE_CODE'],
fieldsMeta: {
}
},
HUAWEI_APP_SECRET: {
component: UpdateToken
},
HUAWEI_SIGN_CHANNEL_NUM: {
label: this.$t('setting.SignChannelNum')
},
HUAWEI_SMS_ENDPOINT: {
label: this.$t('setting.AppEndpoint')
}
},
submitMethod() {
return 'put'
}
}
},
computed: {
},
methods: {
}
}
</script>
<style scoped>
</style>

View File

@@ -6,6 +6,7 @@
import GenericCreateUpdatePage from '@/layout/components/GenericCreateUpdatePage'
import SMSAlibaba from './SMSAlibaba'
import SMSTencent from './SMSTencent'
import SMSHuawei from './SMSHuawei'
import CMPP2 from './CMPP2'
export default {
@@ -24,7 +25,7 @@ export default {
],
[
this.$t('setting.SMSProvider'), [
'ALIYUN', 'QCLOUD', 'CMPP2'
'ALIYUN', 'QCLOUD', 'HUAWEICLOUD', 'CMPP2'
]
]
],
@@ -43,6 +44,13 @@ export default {
return form['SMS_BACKEND'] !== 'tencent'
}
},
HUAWEICLOUD: {
label: this.$t('setting.HuaweiCloud'),
component: SMSHuawei,
hidden: (form) => {
return form['SMS_BACKEND'] !== 'huawei'
}
},
CMPP2: {
label: this.$t('setting.CMPP2'),
component: CMPP2,

View File

@@ -39,9 +39,10 @@ export default {
},
actions: {
formatterArgs: {
canUpdate: this.$hasPerm('terminal.change_endpoint'),
updateRoute: 'EndpointUpdate',
cloneRoute: 'EndpointCreate',
canDelete: ({ row }) => row.id !== '00000000-0000-0000-0000-000000000001'
canDelete: ({ row }) => row.id !== '00000000-0000-0000-0000-000000000001' && this.$hasPerm('terminal.delete_endpoint')
}
}
}

View File

@@ -38,6 +38,7 @@ export default {
},
actions: {
formatterArgs: {
canUpdate: this.$hasPerm('terminal.change_endpointrule'),
updateRoute: 'EndpointRuleUpdate',
cloneRoute: 'EndpointRuleCreate'
}

View File

@@ -1,27 +1,38 @@
<template>
<Page v-bind="$attrs">
<IBox>
<div>
<el-form ref="testForm" label-width="20%" :model="testData" :rules="testRules">
<el-form-item :label="$t('setting.basicTools')">
<el-radio-group v-model="testData.tool_type" @change="changeToolType">
<el-radio v-for="t in tools" :key="t" :label="t" />
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('setting.destinationIP')" prop="dest_addr">
<el-input v-model="testData.dest_addr" :placeholder="$t('setting.destinationIP')" />
</el-form-item>
<el-form-item v-if="testData.tool_type=='Telnet'" :label="$t('setting.testPort')" prop="port_num">
<el-input v-model="testData.port_num" :placeholder="$t('setting.testPort')" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="isTesting" @click="submitTest">{{ $t('setting.testTools') }}</el-button>
</el-form-item>
<el-form-item>
<el-input v-model="testResp" type="textarea" :readonly="true" :rows="8" :placeholder="$t('setting.testHelpText')" />
</el-form-item>
</el-form>
</div>
<el-form ref="testForm" label-width="20%" :model="testData" :rules="testRules">
<el-form-item :label="$t('setting.basicTools')">
<el-radio-group v-model="testData.tool_type" @change="changeToolType">
<el-radio v-for="t in tools" :key="t" :label="t" />
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('setting.destinationIP')" prop="dest_addr">
<el-input v-model="testData.dest_addr" :placeholder="$t('setting.destinationIP')" />
</el-form-item>
<el-form-item v-if="testData.tool_type=='Telnet'" :label="$t('setting.testPort')" prop="port_num">
<el-input v-model="testData.port_num" :placeholder="$t('setting.testPort')" />
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="small"
:loading="isTesting"
@click="submitTest"
>
{{ $t('setting.testTools') }}
</el-button>
</el-form-item>
<el-form-item>
<el-input
v-model="testResp"
type="textarea"
:readonly="true"
:rows="8"
:placeholder="$t('setting.testHelpText')"
/>
</el-form-item>
</el-form>
</IBox>
</Page>
</template>
@@ -95,20 +106,19 @@ export default {
}
</script>
<style scoped>
.el-form ::v-deep .el-form-item {
margin-bottom: 12px;
}
.el-form ::v-deep .el-form-item__content {
width: 75%;
}
.el-form ::v-deep .el-form-item__label {
padding: 0 30px 0 0;
}
.el-form ::v-deep .el-form-item__error {
<style lang="scss" scoped>
.el-form {
&>>> .el-form-item {
margin-bottom: 12px;
}
&>>> .el-form-item__content {
width: 75%;
}
&>>> .el-form-item__label {
padding: 0 30px 0 0;
}
&>>> .el-form-item__error {
position: inherit;
}
}
</style>

View File

@@ -141,7 +141,7 @@ export default {
valueLabel: this.$t('tickets.Pending')
}
},
exclude: ['state'],
exclude: ['state', 'id', 'title'],
options: [
{
value: 'state',
@@ -162,6 +162,30 @@ export default {
label: this.$t('tickets.Rejected')
}
]
},
{
value: 'id',
label: 'ID'
},
{
value: 'title',
label: this.$t('tickets.title')
},
{
value: 'relevant_app',
label: this.$t('tickets.RelevantApp')
},
{
value: 'relevant_asset',
label: this.$t('tickets.RelevantAsset')
},
{
value: 'relevant_system_user',
label: this.$t('tickets.RelevantCommand')
},
{
value: 'relevant_command',
label: this.$t('tickets.RelevantSystemUser')
}
]
},

View File

@@ -115,6 +115,7 @@ export default {
actions: {
formatterArgs: {
hasDelete: hasDelete,
canUpdate: this.$hasPerm('users.change_user'),
extraActions: [
{
title: this.$t('users.Remove'),

View File

@@ -6290,11 +6290,16 @@ jest@^23.6.0:
import-local "^1.0.0"
jest-cli "^23.6.0"
jquery@>=1.4.4, jquery@^3.5.0:
jquery@>=1.4.4:
version "3.5.0"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.0.tgz#9980b97d9e4194611c36530e7dc46a58d7340fc9"
integrity sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ==
jquery@^3.6.1:
version "3.6.1"
resolved "https://registry.npmmirror.com/jquery/-/jquery-3.6.1.tgz#fab0408f8b45fc19f956205773b62b292c147a16"
integrity sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw==
js-base64@^2.1.9:
version "2.5.2"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.2.tgz#313b6274dda718f714d00b3330bbae6e38e90209"