mirror of
https://github.com/jumpserver/lina.git
synced 2026-05-18 13:45:01 +00:00
perf: update encrypt support gm
This commit is contained in:
@@ -59,6 +59,7 @@
|
||||
"npm": "^7.8.0",
|
||||
"nprogress": "0.2.0",
|
||||
"path-to-regexp": "3.3.0",
|
||||
"sm-crypto": "^0.4.0",
|
||||
"sortablejs": "^1.15.6",
|
||||
"v-sanitize": "^0.0.13",
|
||||
"vue": "2.7.16",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<AutoDataForm
|
||||
v-if="!loading"
|
||||
ref="AutoDataForm"
|
||||
:class="addTemplate? '': 'account-add'"
|
||||
:class="addTemplate ? '' : 'account-add'"
|
||||
:submit-btn-text="submitBtnText"
|
||||
v-bind="$data"
|
||||
@submit="confirm"
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<script>
|
||||
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
|
||||
|
||||
export default {
|
||||
@@ -59,16 +59,27 @@ export default {
|
||||
]
|
||||
},
|
||||
url: '/api/v1/accounts/accounts/',
|
||||
form: Object.assign({ 'on_invalid': 'error' }, this.account || {}),
|
||||
form: Object.assign({ on_invalid: 'error' }, this.account || {}),
|
||||
encryptedFields: ['secret'],
|
||||
fields: [
|
||||
[this.$t('Basic'), ['name', 'username', 'privileged', 'su_from', 'su_from_username', 'template']],
|
||||
[
|
||||
this.$t('Basic'),
|
||||
['name', 'username', 'privileged', 'su_from', 'su_from_username', 'template']
|
||||
],
|
||||
[this.$t('Asset'), ['nodes', 'assets']],
|
||||
[this.$t('Secret'), [
|
||||
'secret_type', 'password', 'ssh_key', 'token',
|
||||
'access_key', 'passphrase', 'api_key',
|
||||
'secret_reset'
|
||||
]],
|
||||
[
|
||||
this.$t('Secret'),
|
||||
[
|
||||
'secret_type',
|
||||
'password',
|
||||
'ssh_key',
|
||||
'token',
|
||||
'access_key',
|
||||
'passphrase',
|
||||
'api_key',
|
||||
'secret_reset'
|
||||
]
|
||||
],
|
||||
[this.$t('Other'), ['push_now', 'params', 'on_invalid', 'is_active', 'comment']]
|
||||
],
|
||||
fieldsMeta: accountFieldsMeta(this),
|
||||
@@ -170,7 +181,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
<style lang="scss" scoped>
|
||||
.account-add {
|
||||
::v-deep .el-form-item {
|
||||
//margin-bottom: 5px;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<script>
|
||||
import { GenericUpdateFormDialog } from '@/layout/components'
|
||||
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
|
||||
export default {
|
||||
name: 'AccountBulkUpdateDialog',
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
},
|
||||
selectedRows: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -35,7 +35,7 @@ export default {
|
||||
hasSaveContinue: false,
|
||||
fields: [],
|
||||
fieldsMeta: accountFieldsMeta(this),
|
||||
cleanOtherFormValue: (formValue) => {
|
||||
cleanOtherFormValue: formValue => {
|
||||
for (const value of formValue) {
|
||||
Object.keys(value).forEach((item, index, arr) => {
|
||||
if (['ssh_key', 'token', 'access_key', 'api_key', 'password'].includes(item)) {
|
||||
@@ -82,6 +82,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
|
||||
|
||||
export default {
|
||||
@@ -46,8 +46,14 @@ export default {
|
||||
const accountMeta = accountFieldsMeta(this)
|
||||
return {
|
||||
fields: [
|
||||
'name', 'secret_type', 'password', 'ssh_key', 'token',
|
||||
'access_key', 'passphrase', 'api_key'
|
||||
'name',
|
||||
'secret_type',
|
||||
'password',
|
||||
'ssh_key',
|
||||
'token',
|
||||
'access_key',
|
||||
'passphrase',
|
||||
'api_key'
|
||||
],
|
||||
fieldsMeta: {
|
||||
...accountMeta,
|
||||
@@ -80,18 +86,19 @@ export default {
|
||||
const data = {
|
||||
secret: encryptPassword(form[secretType])
|
||||
}
|
||||
this.$axios.patch(
|
||||
`/api/v1/accounts/accounts/${this.account.id}/`,
|
||||
data,
|
||||
{ disableFlashErrorMsg: true }
|
||||
).then(res => {
|
||||
this.$message.success(this.$tc('UpdateSuccessMsg'))
|
||||
this.iVisible = false
|
||||
}).catch(err => {
|
||||
const errMsg = Object.values(err.response.data).join(', ')
|
||||
this.$message.error(this.$tc('UpdateErrorMsg') + ' ' + errMsg)
|
||||
this.iVisible = false
|
||||
})
|
||||
this.$axios
|
||||
.patch(`/api/v1/accounts/accounts/${this.account.id}/`, data, {
|
||||
disableFlashErrorMsg: true
|
||||
})
|
||||
.then(res => {
|
||||
this.$message.success(this.$tc('UpdateSuccessMsg'))
|
||||
this.iVisible = false
|
||||
})
|
||||
.catch(err => {
|
||||
const errMsg = Object.values(err.response.data).join(', ')
|
||||
this.$message.error(this.$tc('UpdateErrorMsg') + ' ' + errMsg)
|
||||
this.iVisible = false
|
||||
})
|
||||
},
|
||||
handleCancel() {
|
||||
this.$emit('update:visible', false)
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
<el-form-item :label="secretTypeLabel">
|
||||
<SecretViewerFormatter
|
||||
:cell-value="secretInfo.secret"
|
||||
:col="{ formatterArgs: {
|
||||
name: account['name'],
|
||||
secretType: secretType || ''
|
||||
}}"
|
||||
:col="{
|
||||
formatterArgs: {
|
||||
name: account['name'],
|
||||
secretType: secretType || ''
|
||||
}
|
||||
}"
|
||||
@input="onShowKeyCopyFormatterChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -36,12 +38,12 @@
|
||||
<el-form-item :label="$tc('DateUpdated')">
|
||||
<span>{{ account['date_updated'] | date }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showPasswordRecord" v-perms="'accounts.view_accountsecret'" :label="$tc('PasswordRecord')">
|
||||
<el-link
|
||||
:underline="false"
|
||||
type="success"
|
||||
@click="showHistoryDialog"
|
||||
>
|
||||
<el-form-item
|
||||
v-if="showPasswordRecord"
|
||||
v-perms="'accounts.view_accountsecret'"
|
||||
:label="$tc('PasswordRecord')"
|
||||
>
|
||||
<el-link :underline="false" type="success" @click="showHistoryDialog">
|
||||
<span style="padding-right: 30px">
|
||||
{{ versions }}
|
||||
</span>
|
||||
@@ -61,7 +63,7 @@
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import PasswordHistoryDialog from './PasswordHistoryDialog.vue'
|
||||
import { SecretViewerFormatter } from '@/components/Table/TableFormatters'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
@@ -90,7 +92,7 @@ export default {
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
default: function () {
|
||||
return this.$tc('Detail')
|
||||
}
|
||||
},
|
||||
@@ -144,7 +146,8 @@ export default {
|
||||
name: this.secretInfo.name,
|
||||
secret: encryptPassword(this.modifiedSecret)
|
||||
}
|
||||
const url = this.type === 'account' ? `/api/v1/accounts/accounts` : `/api/v1/accounts/account-templates`
|
||||
const url =
|
||||
this.type === 'account' ? `/api/v1/accounts/accounts` : `/api/v1/accounts/account-templates`
|
||||
this.$axios.patch(`${url}/${this.account.id}/`, params).then(() => {
|
||||
this.$message.success(this.$tc('UpdateSuccessMsg'))
|
||||
})
|
||||
@@ -154,7 +157,7 @@ export default {
|
||||
this.$message.warning(this.$tc('AccountSecretReadDisabled'))
|
||||
return
|
||||
}
|
||||
return this.$axios.get(this.url).then((res) => {
|
||||
return this.$axios.get(this.url).then(res => {
|
||||
this.secretInfo = res
|
||||
this.sshKeyFingerprint = res?.spec_info?.ssh_key_fingerprint || '-'
|
||||
this.showSecret = true
|
||||
@@ -180,7 +183,7 @@ export default {
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding: 5px 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import { logout as redirectToLogout } from '@/utils/request'
|
||||
|
||||
export default {
|
||||
@@ -180,7 +180,7 @@ export default {
|
||||
this.inputPlaceholder = this.subTypeChoices.filter(item => item.name === val)[0]?.placeholder
|
||||
this.smsWidth = val === 'sms' ? 6 : 0
|
||||
},
|
||||
performConfirm: _.debounce(function({ response, callback, cancel }) {
|
||||
performConfirm: _.debounce(function ({ response, callback, cancel }) {
|
||||
if (this.processing || this.visible) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -11,10 +11,18 @@
|
||||
<div class="row">
|
||||
<el-progress :percentage="processedPercent" />
|
||||
</div>
|
||||
<DataTable v-if="tableGenDone" id="importTable" ref="dataTable" :config="tableConfig" class="importTable" />
|
||||
<DataTable
|
||||
v-if="tableGenDone"
|
||||
id="importTable"
|
||||
ref="dataTable"
|
||||
:config="tableConfig"
|
||||
class="importTable"
|
||||
/>
|
||||
<div class="row" style="padding-top: 20px">
|
||||
<div class="btn-groups">
|
||||
<el-button v-if="showCancel" size="small" @click="performCancel">{{ $t('Cancel') }}</el-button>
|
||||
<el-button v-if="showCancel" size="small" @click="performCancel">{{
|
||||
$t('Cancel')
|
||||
}}</el-button>
|
||||
<el-button
|
||||
v-show="!disableImportBtn"
|
||||
size="small"
|
||||
@@ -45,7 +53,7 @@ import DataTable from '@/components/Table/DataTable/index.vue'
|
||||
import { getUpdateObjURL } from '@/utils/common/index'
|
||||
import { sleep } from '@/utils/common/time'
|
||||
import { EditableInputFormatter } from '@/components/Table/TableFormatters'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import getStatusColumnMeta from '@/components/Table/ListTable/TableAction/const'
|
||||
|
||||
export default {
|
||||
@@ -162,17 +170,17 @@ export default {
|
||||
return this.importActions[this.importAction]
|
||||
},
|
||||
successData() {
|
||||
return this.iTotalData.filter((item) => {
|
||||
return this.iTotalData.filter(item => {
|
||||
return item['@status'] === 'ok'
|
||||
})
|
||||
},
|
||||
failedData() {
|
||||
return this.iTotalData.filter((item) => {
|
||||
return this.iTotalData.filter(item => {
|
||||
return typeof item['@status'] === 'object' && item['@status'].name === 'error'
|
||||
})
|
||||
},
|
||||
pendingData() {
|
||||
return this.iTotalData.filter((item) => {
|
||||
return this.iTotalData.filter(item => {
|
||||
return item['@status'] === 'pending'
|
||||
})
|
||||
},
|
||||
@@ -195,7 +203,7 @@ export default {
|
||||
if (this.totalCount === 0) {
|
||||
return 0
|
||||
}
|
||||
return Math.round(this.processedCount / this.totalCount * 100)
|
||||
return Math.round((this.processedCount / this.totalCount) * 100)
|
||||
},
|
||||
elDataTable() {
|
||||
return this.$refs['dataTable'].dataTable
|
||||
@@ -218,7 +226,7 @@ export default {
|
||||
} else if (val === 'error') {
|
||||
this.tableConfig.totalData = this.failedData
|
||||
} else {
|
||||
this.tableConfig.totalData = this.iTotalData.filter((item) => {
|
||||
this.tableConfig.totalData = this.iTotalData.filter(item => {
|
||||
return item['@status'] === val
|
||||
})
|
||||
}
|
||||
@@ -278,7 +286,8 @@ export default {
|
||||
return columns
|
||||
},
|
||||
getEncryptFields() {
|
||||
const fromProp = Array.isArray(this.encryptFields) && this.encryptFields.length ? this.encryptFields : null
|
||||
const fromProp =
|
||||
Array.isArray(this.encryptFields) && this.encryptFields.length ? this.encryptFields : null
|
||||
return fromProp || ['password', 'secret', 'private_key']
|
||||
},
|
||||
generateTableData(tableTitles, tableData) {
|
||||
@@ -415,11 +424,7 @@ export default {
|
||||
},
|
||||
async performUpdateObject(item) {
|
||||
const updateUrl = getUpdateObjURL(this.url, item.id)
|
||||
return this.$axios.patch(
|
||||
updateUrl,
|
||||
item,
|
||||
{ disableFlashErrorMsg: true }
|
||||
)
|
||||
return this.$axios.patch(updateUrl, item, { disableFlashErrorMsg: true })
|
||||
},
|
||||
async defaultPerformUploadObject(item) {
|
||||
let handler = this.performCreateObject
|
||||
@@ -439,11 +444,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async performCreateObject(item) {
|
||||
return this.$axios.post(
|
||||
this.url,
|
||||
item,
|
||||
{ disableFlashErrorMsg: true }
|
||||
)
|
||||
return this.$axios.post(this.url, item, { disableFlashErrorMsg: true })
|
||||
},
|
||||
keepElementInViewport() {
|
||||
const tableRef = document.getElementById('importTable')
|
||||
@@ -455,11 +456,7 @@ export default {
|
||||
const rect = parentTdRef.getBoundingClientRect()
|
||||
let windowInnerHeight = window.innerHeight || document.documentElement.clientHeight
|
||||
windowInnerHeight = windowInnerHeight * 0.97 - 150
|
||||
const inViewport = (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= windowInnerHeight
|
||||
)
|
||||
const inViewport = rect.top >= 0 && rect.left >= 0 && rect.bottom <= windowInnerHeight
|
||||
if (!inViewport) {
|
||||
parentTdRef.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'start' })
|
||||
}
|
||||
@@ -468,8 +465,7 @@ export default {
|
||||
this.tableConfig.totalData.push(item)
|
||||
},
|
||||
handleClick(btn) {
|
||||
const callback = btn.callback || function() {
|
||||
}
|
||||
const callback = btn.callback || function () {}
|
||||
callback(btn)
|
||||
}
|
||||
}
|
||||
@@ -477,10 +473,10 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/styles/variables";
|
||||
@import '~@/styles/variables';
|
||||
|
||||
.summary-item {
|
||||
padding: 0 10px
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.summary-success {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<script>
|
||||
import AutoDataForm from '@/components/Form/AutoDataForm'
|
||||
import { getUpdateObjURL } from '@/utils/common/index'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import deepmerge from 'deepmerge'
|
||||
|
||||
export default {
|
||||
@@ -45,12 +45,12 @@ export default {
|
||||
},
|
||||
afterGetFormValue: {
|
||||
type: Function,
|
||||
default: (value) => value
|
||||
default: value => value
|
||||
},
|
||||
// 提交前,清理form的值
|
||||
cleanFormValue: {
|
||||
type: Function,
|
||||
default: (value) => value
|
||||
default: value => value
|
||||
},
|
||||
// 获取 meta
|
||||
afterGetRemoteMeta: {
|
||||
@@ -76,28 +76,28 @@ export default {
|
||||
// 创建成功的msg
|
||||
createSuccessMsg: {
|
||||
type: String,
|
||||
default: function() {
|
||||
default: function () {
|
||||
return this.$t('CreateSuccessMsg')
|
||||
}
|
||||
},
|
||||
// 保存成功,继续添加的msg
|
||||
saveSuccessContinueMsg: {
|
||||
type: String,
|
||||
default: function() {
|
||||
default: function () {
|
||||
return this.$t('SaveSuccessContinueMsg')
|
||||
}
|
||||
},
|
||||
// 更新成功的msg
|
||||
updateSuccessMsg: {
|
||||
type: String,
|
||||
default: function() {
|
||||
default: function () {
|
||||
return this.$t('UpdateSuccessMsg')
|
||||
}
|
||||
},
|
||||
// 创建成功的跳转路由
|
||||
createSuccessNextRoute: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
default: function () {
|
||||
const routeName = this.$route.name?.replace('Create', 'List')
|
||||
return { name: routeName }
|
||||
}
|
||||
@@ -105,16 +105,15 @@ export default {
|
||||
// 更新成功的跳转路由
|
||||
updateSuccessNextRoute: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
default: function () {
|
||||
const routeName = this.$route.name?.replace('Update', 'List')
|
||||
return { name: routeName }
|
||||
}
|
||||
},
|
||||
objectDetailRoute: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
const routeName = this.$route.name?.replace('Update', 'Detail')
|
||||
.replace('Create', 'Detail')
|
||||
default: function () {
|
||||
const routeName = this.$route.name?.replace('Update', 'Detail').replace('Create', 'Detail')
|
||||
return { name: routeName }
|
||||
}
|
||||
},
|
||||
@@ -127,7 +126,7 @@ export default {
|
||||
},
|
||||
cloneNameSuffix: {
|
||||
type: [String, Number],
|
||||
default: function() {
|
||||
default: function () {
|
||||
return this.$t('Duplicate').toLowerCase()
|
||||
}
|
||||
},
|
||||
@@ -139,7 +138,7 @@ export default {
|
||||
// 获取创建和更新的url function
|
||||
getUrl: {
|
||||
type: Function,
|
||||
default: function() {
|
||||
default: function () {
|
||||
const objectId = this.getUpdateId()
|
||||
let url = this.url
|
||||
if (objectId) {
|
||||
@@ -180,12 +179,16 @@ export default {
|
||||
msg = msg[0].toLowerCase() + msg.slice(1)
|
||||
this.$message({
|
||||
message: h('p', null, [
|
||||
h('el-link', {
|
||||
on: {
|
||||
click: () => this.$router.push(detailRoute)
|
||||
h(
|
||||
'el-link',
|
||||
{
|
||||
on: {
|
||||
click: () => this.$router.push(detailRoute)
|
||||
},
|
||||
style: { 'vertical-align': 'top', 'margin-right': '5px' }
|
||||
},
|
||||
style: { 'vertical-align': 'top', 'margin-right': '5px' }
|
||||
}, msgLinkName),
|
||||
msgLinkName
|
||||
),
|
||||
h('span', {}, msg)
|
||||
]),
|
||||
type: 'success'
|
||||
@@ -200,9 +203,12 @@ export default {
|
||||
default(res, method, vm, addContinue) {
|
||||
const route = this.getNextRoute(res, method)
|
||||
if (!(route.params && route.params.id)) {
|
||||
route['params'] = deepmerge(route['params'] || {}, { 'id': res.id })
|
||||
route['params'] = deepmerge(route['params'] || {}, { id: res.id })
|
||||
}
|
||||
route['query'] = deepmerge(route['query'], { 'order': this.extraQueryOrder, 'updated': new Date().getTime() })
|
||||
route['query'] = deepmerge(route['query'], {
|
||||
order: this.extraQueryOrder,
|
||||
updated: new Date().getTime()
|
||||
})
|
||||
|
||||
this.$emit('submitSuccess', res)
|
||||
|
||||
@@ -344,7 +350,7 @@ export default {
|
||||
encryptFields(values) {
|
||||
// 批量提交,clean 后可能是个数组
|
||||
if (values instanceof Array) {
|
||||
return values.map((item) => this.encryptFields(item))
|
||||
return values.map(item => this.encryptFields(item))
|
||||
}
|
||||
values = { ...values }
|
||||
for (const field of this.encryptedFields) {
|
||||
@@ -372,8 +378,8 @@ export default {
|
||||
defaultOnSubmit(validValues, formName, addContinue) {
|
||||
this.isSubmitting = true
|
||||
this.performSubmit(validValues)
|
||||
.then((res) => this.onPerformSuccess.bind(this)(res, this.method, this, addContinue))
|
||||
.catch((error) => this.onPerformError(error, this.method, this))
|
||||
.then(res => this.onPerformSuccess.bind(this)(res, this.method, this, addContinue))
|
||||
.catch(error => this.onPerformError(error, this.method, this))
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.isSubmitting = false
|
||||
@@ -383,7 +389,7 @@ export default {
|
||||
},
|
||||
async getCloneForm(cloneFrom) {
|
||||
const [curUrl, query] = this.url.split('?')
|
||||
const url = `${curUrl}${cloneFrom}/${query ? ('?' + query) : ''}`
|
||||
const url = `${curUrl}${cloneFrom}/${query ? '?' + query : ''}`
|
||||
try {
|
||||
const object = await this.getObjectDetail(url)
|
||||
let name = ''
|
||||
@@ -438,5 +444,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/**
|
||||
* Created by PanJiaChen on 16/11/18.
|
||||
*/
|
||||
import { encryptPassword } from './session-encrypt'
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
@@ -43,60 +41,6 @@ const options = {
|
||||
}
|
||||
}
|
||||
const filter = new xss.FilterXSS(options)
|
||||
|
||||
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { vueCookie as VueCookie } from '@/utils/storage'
|
||||
|
||||
export function fillKey(key) {
|
||||
const KeyLength = 16
|
||||
if (key.length > KeyLength) {
|
||||
key = key.slice(0, KeyLength)
|
||||
}
|
||||
const filledKey = Buffer.alloc(KeyLength)
|
||||
const keys = Buffer.from(key)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
filledKey[i] = keys[i]
|
||||
}
|
||||
return filledKey
|
||||
}
|
||||
|
||||
export function aesEncrypt(text, originKey) {
|
||||
const key = CryptoJS.enc.Utf8.parse(fillKey(originKey))
|
||||
return CryptoJS.AES.encrypt(text, key, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.ZeroPadding
|
||||
}).toString()
|
||||
}
|
||||
|
||||
export function rsaEncrypt(text, pubKey) {
|
||||
const jsEncrypt = new JSEncrypt()
|
||||
jsEncrypt.setPublicKey(pubKey)
|
||||
return jsEncrypt.encrypt(text)
|
||||
}
|
||||
|
||||
export function getCookie(name) {
|
||||
return VueCookie.get(name)
|
||||
}
|
||||
|
||||
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 存储的
|
||||
rsaPublicKeyText = rsaPublicKeyText.replaceAll('"', '')
|
||||
const rsaPublicKey = atob(rsaPublicKeyText)
|
||||
const keyCipher = rsaEncrypt(aesKey, rsaPublicKey)
|
||||
const passwordCipher = aesEncrypt(String(password), aesKey)
|
||||
return `${keyCipher}:${passwordCipher}`
|
||||
}
|
||||
|
||||
window.aesEncrypt = aesEncrypt
|
||||
window.fillKey = fillKey
|
||||
|
||||
export default filter
|
||||
|
||||
window.encryptPassword = encryptPassword
|
||||
|
||||
160
src/utils/session-encrypt.js
Normal file
160
src/utils/session-encrypt.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { vueCookie as VueCookie } from '@/utils/storage'
|
||||
import { sm2, sm4 } from 'sm-crypto'
|
||||
|
||||
export function getCookie(name) {
|
||||
return VueCookie.get(name)
|
||||
}
|
||||
|
||||
export function fillKey(key) {
|
||||
const KeyLength = 16
|
||||
if (key.length > KeyLength) {
|
||||
key = key.slice(0, KeyLength)
|
||||
}
|
||||
const filledKey = Buffer.alloc(KeyLength)
|
||||
const keys = Buffer.from(key)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
filledKey[i] = keys[i]
|
||||
}
|
||||
return filledKey
|
||||
}
|
||||
|
||||
function aesEncrypt(text, originKey) {
|
||||
const key = CryptoJS.enc.Utf8.parse(fillKey(originKey))
|
||||
return CryptoJS.AES.encrypt(text, key, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.ZeroPadding
|
||||
}).toString()
|
||||
}
|
||||
|
||||
function rsaEncrypt(text, pubKey) {
|
||||
if (!text) {
|
||||
return text
|
||||
}
|
||||
const jsEncrypt = new JSEncrypt()
|
||||
jsEncrypt.setPublicKey(pubKey)
|
||||
return jsEncrypt.encrypt(text)
|
||||
}
|
||||
|
||||
function rsaDecrypt(cipher, pkey) {
|
||||
const jsEncrypt = new JSEncrypt()
|
||||
jsEncrypt.setPrivateKey(pkey)
|
||||
return jsEncrypt.decrypt(cipher)
|
||||
}
|
||||
|
||||
window.rsaEncrypt = rsaEncrypt
|
||||
window.rsaDecrypt = rsaDecrypt
|
||||
|
||||
function hexToBytes(hex) {
|
||||
if (!hex) return new Uint8Array([])
|
||||
hex = hex.toString().trim().toLowerCase()
|
||||
if (hex.startsWith('0x')) {
|
||||
hex = hex.slice(2)
|
||||
}
|
||||
// 确保是偶数长度
|
||||
const len = Math.floor(hex.length / 2)
|
||||
const bytes = new Uint8Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = parseInt(hex.substr(i * 2, 2), 16)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
// Uint8Array -> base64(标准 base64)
|
||||
let binary = ''
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binary += String.fromCharCode(bytes[i])
|
||||
}
|
||||
return btoa(binary)
|
||||
}
|
||||
|
||||
function rsaEncryptPassword(password, rsaPublicKey) {
|
||||
const aesKey = (Math.random() + 1).toString(36).substring(2)
|
||||
// public key 是 base64 存储的
|
||||
const keyCipher = rsaEncrypt(aesKey, rsaPublicKey)
|
||||
const passwordCipher = aesEncrypt(password, aesKey)
|
||||
return `${keyCipher}:${passwordCipher}`
|
||||
}
|
||||
|
||||
function ensureSm2PublicKey(sm2PublicKey) {
|
||||
// sm2.min.js 的 doEncrypt 需要能被 decodePointHex 解析的公钥:
|
||||
// 通常为非压缩点 hex,格式 `04||x||y`(总长度 130)。
|
||||
// 但后端生成/下发的公钥有时是 `x||y`(长度 128),这里做归一化补齐 `04` 前缀。
|
||||
if (typeof sm2PublicKey === 'string') {
|
||||
sm2PublicKey = sm2PublicKey.replaceAll('"', '').trim()
|
||||
if (sm2PublicKey.startsWith('0x')) {
|
||||
sm2PublicKey = sm2PublicKey.slice(2)
|
||||
}
|
||||
// 后端下发的 SM2 公钥常见是 x||y(128 hex),sm-crypto 需要 04||x||y(130 hex)
|
||||
if (sm2PublicKey.length === 128 && !sm2PublicKey.startsWith('04')) {
|
||||
sm2PublicKey = '04' + sm2PublicKey
|
||||
}
|
||||
}
|
||||
return sm2PublicKey
|
||||
}
|
||||
|
||||
function gmEncryptPassword(password, sm2PublicKey) {
|
||||
sm2PublicKey = ensureSm2PublicKey(sm2PublicKey)
|
||||
// 只适配前端,不改后端:
|
||||
// 直接生成 16 字符 key(后端 padding_key 会保持原样,不再补齐)
|
||||
const sm4KeyRaw = randomString(16)
|
||||
const sm4KeyHex = Buffer.from(sm4KeyRaw).toString('hex')
|
||||
|
||||
let keyCipher = ''
|
||||
try {
|
||||
// 与后端 gmssl.sm2.CryptSM2 默认 decrypt 的 mode 对齐:
|
||||
// gmssl 解析的格式是 C1C2C3(mode=0),前端这里输出也用 mode=0。
|
||||
keyCipher = sm2.doEncrypt(sm4KeyRaw, sm2PublicKey, 0)
|
||||
} catch (e) {
|
||||
console.error('gmEncryptPassword sm2.doEncrypt failed:', e)
|
||||
// 避免前端崩溃:失败时返回明文,由后端按原值流程处理(至少可继续登录/看报错)
|
||||
return password
|
||||
}
|
||||
|
||||
const passwordCipher = sm4.encrypt(password, sm4KeyHex)
|
||||
// sm2/sm4 默认输出是 hex,但后端 gm.py/session.py 需要 base64:
|
||||
// - sm2_decrypt: base64.b64decode
|
||||
// - sm4 decrypt: base64.urlsafe_b64decode
|
||||
const keyCipherB64 = bytesToBase64(hexToBytes(keyCipher))
|
||||
const passwordCipherB64 = bytesToBase64(hexToBytes(passwordCipher))
|
||||
return `${keyCipherB64}:${passwordCipherB64}`
|
||||
}
|
||||
|
||||
export function encryptPassword(password) {
|
||||
if (!password) {
|
||||
console.log('password is empty')
|
||||
return ''
|
||||
}
|
||||
let publicKeyText = getCookie('jms_public_key')
|
||||
if (!publicKeyText) {
|
||||
console.log('publicKeyText is empty')
|
||||
return password
|
||||
}
|
||||
publicKeyText = publicKeyText.replaceAll('"', '')
|
||||
publicKeyText = atob(publicKeyText)
|
||||
let cipher = ''
|
||||
let jmsGMSSL = getCookie('jms_gm_ssl')
|
||||
if (publicKeyText.includes('PUBLIC KEY')) {
|
||||
jmsGMSSL = '0'
|
||||
}
|
||||
if (jmsGMSSL === '1') {
|
||||
cipher = gmEncryptPassword(password, publicKeyText)
|
||||
} else {
|
||||
cipher = rsaEncryptPassword(password, publicKeyText)
|
||||
}
|
||||
|
||||
return cipher
|
||||
}
|
||||
|
||||
export function randomString(length) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
let result = ''
|
||||
const charactersLength = characters.length
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<script>
|
||||
import { GenericCreateUpdatePage } from '@/layout/components'
|
||||
import getChangeSecretFields from '@/views/accounts/AccountBackup/fields'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import { periodicMeta } from '@/components/const'
|
||||
|
||||
export default {
|
||||
@@ -20,7 +20,8 @@ export default {
|
||||
url: '/api/v1/accounts/account-backup-plans/',
|
||||
fields: [
|
||||
[this.$t('Basic'), ['name', 'types']],
|
||||
[this.$t('Backup'),
|
||||
[
|
||||
this.$t('Backup'),
|
||||
[
|
||||
'backup_type',
|
||||
'is_password_divided_by_email',
|
||||
@@ -91,6 +92,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<GenericCreateUpdatePage
|
||||
v-bind="$data"
|
||||
@getObjectDone="getObjectDone"
|
||||
/>
|
||||
<GenericCreateUpdatePage v-bind="$data" @getObjectDone="getObjectDone" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GenericCreateUpdatePage from '@/layout/components/GenericCreateUpdatePage'
|
||||
import { templateFields, templateFieldsMeta } from './const.js'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
|
||||
export default {
|
||||
name: 'GatewayCreateUpdate',
|
||||
@@ -27,7 +24,7 @@ export default {
|
||||
return {
|
||||
initial: {
|
||||
secret_type: 'password',
|
||||
push_params: { }
|
||||
push_params: {}
|
||||
},
|
||||
url: '/api/v1/accounts/account-templates/',
|
||||
hasDetailInMsg: false,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<script>
|
||||
import GenericCreateUpdatePage from '@/layout/components/GenericCreateUpdatePage'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import { getUpdateObjURL, setUrlParam } from '@/utils/common/index'
|
||||
import { assetFieldsMeta } from '@/views/assets/const'
|
||||
|
||||
@@ -38,7 +38,7 @@ export default {
|
||||
},
|
||||
updateInitial: {
|
||||
type: Function,
|
||||
default: (initial) => {
|
||||
default: initial => {
|
||||
return initial
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ export default {
|
||||
delete values['accounts']
|
||||
} else {
|
||||
const accounts = values?.accounts || []
|
||||
values.accounts = accounts.map((item) => {
|
||||
values.accounts = accounts.map(item => {
|
||||
item['secret'] = encryptPassword(item['secret'])
|
||||
return item
|
||||
})
|
||||
@@ -179,5 +179,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import i18n from '@/i18n/i18n'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
|
||||
export const gcp = 'gcp'
|
||||
export const aliyun = 'aliyun'
|
||||
@@ -55,8 +55,18 @@ export const publicHostProviders = [
|
||||
export const publicDBProviders = [aliyun]
|
||||
|
||||
export const privateCloudProviders = [
|
||||
vmware, qingcloud_private, huaweicloud_private, ctyun_private,
|
||||
openstack, zstack, nutanix, fc, scp, apsara_stack, smartx, proxmox
|
||||
vmware,
|
||||
qingcloud_private,
|
||||
huaweicloud_private,
|
||||
ctyun_private,
|
||||
openstack,
|
||||
zstack,
|
||||
nutanix,
|
||||
fc,
|
||||
scp,
|
||||
apsara_stack,
|
||||
smartx,
|
||||
proxmox
|
||||
]
|
||||
|
||||
export const ACCOUNT_PROVIDER_ATTRS_MAP = {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import { GenericCreateUpdatePage } from '@/layout/components'
|
||||
import { STORAGE_TYPE_META_MAP } from '@/views/sessions/const'
|
||||
import { UploadSecret } from '@/components/Form/FormFields'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
|
||||
export default {
|
||||
name: 'ReplayStorageUpdate',
|
||||
@@ -51,19 +51,19 @@ export default {
|
||||
fields: storageTypeMeta.meta,
|
||||
fieldsMeta: {
|
||||
SFTP_PASSWORD: {
|
||||
hidden: (formValue) => formValue.STP_SECRET_TYPE !== 'password'
|
||||
hidden: formValue => formValue.STP_SECRET_TYPE !== 'password'
|
||||
},
|
||||
STP_PRIVATE_KEY: {
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.STP_SECRET_TYPE !== 'ssh_key'
|
||||
hidden: formValue => formValue.STP_SECRET_TYPE !== 'ssh_key'
|
||||
},
|
||||
STP_PASSPHRASE: {
|
||||
hidden: (formValue) => formValue.STP_SECRET_TYPE !== 'ssh_key'
|
||||
hidden: formValue => formValue.STP_SECRET_TYPE !== 'ssh_key'
|
||||
}
|
||||
}
|
||||
},
|
||||
is_default: {
|
||||
hidden: (formValue) => formValue.type === 'sftp'
|
||||
hidden: formValue => formValue.type === 'sftp'
|
||||
},
|
||||
comment: {
|
||||
component: 'el-input',
|
||||
@@ -102,6 +102,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@@ -8577,6 +8577,11 @@ js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.7.0, js-yaml@^3.9.
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
jsbn@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmmirror.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
|
||||
integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==
|
||||
|
||||
jsbn@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmmirror.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
@@ -12849,6 +12854,13 @@ slice-ansi@^4.0.0:
|
||||
astral-regex "^2.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
|
||||
sm-crypto@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmmirror.com/sm-crypto/-/sm-crypto-0.4.0.tgz#13917f5b41e8428d764e9e6934840fdede6a4022"
|
||||
integrity sha512-OexH2V1EqmhXuOIPGoCl55OjMF0wwPUM/zhUjT0Q6vHBeopSRvTNRy76/1eRoFs3VBKt39hdFnxwpFmooHYa2A==
|
||||
dependencies:
|
||||
jsbn "^1.1.0"
|
||||
|
||||
smart-buffer@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
|
||||
|
||||
Reference in New Issue
Block a user