mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-01-11 01:25:11 +08:00
parent
e4478da423
commit
99023569fc
10 changed files with 418 additions and 248 deletions
|
|
@ -70,42 +70,20 @@ func (b *BaseApi) UpdateSSH(c *gin.Context) {
|
|||
// @Tags SSH
|
||||
// @Summary Generate host SSH secret
|
||||
// @Accept json
|
||||
// @Param request body dto.CreateRootCert true "request"
|
||||
// @Param request body dto.RootCertOperate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/ssh/cert [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 SSH 密钥 ","formatEN":"generate SSH secret"}
|
||||
func (b *BaseApi) CreateRootCert(c *gin.Context) {
|
||||
var req dto.CreateRootCert
|
||||
var req dto.RootCertOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if len(req.PassPhrase) != 0 {
|
||||
passPhrase, err := base64.StdEncoding.DecodeString(req.PassPhrase)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
req.PassPhrase = string(passPhrase)
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
}
|
||||
if len(req.PrivateKey) != 0 {
|
||||
privateKey, err := base64.StdEncoding.DecodeString(req.PrivateKey)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
req.PrivateKey = string(privateKey)
|
||||
}
|
||||
if len(req.PublicKey) != 0 {
|
||||
publicKey, err := base64.StdEncoding.DecodeString(req.PublicKey)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
req.PublicKey = string(publicKey)
|
||||
}
|
||||
|
||||
if err := sshService.CreateRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
|
|
@ -113,6 +91,30 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
|
|||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags SSH
|
||||
// @Summary Update host SSH secret
|
||||
// @Accept json
|
||||
// @Param request body dto.RootCertOperate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/ssh/cert/update [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 SSH 密钥 ","formatEN":"generate SSH secret"}
|
||||
func (b *BaseApi) EditRootCert(c *gin.Context) {
|
||||
var req dto.RootCertOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
}
|
||||
if err := sshService.EditRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags SSH
|
||||
// @Summary Sycn host SSH secret
|
||||
// @Success 200
|
||||
|
|
@ -265,3 +267,28 @@ func (b *BaseApi) UpdateSSHByFile(c *gin.Context) {
|
|||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
func loadCertAfterDecrypt(req *dto.RootCertOperate) error {
|
||||
if len(req.PassPhrase) != 0 {
|
||||
passPhrase, err := base64.StdEncoding.DecodeString(req.PassPhrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.PassPhrase = string(passPhrase)
|
||||
}
|
||||
if len(req.PrivateKey) != 0 {
|
||||
privateKey, err := base64.StdEncoding.DecodeString(req.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.PrivateKey = string(privateKey)
|
||||
}
|
||||
if len(req.PublicKey) != 0 {
|
||||
publicKey, err := base64.StdEncoding.DecodeString(req.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.PublicKey = string(publicKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ type SSHInfo struct {
|
|||
CurrentUser string `json:"currentUser"`
|
||||
}
|
||||
|
||||
type CreateRootCert struct {
|
||||
type RootCertOperate struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Mode string `json:"mode"`
|
||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ type IHostRepo interface {
|
|||
GetCert(opts ...DBOption) (model.RootCert, error)
|
||||
PageCert(limit, offset int, opts ...DBOption) (int64, []model.RootCert, error)
|
||||
ListCert(opts ...DBOption) ([]model.RootCert, error)
|
||||
CreateCert(cert *model.RootCert) error
|
||||
SaveCert(cert *model.RootCert) error
|
||||
UpdateCert(id uint, vars map[string]interface{}) error
|
||||
DeleteCert(opts ...DBOption) error
|
||||
}
|
||||
|
|
@ -107,8 +107,8 @@ func (u *HostRepo) ListCert(opts ...DBOption) ([]model.RootCert, error) {
|
|||
return ops, err
|
||||
}
|
||||
|
||||
func (u *HostRepo) CreateCert(cert *model.RootCert) error {
|
||||
return global.DB.Create(cert).Error
|
||||
func (u *HostRepo) SaveCert(cert *model.RootCert) error {
|
||||
return global.DB.Save(cert).Error
|
||||
}
|
||||
|
||||
func (u *HostRepo) UpdateCert(id uint, vars map[string]interface{}) error {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ type ISSHService interface {
|
|||
ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error)
|
||||
|
||||
SyncRootCert() error
|
||||
CreateRootCert(req dto.CreateRootCert) error
|
||||
CreateRootCert(req dto.RootCertOperate) error
|
||||
EditRootCert(req dto.RootCertOperate) error
|
||||
SearchRootCerts(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
DeleteRootCerts(req dto.ForceDelete) error
|
||||
}
|
||||
|
|
@ -260,7 +261,7 @@ func (u *SSHService) SyncRootCert() error {
|
|||
return hostRepo.SyncCert(rootCerts)
|
||||
}
|
||||
|
||||
func (u *SSHService) CreateRootCert(req dto.CreateRootCert) error {
|
||||
func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
|
||||
if cmd.CheckIllegal(req.EncryptionMode, req.PassPhrase) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
|
|
@ -280,6 +281,19 @@ func (u *SSHService) CreateRootCert(req dto.CreateRootCert) error {
|
|||
publicPath := fmt.Sprintf("%s/.ssh/%s.pub", currentUser.HomeDir, req.Name)
|
||||
authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys"
|
||||
|
||||
authFileItem, _ := os.ReadFile(authFilePath)
|
||||
authFile := string(authFileItem)
|
||||
if authFile != "" && !strings.HasSuffix(authFile, "\n") {
|
||||
file, err := os.OpenFile(authFilePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
if _, err := file.WriteString("\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if req.Mode == "input" || req.Mode == "import" {
|
||||
if err := os.WriteFile(privatePath, []byte(req.PrivateKey), constant.FilePerm); err != nil {
|
||||
return err
|
||||
|
|
@ -308,7 +322,58 @@ func (u *SSHService) CreateRootCert(req dto.CreateRootCert) error {
|
|||
if len(cert.PassPhrase) != 0 {
|
||||
cert.PassPhrase, _ = encrypt.StringEncrypt(cert.PassPhrase)
|
||||
}
|
||||
return hostRepo.CreateCert(&cert)
|
||||
return hostRepo.SaveCert(&cert)
|
||||
}
|
||||
|
||||
func (u *SSHService) EditRootCert(req dto.RootCertOperate) error {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load current user failed, err: %v", err)
|
||||
}
|
||||
certItem, _ := hostRepo.GetCert(repo.WithByID(req.ID))
|
||||
if certItem.ID == 0 {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
oldPublicItem, err := os.ReadFile(certItem.PublicKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cert model.RootCert
|
||||
if err := copier.Copy(&cert, req); err != nil {
|
||||
return err
|
||||
}
|
||||
cert.PrivateKeyPath = fmt.Sprintf("%s/.ssh/%s", currentUser.HomeDir, req.Name)
|
||||
cert.PublicKeyPath = fmt.Sprintf("%s/.ssh/%s.pub", currentUser.HomeDir, req.Name)
|
||||
if err := os.WriteFile(cert.PrivateKeyPath, []byte(req.PrivateKey), constant.FilePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(cert.PublicKeyPath, []byte(req.PublicKey), constant.FilePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys"
|
||||
authItem, err := os.ReadFile(authFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldPublic := strings.ReplaceAll(string(oldPublicItem), "\n", "")
|
||||
newPublic := strings.ReplaceAll(string(req.PublicKey), "\n", "")
|
||||
lines := strings.Split(string(authItem), "\n")
|
||||
var newFiles []string
|
||||
for i := 0; i < len(lines); i++ {
|
||||
if len(lines[i]) != 0 && lines[i] != oldPublic && lines[i] != newPublic {
|
||||
newFiles = append(newFiles, lines[i])
|
||||
}
|
||||
}
|
||||
newFiles = append(newFiles, newPublic)
|
||||
if err := os.WriteFile(authFilePath, []byte(strings.Join(newFiles, "\n")), constant.FilePerm); err != nil {
|
||||
return fmt.Errorf("refresh authorized_keys failed, err: %v", err)
|
||||
}
|
||||
if len(cert.PassPhrase) != 0 {
|
||||
cert.PassPhrase, _ = encrypt.StringEncrypt(cert.PassPhrase)
|
||||
}
|
||||
return hostRepo.SaveCert(&cert)
|
||||
}
|
||||
|
||||
func (u *SSHService) SearchRootCerts(req dto.SearchWithPage) (int64, interface{}, error) {
|
||||
|
|
|
|||
|
|
@ -529,7 +529,7 @@ var InitLocalSSHConn = &gormigrate.Migration{
|
|||
itemPath = path.Join(currentInfo.HomeDir, ".ssh/id_ed25519_1panel")
|
||||
}
|
||||
if _, err := os.Stat(itemPath); err != nil {
|
||||
_ = service.NewISSHService().CreateRootCert(dto.CreateRootCert{EncryptionMode: "ed25519", Name: "id_ed25519_1panel", Description: "1Panel Terminal"})
|
||||
_ = service.NewISSHService().CreateRootCert(dto.RootCertOperate{EncryptionMode: "ed25519", Name: "id_ed25519_1panel", Description: "1Panel Terminal"})
|
||||
}
|
||||
privateKey, _ := os.ReadFile(itemPath)
|
||||
connWithKey := ssh.ConnInfo{
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
|
|||
hostRouter.POST("/ssh/file/update", baseApi.UpdateSSHByFile)
|
||||
|
||||
hostRouter.POST("/ssh/cert", baseApi.CreateRootCert)
|
||||
hostRouter.POST("/ssh/cert/update", baseApi.EditRootCert)
|
||||
hostRouter.POST("/ssh/cert/sync", baseApi.SyncRootCert)
|
||||
hostRouter.POST("/ssh/cert/search", baseApi.SearchRootCert)
|
||||
hostRouter.POST("/ssh/cert/delete", baseApi.DeleteRootCert)
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ export namespace Host {
|
|||
id: number;
|
||||
createAt: Date;
|
||||
name: string;
|
||||
mode: string;
|
||||
encryptionMode: string;
|
||||
passPhrase: string;
|
||||
description: string;
|
||||
|
|
|
|||
|
|
@ -93,6 +93,19 @@ export const createCert = (params: Host.RootCert) => {
|
|||
}
|
||||
return http.post(`/hosts/ssh/cert`, request);
|
||||
};
|
||||
export const editCert = (params: Host.RootCert) => {
|
||||
let request = deepCopy(params) as Host.RootCert;
|
||||
if (request.passPhrase) {
|
||||
request.passPhrase = Base64.encode(request.passPhrase);
|
||||
}
|
||||
if (request.privateKey) {
|
||||
request.privateKey = Base64.encode(request.privateKey);
|
||||
}
|
||||
if (request.publicKey) {
|
||||
request.publicKey = Base64.encode(request.publicKey);
|
||||
}
|
||||
return http.post(`/hosts/ssh/cert/update`, request);
|
||||
};
|
||||
export const searchCert = (params: ReqPage) => {
|
||||
return http.post<ResPage<Host.RootCertInfo>>(`/hosts/ssh/cert/search`, params);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div class="mb-4">
|
||||
<el-alert :closable="false">{{ $t('ssh.pubKeyHelper', [currentUser]) }}</el-alert>
|
||||
</div>
|
||||
<el-button type="primary" plain @click="onCreate()">
|
||||
<el-button type="primary" plain @click="onOpenDialog('create')">
|
||||
{{ $t('commons.button.create') }}
|
||||
</el-button>
|
||||
<el-button plain @click="onSync()">
|
||||
|
|
@ -33,115 +33,6 @@
|
|||
</template>
|
||||
</DrawerPro>
|
||||
|
||||
<DialogPro v-model="formOpen" :title="$t('commons.button.create')" size="w-60">
|
||||
<div>
|
||||
<el-form ref="formRef" label-position="top" :rules="rules" :model="form" v-loading="loading">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
<el-input v-model="form.name" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.encryptionMode')" prop="encryptionMode">
|
||||
<el-select v-model="form.encryptionMode">
|
||||
<el-option label="ED25519" value="ed25519" />
|
||||
<el-option label="ECDSA" value="ecdsa" />
|
||||
<el-option label="RSA" value="rsa" />
|
||||
<el-option label="DSA" value="dsa" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('commons.login.password')" prop="passPhrase">
|
||||
<el-input v-model="form.passPhrase" type="password" show-password>
|
||||
<template #append>
|
||||
<el-button @click="random">
|
||||
{{ $t('commons.button.random') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('ssh.createMode')" prop="privateKey">
|
||||
<el-radio-group v-model="form.mode">
|
||||
<el-radio value="generate">{{ $t('ssh.generate') }}</el-radio>
|
||||
<el-radio value="input">{{ $t('ssh.input') }}</el-radio>
|
||||
<el-radio value="import">{{ $t('ssh.import') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<div v-if="form.mode === 'input'">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.privateKey')" prop="privateKey">
|
||||
<el-input type="textarea" :rows="2" v-model="form.privateKey" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.publicKey')" prop="publicKey">
|
||||
<el-input type="textarea" :rows="2" v-model="form.publicKey" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div v-if="form.mode === 'import'">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.privateKey')" prop="privateKey">
|
||||
<el-upload
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
ref="uploadPrivateRef"
|
||||
class="upload mt-2 w-full"
|
||||
:limit="1"
|
||||
:on-change="privateOnChange"
|
||||
:on-exceed="privateExceed"
|
||||
>
|
||||
<el-button size="small" icon="Upload">
|
||||
{{ $t('commons.button.upload') }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.publicKey')" prop="publicKey">
|
||||
<el-upload
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
ref="uploadPublicRef"
|
||||
class="upload mt-2 w-full"
|
||||
:limit="1"
|
||||
:on-change="publicOnChange"
|
||||
:on-exceed="publicExceed"
|
||||
>
|
||||
<el-button size="small" icon="Upload">
|
||||
{{ $t('commons.button.upload') }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-form-item :label="$t('commons.table.description')" prop="description">
|
||||
<el-input v-model="form.description" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="onCancel">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" :disabled="loading" @click="onConfirm(formRef)">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
|
||||
<DialogPro v-model="connOpen" :title="$t('ssh.pubkey')" size="small" :showClose="false">
|
||||
<el-descriptions class="margin-top" :column="1" border>
|
||||
<el-descriptions-item align="center" :label="$t('ssh.password')">
|
||||
|
|
@ -198,16 +89,16 @@
|
|||
</el-form>
|
||||
</template>
|
||||
</OpDialog>
|
||||
<Operate ref="dialogRef" @search="search" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Host } from '@/api/interface/host';
|
||||
import { createCert, deleteCert, searchCert, syncCert } from '@/api/modules/host';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { deleteCert, searchCert, syncCert } from '@/api/modules/host';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import { copyText, getRandomStr } from '@/utils/util';
|
||||
import { FormInstance, genFileId, UploadFile, UploadProps, UploadRawFile } from 'element-plus';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import Operate from '@/views/host/ssh/ssh/certification/operate/index.vue';
|
||||
import { copyText } from '@/utils/util';
|
||||
import { Base64 } from 'js-base64';
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
|
|
@ -229,37 +120,7 @@ const opRef = ref();
|
|||
|
||||
const currentRow = ref();
|
||||
const connOpen = ref();
|
||||
|
||||
const formOpen = ref();
|
||||
const formRef = ref();
|
||||
const uploadPrivateRef = ref();
|
||||
const uploadPublicRef = ref();
|
||||
|
||||
const currentUser = ref();
|
||||
const form = reactive({
|
||||
name: '',
|
||||
mode: 'generate',
|
||||
passPhrase: '',
|
||||
encryptionMode: '',
|
||||
privateKey: '',
|
||||
publicKey: '',
|
||||
description: '',
|
||||
});
|
||||
const rules = reactive({
|
||||
name: Rules.simpleName,
|
||||
encryptionMode: Rules.requiredSelect,
|
||||
passPhrase: [{ validator: checkPassword, trigger: 'blur' }],
|
||||
});
|
||||
|
||||
function checkPassword(rule: any, value: any, callback: any) {
|
||||
if (form.passPhrase !== '') {
|
||||
const reg = /^[A-Za-z0-9]{6,15}$/;
|
||||
if (!reg.test(form.passPhrase)) {
|
||||
return callback(new Error(i18n.global.t('ssh.passwordHelper')));
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
const acceptParams = async (user: string): Promise<void> => {
|
||||
search();
|
||||
|
|
@ -267,10 +128,6 @@ const acceptParams = async (user: string): Promise<void> => {
|
|||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
const random = async () => {
|
||||
form.passPhrase = getRandomStr(10);
|
||||
};
|
||||
|
||||
const loadPassPhrase = () => {
|
||||
if (currentRow.value.passPhrase === '') {
|
||||
return '-';
|
||||
|
|
@ -284,72 +141,19 @@ const onCopy = async (content: string) => {
|
|||
copyText(content);
|
||||
};
|
||||
|
||||
const onCreate = () => {
|
||||
form.name = '';
|
||||
form.mode = 'generate';
|
||||
form.encryptionMode = 'ed25519';
|
||||
form.passPhrase = '';
|
||||
form.privateKey = '';
|
||||
form.publicKey = '';
|
||||
form.description = '';
|
||||
formOpen.value = true;
|
||||
};
|
||||
|
||||
const onConfirm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
loading.value = true;
|
||||
await createCert(form)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
formOpen.value = false;
|
||||
search();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const privateOnChange = (_uploadFile: UploadFile) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
form.privateKey = e.target.result as string;
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('cronjob.errImport') + error.message);
|
||||
}
|
||||
const dialogRef = ref();
|
||||
const onOpenDialog = async (
|
||||
title: string,
|
||||
rowData: Partial<Host.RootCertInfo> = {
|
||||
mode: 'generate',
|
||||
encryptionMode: 'ed25519',
|
||||
},
|
||||
) => {
|
||||
let params = {
|
||||
title,
|
||||
rowData: { ...rowData },
|
||||
};
|
||||
reader.readAsText(_uploadFile.raw);
|
||||
};
|
||||
const privateExceed: UploadProps['onExceed'] = (files) => {
|
||||
uploadPrivateRef.value!.clearFiles();
|
||||
const file = files[0] as UploadRawFile;
|
||||
file.uid = genFileId();
|
||||
uploadPrivateRef.value!.handleStart(file);
|
||||
};
|
||||
const publicOnChange = (_uploadFile: UploadFile) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
form.publicKey = e.target.result as string;
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('cronjob.errImport') + error.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(_uploadFile.raw);
|
||||
};
|
||||
const publicExceed: UploadProps['onExceed'] = (files) => {
|
||||
uploadPublicRef.value!.clearFiles();
|
||||
const file = files[0] as UploadRawFile;
|
||||
file.uid = genFileId();
|
||||
uploadPublicRef.value!.handleStart(file);
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
formOpen.value = false;
|
||||
dialogRef.value!.acceptParams(params);
|
||||
};
|
||||
|
||||
const onDownload = async (row: Host.RootCertInfo, type: string) => {
|
||||
|
|
@ -455,6 +259,12 @@ const handleClose = () => {
|
|||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
click: (row: Host.RootCertInfo) => {
|
||||
onOpenDialog('edit', row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.view'),
|
||||
click: (row: Host.RootCertInfo) => {
|
||||
|
|
|
|||
252
frontend/src/views/host/ssh/ssh/certification/operate/index.vue
Normal file
252
frontend/src/views/host/ssh/ssh/certification/operate/index.vue
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
<template>
|
||||
<DialogPro
|
||||
v-model="drawerVisible"
|
||||
:header="title"
|
||||
@close="handleClose"
|
||||
:resource="dialogData.title !== 'edit' ? '' : dialogData.rowData?.name"
|
||||
size="large"
|
||||
:autoClose="false"
|
||||
:fullScreen="true"
|
||||
>
|
||||
<el-form ref="formRef" label-position="top" :rules="rules" :model="dialogData.rowData" v-loading="loading">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
<el-input v-model="dialogData.rowData.name" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.encryptionMode')" prop="encryptionMode">
|
||||
<el-select v-model="dialogData.rowData.encryptionMode">
|
||||
<el-option label="ED25519" value="ed25519" />
|
||||
<el-option label="ECDSA" value="ecdsa" />
|
||||
<el-option label="RSA" value="rsa" />
|
||||
<el-option label="DSA" value="dsa" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('commons.login.password')" prop="passPhrase">
|
||||
<el-input v-model="dialogData.rowData.passPhrase" type="password" show-password>
|
||||
<template #append>
|
||||
<el-button @click="random">
|
||||
{{ $t('commons.button.random') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('ssh.createMode')" prop="mode" v-if="dialogData.title === 'create'">
|
||||
<el-radio-group v-model="dialogData.rowData.mode">
|
||||
<el-radio value="generate">{{ $t('ssh.generate') }}</el-radio>
|
||||
<el-radio value="input">{{ $t('ssh.input') }}</el-radio>
|
||||
<el-radio value="import">{{ $t('ssh.import') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<div v-if="dialogData.rowData.mode === 'input'">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.privateKey')" prop="privateKey">
|
||||
<el-input type="textarea" :rows="2" v-model="dialogData.rowData.privateKey" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.publicKey')" prop="publicKey">
|
||||
<el-input type="textarea" :rows="2" v-model="dialogData.rowData.publicKey" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div v-if="dialogData.rowData.mode === 'import'">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.privateKey')" prop="privateKey">
|
||||
<el-upload
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
ref="uploadPrivateRef"
|
||||
class="upload mt-2 w-full"
|
||||
:limit="1"
|
||||
:on-change="privateOnChange"
|
||||
:on-exceed="privateExceed"
|
||||
>
|
||||
<el-button size="small" icon="Upload">
|
||||
{{ $t('commons.button.upload') }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.publicKey')" prop="publicKey">
|
||||
<el-upload
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
ref="uploadPublicRef"
|
||||
class="upload mt-2 w-full"
|
||||
:limit="1"
|
||||
:on-change="publicOnChange"
|
||||
:on-exceed="publicExceed"
|
||||
>
|
||||
<el-button size="small" icon="Upload">
|
||||
{{ $t('commons.button.upload') }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-form-item :label="$t('commons.table.description')" prop="description">
|
||||
<el-input v-model="dialogData.rowData.description" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="onConfirm(formRef)">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm, genFileId, UploadFile, UploadProps, UploadRawFile } from 'element-plus';
|
||||
import { Host } from '@/api/interface/host';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { getRandomStr } from '@/utils/util';
|
||||
import { createCert, editCert } from '@/api/modules/host';
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
interface DialogProps {
|
||||
title: string;
|
||||
rowData?: Host.RootCertInfo;
|
||||
}
|
||||
const title = ref<string>('');
|
||||
const drawerVisible = ref(false);
|
||||
const dialogData = ref<DialogProps>({
|
||||
title: '',
|
||||
});
|
||||
const loading = ref();
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const formRef = ref();
|
||||
const uploadPrivateRef = ref();
|
||||
const uploadPublicRef = ref();
|
||||
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
dialogData.value = params;
|
||||
if (params.title === 'edit') {
|
||||
params.rowData.mode = 'input';
|
||||
dialogData.value.rowData.publicKey = Base64.decode(params.rowData.publicKey);
|
||||
dialogData.value.rowData.privateKey = Base64.decode(params.rowData.privateKey);
|
||||
if (params.rowData.passPhrase) {
|
||||
dialogData.value.rowData.passPhrase = Base64.decode(params.rowData.passPhrase);
|
||||
}
|
||||
}
|
||||
title.value = i18n.global.t('commons.button.' + dialogData.value.title);
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
function checkPassword(rule: any, value: any, callback: any) {
|
||||
if (dialogData.value.rowData.passPhrase !== '') {
|
||||
const reg = /^[A-Za-z0-9]{6,15}$/;
|
||||
if (!reg.test(dialogData.value.rowData.passPhrase)) {
|
||||
return callback(new Error(i18n.global.t('ssh.passwordHelper')));
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
const rules = reactive({
|
||||
name: Rules.simpleName,
|
||||
encryptionMode: Rules.requiredSelect,
|
||||
passPhrase: [{ validator: checkPassword, trigger: 'blur' }],
|
||||
privateKey: [Rules.requiredInput],
|
||||
publicKey: [Rules.requiredInput],
|
||||
});
|
||||
|
||||
const onConfirm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
loading.value = true;
|
||||
if (dialogData.value.title === 'create') {
|
||||
await createCert(dialogData.value.rowData)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
drawerVisible.value = false;
|
||||
emit('search');
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
} else {
|
||||
await editCert(dialogData.value.rowData)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
drawerVisible.value = false;
|
||||
emit('search');
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const privateOnChange = (_uploadFile: UploadFile) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
dialogData.value.rowData.privateKey = e.target.result as string;
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('cronjob.errImport') + error.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(_uploadFile.raw);
|
||||
};
|
||||
const privateExceed: UploadProps['onExceed'] = (files) => {
|
||||
uploadPrivateRef.value!.clearFiles();
|
||||
const file = files[0] as UploadRawFile;
|
||||
file.uid = genFileId();
|
||||
uploadPrivateRef.value!.handleStart(file);
|
||||
};
|
||||
const publicOnChange = (_uploadFile: UploadFile) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
dialogData.value.rowData.publicKey = e.target.result as string;
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('cronjob.errImport') + error.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(_uploadFile.raw);
|
||||
};
|
||||
const publicExceed: UploadProps['onExceed'] = (files) => {
|
||||
uploadPublicRef.value!.clearFiles();
|
||||
const file = files[0] as UploadRawFile;
|
||||
file.uid = genFileId();
|
||||
uploadPublicRef.value!.handleStart(file);
|
||||
};
|
||||
|
||||
const random = async () => {
|
||||
dialogData.value.rowData.passPhrase = getRandomStr(10);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
Loading…
Add table
Reference in a new issue