新增凭证管理功能&字段储存

This commit is contained in:
chaoszhu 2024-07-21 02:25:30 +08:00
parent 5c3818dd73
commit 5b2b776155
21 changed files with 443 additions and 700 deletions

View File

@ -1,30 +1,45 @@
const { readHostList, writeHostList } = require('../utils')
const { readHostList, writeHostList, RSADecryptSync, AESEncryptSync, AESDecryptSync } = require('../utils')
async function getHostList({ res }) {
// console.log('get-host-list')
const data = await readHostList()
let data = await readHostList()
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
data = data.map((item) => {
const isConfig = Boolean(item.username && item.port && (item[item.authType]))
return {
...item,
isConfig,
password: '',
privateKey: ''
}
})
res.success({ data })
}
async function saveHost({
async function addHost({
res, request
}) {
let {
body: {
name, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
port, username, authType, password, privateKey, command
port, username, authType, password, privateKey, credential, command, tempKey
}
} = request
// console.log(request)
if (!newHost || !name) return res.fail({ msg: 'missing params: name or host' })
let hostList = await readHostList()
if (hostList?.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
if (!Array.isArray(hostList)) hostList = []
hostList.push({
host: newHost, name, index, expired, expiredNotify, group, consoleUrl, remark,
port, username, authType, password, privateKey, command
})
let record = {
name, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
port, username, authType, password, privateKey, credential, command
}
const clearTempKey = await RSADecryptSync(tempKey)
console.log('clearTempKey:', clearTempKey)
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
// console.log(`${ authType }原密文: `, clearSSHKey)
record[authType] = await AESEncryptSync(clearSSHKey)
console.log(`${ authType }__commonKey加密存储: `, record[authType])
hostList.push(record)
await writeHostList(hostList)
res.success()
}
@ -35,17 +50,31 @@ async function updateHost({
let {
body: {
host: newHost, name: newName, index, oldHost, expired, expiredNotify, group, consoleUrl, remark,
port, username, authType, password, privateKey, command
port, username, authType, password, privateKey, credential, command, tempKey
}
} = request
if (!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
let hostList = await readHostList()
if (!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `原实例[${ oldHost }]不存在,请尝试新增实例` })
let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
hostList.splice(targetIdx, 1, {
let record = {
name: newName, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
port, username, authType, password, privateKey, command
})
port, username, authType, password, privateKey, credential, command
}
if (!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `原实例[${ oldHost }]不存在,请尝试新增实例` })
let idx = hostList.findIndex(({ host }) => host === oldHost)
const oldRecord = hostList[idx]
// 如果存在原认证方式则保存下来
if (!record[authType] && oldRecord[authType]) {
record[authType] = oldRecord[authType]
} else {
const clearTempKey = await RSADecryptSync(tempKey)
console.log('clearTempKey:', clearTempKey)
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
// console.log(`${ authType }原密文: `, clearSSHKey)
record[authType] = await AESEncryptSync(clearSSHKey)
console.log(`${ authType }__commonKey加密存储: `, record[authType])
}
hostList.splice(idx, 1, record)
writeHostList(hostList)
res.success()
}
@ -89,7 +118,7 @@ async function removeHost({
module.exports = {
getHostList,
saveHost,
addHost,
updateHost,
removeHost
// updateHostSort

View File

@ -1,52 +1,81 @@
const { readSSHRecord, writeSSHRecord, AESEncryptSync } = require('../utils')
const { readSSHRecord, writeSSHRecord, readHostList, writeHostList, RSADecryptSync, AESEncryptSync, AESDecryptSync } = require('../utils')
async function getSSHList({ res }) {
// console.log('get-host-list')
let data = await readSSHRecord()
data = data?.map(item => {
const { host, port, username, _id } = item
return { host, port, username, _id }
const { name, authType, _id: id, date } = item
return { id, name, authType, privateKey: '', password: '', date }
}) || []
data.sort((a, b) => b.date - a.date)
res.success({ data })
}
const updateSSH = async ({ res, request }) => {
let { body: { host, port, username, type, password, privateKey, randomKey, command } } = request
let record = { host, port, username, type, password, privateKey, randomKey, command }
if(!host || !port || !username || !type || !randomKey) return res.fail({ data: false, msg: '参数错误' })
// 再做一次对称加密(方便ssh连接时解密)
record.randomKey = await AESEncryptSync(randomKey)
const addSSH = async ({ res, request }) => {
let { body: { name, authType, password, privateKey, tempKey } } = request
let record = { name, authType, password, privateKey }
if(!name || !record[authType]) return res.fail({ data: false, msg: '参数错误' })
let sshRecord = await readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1)
sshRecord.push(record)
else
sshRecord.splice(idx, 1, record)
if (sshRecord.some(item => item.name === name)) return res.fail({ data: false, msg: '已存在同名凭证' })
const clearTempKey = await RSADecryptSync(tempKey)
console.log('clearTempKey:', clearTempKey)
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
// console.log(`${ authType }原密文: `, clearSSHKey)
record[authType] = await AESEncryptSync(clearSSHKey)
console.log(`${ authType }__commonKey加密存储: `, record[authType])
sshRecord.push({ ...record, date: Date.now() })
await writeSSHRecord(sshRecord)
consola.info('新增凭证:', host)
consola.info('新增凭证:', name)
res.success({ data: '保存成功' })
}
const updateSSH = async ({ res, request }) => {
let { body: { id, name, authType, password, privateKey, date, tempKey } } = request
let record = { name, authType, password, privateKey, date }
if(!id || !name) return res.fail({ data: false, msg: '请输入凭据名称' })
let sshRecord = await readSSHRecord()
let idx = sshRecord.findIndex(item => item._id === id)
if (sshRecord.some(item => item.name === name && item.date !== date)) return res.fail({ data: false, msg: '已存在同名凭证' })
if(idx === -1) res.fail({ data: false, msg: '请输入凭据名称' })
const oldRecord = sshRecord[idx]
// 判断原记录是否存在当前更新记录的认证方式
if (!oldRecord[authType] && !record[authType]) return res.fail({ data: false, msg: `请输入${ authType === 'password' ? '密码' : '密钥' }` })
if (!record[authType] && oldRecord[authType]) {
record[authType] = oldRecord[authType]
} else {
const clearTempKey = await RSADecryptSync(tempKey)
console.log('clearTempKey:', clearTempKey)
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
// console.log(`${ authType }原密文: `, clearSSHKey)
record[authType] = await AESEncryptSync(clearSSHKey)
console.log(`${ authType }__commonKey加密存储: `, record[authType])
}
sshRecord.splice(idx, 1, record)
await writeSSHRecord(sshRecord)
consola.info('修改凭证:', name)
res.success({ data: '保存成功' })
}
const removeSSH = async ({ res, request }) => {
let { body: { host } } = request
let { params: { id } } = request
let sshRecord = await readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host)
let idx = sshRecord.findIndex(item => item._id === id)
if(idx === -1) return res.fail({ msg: '凭证不存在' })
sshRecord.splice(idx, 1)
consola.info('移除凭证:', host)
// 将删除的凭证id从host中删除
let hostList = await readHostList()
hostList = hostList.map(item => {
if (item.credential === id) item.credential = ''
return item
})
await writeHostList(hostList)
consola.info('移除凭证:', id)
await writeSSHRecord(sshRecord)
res.success({ data: '移除成功' })
}
const existSSH = async ({ res, request }) => {
let { body: { host } } = request
let sshRecord = await readSSHRecord()
let isExist = sshRecord?.some(item => item.host === host)
consola.info('查询凭证:', host)
if(!isExist) return res.success({ data: false }) // host不存在
res.success({ data: true }) // 存在
}
const getCommand = async ({ res, request }) => {
let { host } = request.query
if(!host) return res.fail({ data: false, msg: '参数错误' })
@ -61,8 +90,8 @@ const getCommand = async ({ res, request }) => {
module.exports = {
getSSHList,
addSSH,
updateSSH,
removeSSH,
existSSH,
getCommand
}

View File

@ -1,5 +1,5 @@
const { getSSHList, updateSSH, removeSSH, existSSH, getCommand } = require('../controller/ssh')
const { getHostList, saveHost, updateHost, removeHost } = require('../controller/host')
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand } = require('../controller/ssh')
const { getHostList, addHost, updateHost, removeHost } = require('../controller/host')
const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controller/user')
const { getSupportEmailList, getUserEmailList, updateUserEmailList, removeUserEmail, pushEmail, getNotifyList, updateNotifyList } = require('../controller/notify')
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
@ -10,21 +10,21 @@ const ssh = [
path: '/get-ssh-list',
controller: getSSHList
},
{
method: 'post',
path: '/add-ssh',
controller: addSSH
},
{
method: 'post',
path: '/update-ssh',
controller: updateSSH
},
{
method: 'post',
path: '/remove-ssh',
method: 'delete',
path: '/remove-ssh/:id',
controller: removeSSH
},
{
method: 'post',
path: '/exist-ssh',
controller: existSSH
},
{
method: 'get',
path: '/command',
@ -40,7 +40,7 @@ const host = [
{
method: 'post',
path: '/host-save',
controller: saveHost
controller: addHost
},
{
method: 'put',

View File

@ -52,6 +52,7 @@ module.exports = (httpServer) => {
const sshRecord = await readSSHRecord()
let loginInfo = sshRecord.find(item => item.host === ip)
if (!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
// :TODO: 不用tempKey加密了统一使用commonKey加密
let { type, host, port, username, randomKey } = loginInfo
try {
// 解密放到try里面防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】

View File

@ -10,22 +10,25 @@ export default {
getSSHList(params = {}) {
return axios({ url: '/get-ssh-list', method: 'get', params })
},
addSSH(data) {
return axios({ url: '/add-ssh', method: 'post', data })
},
updateSSH(data) {
return axios({ url: '/update-ssh', method: 'post', data })
},
removeSSH(host) {
return axios({ url: '/remove-ssh', method: 'post', data: { host } })
},
existSSH(host) {
return axios({ url: '/exist-ssh', method: 'post', data: { host } })
removeSSH(id) {
return axios({ url: `/remove-ssh/${ id }`, method: 'delete' })
},
// existSSH(host) {
// return axios({ url: '/exist-ssh', method: 'post', data: { host } })
// },
getCommand(host) {
return axios({ url: '/command', method: 'get', params: { host } })
},
getHostList() {
return axios({ url: '/host-list', method: 'get' })
},
saveHost(data) {
addHost(data) {
return axios({ url: '/host-save', method: 'post', data })
},
updateHost(data) {

View File

@ -52,11 +52,11 @@ let menuList = reactive([
icon: markRaw(ScaleToOriginal),
index: '/terminal'
},
// {
// name: '',
// icon: markRaw(Key),
// index: '/credentials'
// },
{
name: '凭据管理',
icon: markRaw(Key),
index: '/credentials'
},
{
name: '分组管理',
icon: markRaw(FolderOpened),

View File

@ -49,6 +49,11 @@ const useStore = defineStore({
// console.log('groupList:', groupList)
this.$patch({ groupList })
},
async getSSHList() {
const { data: sshList } = await $api.getSSHList()
// console.log('sshList:', sshList)
this.$patch({ sshList })
},
getHostPing() {
setTimeout(() => {
this.hostList.forEach((item) => {

View File

@ -14,21 +14,11 @@ export const randomStr = (e) =>{
// rsa公钥加密
export const RSAEncrypt = (text) => {
const publicKey = localStorage.getItem('publicKey')
if(!publicKey) return -1 // 公钥不存在
const RSAPubEncrypt = new JSRsaEncrypt() // 生成实例
RSAPubEncrypt.setPublicKey(publicKey) // 配置公钥(不是将公钥实例化时传入!!!)
const ciphertext = RSAPubEncrypt.encrypt(text) // 加密
// console.log('rsa加密', ciphertext)
return ciphertext
}
// rsa公钥解密
export const RSADecrypt = (text) => {
const publicKey = localStorage.getItem('publicKey')
if(!publicKey) return -1 // 公钥不存在
if (!publicKey) return -1 // 公钥不存在
const RSAPubEncrypt = new JSRsaEncrypt() // 生成实例
RSAPubEncrypt.setPublicKey(publicKey) // 配置公钥(不是将公钥实例化时传入!!!)
const ciphertext = RSAPubEncrypt.encrypt(text) // 加密
// console.log('rsa公钥加密', ciphertext)
return ciphertext
}

View File

@ -1,19 +1,204 @@
<template>
<div class="">
credentials
<div class="credentials_container">
<div class="header">
<el-button type="primary" @click="addCredentials">添加凭证</el-button>
</div>
<el-table v-loading="loading" :data="sshList">
<el-table-column prop="name" label="名称" />
<el-table-column prop="authType" label="类型">
<template #default="{ row }">
{{ row.authType === 'privateKey' ? '密钥' : '密码' }}
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="{ row }">
<el-button type="primary" @click="handleChange(row)">修改</el-button>
<el-button v-show="row.id !== 'default'" type="danger" @click="removeSSH(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog
v-model="sshFormVisible"
width="600px"
top="150px"
:title="isModify ? '修改凭证' : '添加凭证'"
:close-on-click-modal="false"
@close="clearFormInfo"
>
<el-form
ref="updateFormRef"
:model="sshForm"
:rules="rules"
:hide-required-asterisk="true"
label-suffix=""
label-width="100px"
:show-message="false"
>
<el-form-item label="凭证名称" prop="name">
<el-input
v-model.trim="sshForm.name"
clearable
placeholder=""
autocomplete="off"
/>
</el-form-item>
<el-form-item label="认证方式" prop="type">
<el-radio v-model.trim="sshForm.authType" value="privateKey">密钥</el-radio>
<el-radio v-model.trim="sshForm.authType" value="password">密码</el-radio>
</el-form-item>
<el-form-item v-if="sshForm.authType === 'privateKey'" prop="privateKey" label="密钥">
<el-button type="primary" size="small" @click="handleClickUploadBtn">
本地私钥...
</el-button>
<input
ref="privateKeyRef"
type="file"
name="privateKey"
style="display: none;"
@change="handleSelectPrivateKeyFile"
>
<el-input
v-model.trim="sshForm.privateKey"
type="textarea"
:rows="5"
clearable
autocomplete="off"
style="margin-top: 5px;"
placeholder="-----BEGIN RSA PRIVATE KEY-----"
/>
</el-form-item>
<el-form-item v-if="sshForm.authType === 'password'" prop="password" label="密码">
<el-input
v-model.trim="sshForm.password"
type="text"
placeholder=""
autocomplete="off"
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<span>
<el-button @click="sshFormVisible = false">关闭</el-button>
<el-button type="primary" @click="updateForm">{{ isModify ? '修改' : '新增' }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
<script setup>
import { ref, reactive, computed, nextTick, getCurrentInstance } from 'vue'
import { randomStr, AESEncrypt, RSAEncrypt } from '@utils/index.js'
const { proxy: { $api, $message, $messageBox, $store } } = getCurrentInstance()
const loading = ref(false)
const sshFormVisible = ref(false)
let isModify = ref(false)
const sshForm = reactive({
name: '',
data() {
return {
}
authType: 'privateKey',
privateKey: '',
password: ''
})
const rules = computed(() => {
return {
name: { required: true, message: '需输入凭证名称', trigger: 'change' },
password: [{ required: !isModify.value && sshForm.authType === 'password', trigger: 'change' },],
privateKey: [{ required: !isModify.value && sshForm.authType === 'privateKey', trigger: 'change' },]
}
})
const updateFormRef = ref(null)
const privateKeyRef = ref(null)
let sshList = computed(() => $store.sshList)
let addCredentials = () => {
sshForm.id = null
sshFormVisible.value = true
isModify.value = false
}
const handleChange = (row) => {
Object.assign(sshForm, { ...row })
sshFormVisible.value = true
isModify.value = true
}
const updateForm = () => {
updateFormRef.value.validate()
.then(async () => {
let formData = { ...sshForm }
let tempKey = randomStr(16)
//
if (formData.password) formData.password = AESEncrypt(formData.password, tempKey)
if (formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, tempKey)
formData.tempKey = RSAEncrypt(tempKey)
//
if (isModify.value) {
await $api.updateSSH(formData)
} else {
await $api.addSSH(formData)
}
sshFormVisible.value = false
await $store.getSSHList()
$message.success('success')
})
}
const clearFormInfo = () => {
nextTick(() => updateFormRef.value.resetFields())
}
const removeSSH = ({ id, name }) => {
$messageBox.confirm(`确认删除该凭证:${ name }`, 'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await $api.removeSSH(id) // credential
await $store.getSSHList()
await $store.getHostList() // isConfig
$message.success('success')
})
}
const handleClickUploadBtn = () => {
privateKeyRef.value.click()
}
const handleSelectPrivateKeyFile = (event) => {
let file = event.target.files[0]
let reader = new FileReader()
reader.onload = (e) => {
sshForm.privateKey = e.target.result
privateKeyRef.value.value = ''
}
reader.readAsText(file)
}
</script>
<style lang="scss" scoped>
.credentials_container {
padding: 20px;
.header {
padding: 15px;
display: flex;
align-items: center;
justify-content: end;
}
}
</style>
.host_count {
display: block;
width: 100px;
text-align: center;
font-size: 15px;
color: #87cf63;
cursor: pointer;
}
</style>

View File

@ -46,6 +46,7 @@ onBeforeMount(async () => {
.router_box {
min-height: calc(100vh - 60px - 20px);
background-color: #fff;
border-radius: 6px;
margin: 10px;
}
}

View File

@ -86,7 +86,6 @@ const { proxy: { $store, $api, $message, $router } } = getCurrentInstance()
const loginFormRefs = ref(null)
const isSession = ref(true)
const visible = ref(true)
const notKey = ref(false)
const loading = ref(false)
const loginForm = reactive({

View File

@ -164,12 +164,10 @@
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue'
import { computed, getCurrentInstance } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import SSHForm from './ssh-form.vue'
const { proxy: { $api, $tools } } = getCurrentInstance()
const { proxy: { $api, $router, $tools } } = getCurrentInstance()
const props = defineProps({
hostInfo: {
@ -226,22 +224,16 @@ const handleToConsole = () => {
}
const handleSSH = async () => {
let { data } = host.value
}
const handleRemoveSSH = async () => {
ElMessageBox.confirm('确认删除SSH凭证', 'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
let { data } = await $api.removeSSH(host.value)
if(!hostInfo.value?.isConfig) {
ElMessage({
message: data,
type: 'success',
message: '请先配置SSH连接信息',
type: 'warning',
center: true
})
})
handleUpdate()
return
}
$router.push({ path: '/terminal', query: { host: host.value } })
}
const handleRemoveHost = async () => {

View File

@ -2,6 +2,9 @@
<el-dialog
v-model="visible"
width="600px"
top="45px"
modal-class="host_form_dialog"
append-to-body
:title="title"
:close-on-click-modal="false"
@open="setDefaultData"
@ -46,8 +49,8 @@
<div key="instance_info" class="instance_info">
<el-form-item
key="host"
class="form_item"
label="实例"
class="form_item_host"
label="主机"
prop="host"
>
<el-input
@ -59,7 +62,7 @@
</el-form-item>
<el-form-item
key="port"
class="form_item"
class="form_item_port"
label="端口"
prop="port"
>
@ -86,6 +89,7 @@
<el-form-item key="authType" label="认证方式" prop="authType">
<el-radio v-model.trim="hostForm.authType" value="privateKey">密钥</el-radio>
<el-radio v-model.trim="hostForm.authType" value="password">密码</el-radio>
<el-radio v-model.trim="hostForm.authType" value="credential">凭据</el-radio>
</el-form-item>
<el-form-item
v-if="hostForm.authType === 'privateKey'"
@ -131,6 +135,29 @@
show-password
/>
</el-form-item>
<el-form-item
v-if="hostForm.authType === 'credential'"
key="credential"
prop="credential"
label="凭据"
>
<el-select v-model="hostForm.credential" class="credential_select" placeholder="">
<template #empty>
<div class="empty_credential">
<span>无凭据数据,</span>
<el-button type="primary" link @click="toCredentials">
去添加
</el-button>
</div>
</template>
<el-option
v-for="item in sshList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item key="command" prop="command" label="执行指令">
<el-input
v-model="hostForm.command"
@ -204,8 +231,9 @@
<script setup>
import { ref, reactive, computed, getCurrentInstance, nextTick } from 'vue'
import { RSAEncrypt, AESEncrypt, randomStr } from '@utils/index.js'
const { proxy: { $api, $message, $store } } = getCurrentInstance()
const { proxy: { $api, $router, $message, $store } } = getCurrentInstance()
const props = defineProps({
show: {
@ -229,6 +257,7 @@ const resetForm = () => ({
authType: 'privateKey',
password: '',
privateKey: '',
credential: '', // credentials -> _id
index: 0,
expired: null,
expiredNotify: false,
@ -265,7 +294,8 @@ const visible = computed({
const title = computed(() => props.defaultData ? '修改实例' : '新增实例')
let groupList = computed(() => $store.groupList || [])
let groupList = computed(() => $store.groupList)
let sshList = computed(() => $store.sshList)
const handleClosed = () => {
// console.log('handleClosed')
@ -312,14 +342,25 @@ const userSearch = (keyword, cb) => {
cb(res)
}
const toCredentials = () => {
visible.value = false
$router.push({ path: '/credentials' })
}
const handleSave = () => {
formRef.value.validate()
.then(async () => {
let tempKey = randomStr(16)
let formData = { ...hostForm }
//
if (formData.password) formData.password = AESEncrypt(formData.password, tempKey)
if (formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, tempKey)
formData.tempKey = RSAEncrypt(tempKey)
if (props.defaultData) {
let { msg } = await $api.updateHost(Object.assign({}, hostForm, { oldHost: oldHost.value }))
let { msg } = await $api.updateHost(Object.assign({}, formData, { oldHost: oldHost.value }))
$message({ type: 'success', center: true, message: msg })
} else {
let { msg } = await $api.saveHost(hostForm)
let { msg } = await $api.addHost(formData)
$message({ type: 'success', center: true, message: msg })
}
visible.value = false
@ -332,9 +373,18 @@ const handleSave = () => {
<style lang="scss" scoped>
.instance_info {
display: flex;
.form_item {
width: 50%;
justify-content: space-between;
.form_item_host {
width: 60%;
}
.form_item_port {
flex: 1;
}
}
.empty_credential {
display: flex;
align-items: center;
justify-content: center;
}
.dialog-footer {
display: flex;

View File

@ -1,205 +0,0 @@
<template>
<el-dialog
v-model="visible"
title="SSH连接"
:close-on-click-modal="false"
@closed="clearFormInfo"
>
<el-form
ref="formRef"
:model="sshForm"
:rules="rules"
:hide-required-asterisk="true"
label-suffix=""
label-width="90px"
>
<el-form-item label="实例" prop="host">
<el-input
v-model.trim="sshForm.host"
disabled
clearable
autocomplete="off"
/>
</el-form-item>
<el-form-item label="端口" prop="port">
<el-input v-model.trim="sshForm.port" clearable autocomplete="off" />
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-autocomplete
v-model.trim="sshForm.username"
:fetch-suggestions="userSearch"
style="width: 100%;"
clearable
>
<template #default="{item}">
<div class="value">{{ item.value }}</div>
</template>
</el-autocomplete>
</el-form-item>
<el-form-item label="认证方式" prop="type">
<el-radio v-model.trim="sshForm.type" value="privateKey">密钥</el-radio>
<el-radio v-model.trim="sshForm.type" value="password">密码</el-radio>
</el-form-item>
<el-form-item v-if="sshForm.type === 'password'" prop="password" label="密码">
<el-input
v-model.trim="sshForm.password"
type="password"
placeholder="Please input password"
autocomplete="off"
clearable
show-password
/>
</el-form-item>
<el-form-item v-if="sshForm.type === 'privateKey'" prop="privateKey" label="密钥">
<el-button type="primary" size="small" @click="handleClickUploadBtn">
本地私钥...
</el-button>
<input
ref="privateKeyRef"
type="file"
name="privateKey"
style="display: none;"
@change="handleSelectPrivateKeyFile"
>
<el-input
v-model.trim="sshForm.privateKey"
type="textarea"
:rows="5"
clearable
autocomplete="off"
style="margin-top: 5px;"
placeholder="-----BEGIN RSA PRIVATE KEY-----"
/>
</el-form-item>
<el-form-item prop="command" label="执行指令">
<el-input
v-model="sshForm.command"
type="textarea"
:rows="5"
clearable
autocomplete="off"
placeholder="连接服务器后自动执行的指令(例如: sudo -i)"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleSaveSSH">保存</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, watch, getCurrentInstance, nextTick } from 'vue'
import { ElNotification } from 'element-plus'
import { randomStr, AESEncrypt, RSAEncrypt } from '@utils/index.js'
const props = defineProps({
show: {
required: true,
type: Boolean
},
tempHost: {
required: true,
type: String
},
name: {
required: true,
type: String
}
})
const emit = defineEmits(['update:show',])
const formRef = ref(null)
const privateKeyRef = ref(null)
const sshForm = reactive({
host: '',
port: 22,
username: '',
type: 'privateKey',
password: '',
privateKey: '',
command: ''
})
const defaultUsers = [
{ value: 'root' },
{ value: 'ubuntu' },
]
const rules = reactive({
host: { required: true, message: '需输入主机', trigger: 'change' },
port: { required: true, message: '需输入端口', trigger: 'change' },
username: { required: true, message: '需输入用户名', trigger: 'change' },
type: { required: true },
password: { required: true, message: '需输入密码', trigger: 'change' },
privateKey: { required: true, message: '需输入密钥', trigger: 'change' },
command: { required: false }
})
const { proxy: { $api } } = getCurrentInstance()
const visible = computed({
get() {
return props.show
},
set(newVal) {
emit('update:show', newVal)
}
})
watch(() => props.tempHost, (newVal) => {
sshForm.host = newVal
})
const handleClickUploadBtn = () => {
privateKeyRef.value.click()
}
const handleSelectPrivateKeyFile = (event) => {
let file = event.target.files[0]
let reader = new FileReader()
reader.onload = (e) => {
sshForm.privateKey = e.target.result
privateKeyRef.value.value = ''
}
reader.readAsText(file)
}
const handleSaveSSH = () => {
formRef.value.validate()
.then(async () => {
let randomKey = randomStr(16)
let formData = JSON.parse(JSON.stringify(sshForm))
//
if (formData.password) formData.password = AESEncrypt(formData.password, randomKey)
if (formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, randomKey)
formData.randomKey = RSAEncrypt(randomKey)
await $api.updateSSH(formData)
ElNotification({
title: '保存成功',
message: '下次点击 [Web SSH] 可直接登录终端\n如无法登录请 [移除凭证] 后重新添加',
type: 'success'
})
visible.value = false
})
}
const userSearch = (keyword, cb) => {
let res = keyword
? defaultUsers.filter((item) => item.value.includes(keyword))
: defaultUsers
cb(res)
}
const clearFormInfo = () => {
nextTick(() => formRef.value.resetFields())
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -106,7 +106,7 @@ const unwatchHost = watch(hostList, () => {
const connectIo = () => {
if (socket.value) socket.value.close()
unwatchHost()
if (typeof(unwatchHost) === 'function') unwatchHost()
let socketInstance = io($serviceURI, {
path: '/clients',
forceNew: true,

View File

@ -1,239 +0,0 @@
<template>
<el-form
ref="groupFormRef"
:model="groupForm"
:rules="rules"
:inline="true"
:hide-required-asterisk="true"
label-suffix=""
>
<el-form-item label="" prop="name" style="width: 200px;">
<el-input
v-model.trim="groupForm.name"
clearable
placeholder="分组名称"
autocomplete="off"
@keyup.enter="addGroup"
/>
</el-form-item>
<el-form-item label="" prop="index" style="width: 200px;">
<el-input
v-model.number="groupForm.index"
clearable
placeholder="序号(数字, 用于分组排序)"
autocomplete="off"
@keyup.enter="addGroup"
/>
</el-form-item>
<el-form-item label="">
<el-button type="primary" @click="addGroup">
添加
</el-button>
</el-form-item>
</el-form>
<!-- 提示 -->
<el-alert type="success" :closable="false">
<template #title>
<span style="letter-spacing: 2px;">
Tips: 已添加实例数量 <u>{{ hostGroupInfo.total }}</u>
<span v-show="hostGroupInfo.notGroupCount">, <u>{{ hostGroupInfo.notGroupCount }}</u> 台实例尚未分组</span>
</span>
</template>
</el-alert><br>
<el-alert type="success" :closable="false">
<template #title>
<span style="letter-spacing: 2px;"> Tips: 删除分组会将分组内所有实例移至默认分组 </span>
</template>
</el-alert>
<el-table v-loading="loading" :data="list">
<el-table-column prop="index" label="序号" />
<el-table-column prop="id" label="ID" />
<el-table-column prop="name" label="分组名称" />
<el-table-column label="关联实例数量">
<template #default="{ row }">
<el-popover
v-if="row.hosts.list.length !== 0"
placement="right"
:width="350"
trigger="hover"
>
<template #reference>
<u class="host-count">{{ row.hosts.count }}</u>
</template>
<ul>
<li v-for="item in row.hosts.list" :key="item.host">
<span>{{ item.host }}</span>
-
<span>{{ item.name }}</span>
</li>
</ul>
</el-popover>
<u v-else class="host-count">0</u>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="{ row }">
<el-button type="primary" @click="handleChange(row)">修改</el-button>
<el-button v-show="row.id !== 'default'" type="danger" @click="deleteGroup(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog
v-model="visible"
width="400px"
title="修改分组"
:close-on-click-modal="false"
>
<el-form
ref="updateFormRef"
:model="updateForm"
:rules="rules"
:hide-required-asterisk="true"
label-suffix=""
label-width="100px"
>
<el-form-item label="分组名称" prop="name">
<el-input
v-model.trim="updateForm.name"
clearable
placeholder="分组名称"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="分组序号" prop="index">
<el-input
v-model.number="updateForm.index"
clearable
placeholder="分组序号"
autocomplete="off"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">关闭</el-button>
<el-button type="primary" @click="updateGroup">修改</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
const { proxy: { $api, $message, $messageBox, $store } } = getCurrentInstance()
const loading = ref(false)
const visible = ref(false)
const groupList = ref([])
const groupForm = reactive({
name: '',
index: ''
})
const updateForm = reactive({
name: '',
index: ''
})
const rules = reactive({
name: { required: true, message: '需输入分组名称', trigger: 'change' },
index: { required: true, type: 'number', message: '需输入数字', trigger: 'change' }
})
const groupFormRef = ref(null)
const updateFormRef = ref(null)
const hostGroupInfo = computed(() => {
const total = $store.hostList.length
const notGroupCount = $store.hostList.reduce((prev, next) => {
if (!next.group) prev++
return prev
}, 0)
return { total, notGroupCount }
})
const list = computed(() => {
return groupList.value.map(item => {
const hosts = $store.hostList.reduce((prev, next) => {
if (next.group === item.id) {
prev.count++
prev.list.push(next)
}
return prev
}, { count: 0, list: [] })
return { ...item, hosts }
})
})
const getGroupList = () => {
loading.value = true
$api.getGroupList()
.then(({ data }) => {
groupList.value = data
groupForm.index = data.length
})
.finally(() => loading.value = false)
}
const addGroup = () => {
groupFormRef.value.validate()
.then(() => {
const { name, index } = groupForm
$api.addGroup({ name, index })
.then(() => {
$message.success('success')
groupForm.name = ''
groupForm.index = ''
getGroupList()
})
})
}
const handleChange = ({ id, name, index }) => {
updateForm.id = id
updateForm.name = name
updateForm.index = index
visible.value = true
}
const updateGroup = () => {
updateFormRef.value.validate()
.then(() => {
const { id, name, index } = updateForm
$api.updateGroup(id, { name, index })
.then(() => {
$message.success('success')
visible.value = false
getGroupList()
})
})
}
const deleteGroup = ({ id, name }) => {
$messageBox.confirm(`确认删除分组:${ name }`, 'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await $api.deleteGroup(id)
await $store.getHostList()
$message.success('success')
getGroupList()
})
}
onMounted(() => {
getGroupList()
})
</script>
<style lang="scss" scoped>
.host-count {
display: block;
width: 100px;
text-align: center;
font-size: 15px;
color: #87cf63;
cursor: pointer;
}
</style>

View File

@ -1,98 +0,0 @@
<template>
<transition-group
name="list"
mode="out-in"
tag="ul"
class="host-list"
>
<li
v-for="(item, index) in list"
:key="item.host"
:draggable="true"
class="host-item"
@dragenter="dragenter($event, index)"
@dragover="dragover($event)"
@dragstart="dragstart(index)"
>
<span>{{ item.host }}</span>
---
<span>{{ item.name }}</span>
</li>
</transition-group>
<div style="display: flex; justify-content: center; margin-top: 25px">
<el-button type="primary" @click="handleUpdateSort">
保存
</el-button>
</div>
</template>
<script setup>
import { ref, onMounted, getCurrentInstance } from 'vue'
const emit = defineEmits(['update-list',])
const { proxy: { $api, $message, $store } } = getCurrentInstance()
const targetIndex = ref(0)
const list = ref([])
const dragstart = (index) => {
targetIndex.value = index
}
const dragenter = (e, curIndex) => {
e.preventDefault()
if (targetIndex.value !== curIndex) {
let target = list.value.splice(targetIndex.value, 1)[0]
list.value.splice(curIndex, 0, target)
targetIndex.value = curIndex
}
}
const dragover = (e) => {
e.preventDefault()
}
const handleUpdateSort = () => {
$api.updateHostSort({ list: list.value })
.then(({ msg }) => {
$message({ type: 'success', center: true, message: msg })
$store.sortHostList(list.value)
emit('update-list', list.value) //
})
}
onMounted(() => {
list.value = $store.hostList.map(({ name, host }) => ({ name, host }))
})
</script>
<style lang="scss" scoped>
.drag-move {
transition: transform .3s;
}
.host-list {
padding-top: 10px;
padding-right: 50px;
}
.host-item {
transition: all .3s;
box-shadow: var(--el-box-shadow-lighter);
cursor: move;
font-size: 12px;
color: #595959;
padding: 0 20px;
margin: 0 auto;
border-radius: 4px;
color: #000;
margin-bottom: 6px;
height: 35px;
line-height: 35px;
&:hover {
box-shadow: var(--el-box-shadow);
}
}
.dialog-footer {
display: flex;
justify-content: center;
}
</style>

View File

@ -2,7 +2,7 @@
<div class="setting_container">
<el-tabs tab-position="top">
<el-tab-pane label="修改密码" lazy>
<Password />
<User />
</el-tab-pane>
<!-- <el-tab-pane label="分组管理">
<Group />
@ -24,31 +24,11 @@
</template>
<script setup>
import { computed } from 'vue'
import NotifyList from './components/notify-list.vue'
import EmailList from './components/email-list.vue'
// import Sort from './components/sort.vue'
import Record from './components/record.vue'
// import Group from './components/group.vue'
import Password from './components/password.vue'
import User from './components/user.vue'
// const props = defineProps({
// show: {
// required: true,
// type: Boolean
// }
// })
const emit = defineEmits(['update:show', 'update-list',])
// const visible = computed({
// get: () => props.show,
// set: (newVal) => emit('update:show', newVal)
// })
const emitUpdateList = () => {
emit('update-list')
}
</script>
<style lang="scss" scoped>

View File

@ -51,7 +51,7 @@
</template>
<script setup>
import { ref, reactive, computed, onBeforeMount, getCurrentInstance } from 'vue'
import { ref, reactive, computed, onBeforeMount,defineProps, getCurrentInstance } from 'vue'
import TerminalTab from './terminal-tab.vue'
import InfoSide from './info-side.vue'
import SftpFooter from './sftp-footer.vue'
@ -59,6 +59,13 @@ import InputCommand from '@/components/input-command/index.vue'
const { proxy: { $store, $router, $route, $nextTick } } = getCurrentInstance()
const props = defineProps({
ternimalTabs: {
type: Array,
required: true
}
})
const name = ref('')
const host = ref('')
const activeTab = ref('')
@ -71,6 +78,7 @@ const visible = ref(true)
const infoSideRef = ref(null)
const terminalTabRefs = ref([])
const token = computed(() => $store.token)
const ternimalTabs = computed(() => props.ternimalTabs)
const closable = computed(() => terminalTabs.length > 1)

View File

@ -3,7 +3,7 @@
<div v-if="showLinkTips" class="terminal_link_tips">
<h2 class="quick_link_text">快速连接</h2>
<el-table
:data="tabelData"
:data="hostList"
:show-header="false"
>
<el-table-column prop="name" label="name" />
@ -16,7 +16,7 @@
<template #default="{ row }">
<div class="actios_btns">
<el-button
v-if="row.username && row.port"
v-if="row.isConfig"
type="primary"
link
@click="linkTerminal(row)"
@ -27,7 +27,7 @@
v-else
type="success"
link
@click="confSSH(row)"
@click="handleUpdateHost(row)"
>
配置ssh
</el-button>
@ -37,46 +37,59 @@
</el-table>
</div>
<div v-else>
<Terminal />
<Terminal :ternimal-tabs="ternimalTabs" />
</div>
<HostForm
v-model:show="hostFormVisible"
:default-data="updateHostData"
@update-list="handleUpdateList"
@closed="updateHostData = null"
/>
</div>
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue'
import { ref, computed, onActivated, getCurrentInstance } from 'vue'
import Terminal from './components/terminal.vue'
import HostForm from '../server/components/host-form.vue'
const { proxy: { $store } } = getCurrentInstance()
const { proxy: { $store, $message } } = getCurrentInstance()
let showLinkTips = ref(true)
let ternimalTabs = ref([])
const hostFormVisible = ref(false)
const updateHostData = ref(null)
let showLinkTips = computed(() => !Boolean(ternimalTabs.value.length))
let hostList = computed(() => $store.hostList)
let sshList = computed(() => $store.sshList)
let tabelData = computed(() => {
return hostList.value.map(hostConf => {
// console.log(sshList.value)
let target = sshList.value?.find(sshConf => sshConf.host === hostConf.host)
if (target !== -1) {
return { ...hostConf, ...target }
}
return hostConf
})
})
let isAllConfssh = computed(() => {
return tabelData.value?.every(item => item.username && item.port)
return hostList.value?.every(item => item.isConfig)
})
function linkTerminal(row) {
// console.log(row)
ternimalTabs.value.push(row)
showLinkTips.value = false
}
function confSSH(row) {
function handleUpdateHost(row) {
hostFormVisible.value = true
updateHostData.value = { ...row }
}
const handleUpdateList = async () => {
try {
await $store.getHostList()
} catch (err) {
$message.error('获取实例列表失败')
console.error('获取实例列表失败: ', err)
}
}
onActivated(() => {
console.log()
})
</script>
<style lang="scss" scoped>