mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-24 22:51:19 +08:00
feat: Optimize SSH key management (#9782)
This commit is contained in:
parent
c0a041d024
commit
be0a460935
22 changed files with 981 additions and 240 deletions
|
|
@ -113,7 +113,7 @@ func loadLocalConn() (*ssh.SSHClient, error) {
|
|||
itemPath = path.Join(currentInfo.HomeDir, ".ssh/id_ed25519_1panel")
|
||||
}
|
||||
if _, err := os.Stat(itemPath); err != nil {
|
||||
_ = sshService.GenerateSSH(dto.GenerateSSH{EncryptionMode: "ed25519", Name: "_1panel"})
|
||||
_ = sshService.CreateRootCert(dto.CreateRootCert{EncryptionMode: "ed25519", Name: "1panel", Description: "1Panel Terminal"})
|
||||
}
|
||||
|
||||
privateKey, _ := os.ReadFile(itemPath)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package v2
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
|
@ -90,19 +92,58 @@ func (b *BaseApi) UpdateSSHByfile(c *gin.Context) {
|
|||
// @Tags SSH
|
||||
// @Summary Generate host SSH secret
|
||||
// @Accept json
|
||||
// @Param request body dto.GenerateSSH true "request"
|
||||
// @Param request body dto.CreateRootCert true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/ssh/generate [post]
|
||||
// @Router /hosts/ssh/cert [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 SSH 密钥 ","formatEN":"generate SSH secret"}
|
||||
func (b *BaseApi) GenerateSSH(c *gin.Context) {
|
||||
var req dto.GenerateSSH
|
||||
func (b *BaseApi) CreateRootCert(c *gin.Context) {
|
||||
var req dto.CreateRootCert
|
||||
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 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.GenerateSSH(req); err != nil {
|
||||
if err := sshService.CreateRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags SSH
|
||||
// @Summary Sycn host SSH secret
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/ssh/cert/sync [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 SSH 密钥 ","formatEN":"sync SSH secret"}
|
||||
func (b *BaseApi) SyncRootCert(c *gin.Context) {
|
||||
if err := sshService.SyncRootCert(); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -112,23 +153,48 @@ func (b *BaseApi) GenerateSSH(c *gin.Context) {
|
|||
// @Tags SSH
|
||||
// @Summary Load host SSH secret
|
||||
// @Accept json
|
||||
// @Param request body dto.GenerateLoad true "request"
|
||||
// @Success 200 {string} secret
|
||||
// @Param request body dto.SearchWithPage true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/ssh/secret [post]
|
||||
func (b *BaseApi) LoadSSHSecret(c *gin.Context) {
|
||||
var req dto.GenerateLoad
|
||||
// @Router /hosts/ssh/cert/search [post]
|
||||
func (b *BaseApi) SearchRootCert(c *gin.Context) {
|
||||
var req dto.SearchWithPage
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := sshService.LoadSSHSecret(req.EncryptionMode)
|
||||
total, data, err := sshService.SearchRootCerts(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Total: total,
|
||||
Items: data,
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags SSH
|
||||
// @Summary Delete host SSH secret
|
||||
// @Accept json
|
||||
// @Param request body dto.ForceDelete true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/ssh/cert/delete [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 SSH 密钥 ","formatEN":"delete SSH secret"}
|
||||
func (b *BaseApi) DeleteRootCert(c *gin.Context) {
|
||||
var req dto.ForceDelete
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := sshService.DeleteRootCerts(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags SSH
|
||||
|
|
|
|||
|
|
@ -19,12 +19,27 @@ type SSHInfo struct {
|
|||
PubkeyAuthentication string `json:"pubkeyAuthentication"`
|
||||
PermitRootLogin string `json:"permitRootLogin"`
|
||||
UseDNS string `json:"useDNS"`
|
||||
CurrentUser string `json:"currentUser"`
|
||||
}
|
||||
|
||||
type GenerateSSH struct {
|
||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
||||
Password string `json:"password"`
|
||||
type CreateRootCert struct {
|
||||
Name string `json:"name"`
|
||||
Mode string `json:"mode"`
|
||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
||||
PassPhrase string `json:"passPhrase"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
type RootCert struct {
|
||||
ID uint `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Name string `json:"name"`
|
||||
EncryptionMode string `json:"encryptionMode"`
|
||||
PassPhrase string `json:"passPhrase"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type GenerateLoad struct {
|
||||
|
|
|
|||
11
agent/app/model/ssh.go
Normal file
11
agent/app/model/ssh.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package model
|
||||
|
||||
type RootCert struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"not null;"`
|
||||
EncryptionMode string `json:"encryptionMode"`
|
||||
PassPhrase string `json:"passPhrase"`
|
||||
PublicKeyPath string `json:"publicKeyPath"`
|
||||
PrivateKeyPath string `json:"privateKeyPath"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@ package repo
|
|||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
)
|
||||
|
||||
type HostRepo struct{}
|
||||
|
|
@ -13,6 +15,14 @@ type IHostRepo interface {
|
|||
SaveFirewallRecord(firewall *model.Firewall) error
|
||||
DeleteFirewallRecordByID(id uint) error
|
||||
DeleteFirewallRecord(fType, port, protocol, address, strategy string) error
|
||||
|
||||
SyncCert(data []model.RootCert) error
|
||||
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
|
||||
UpdateCert(id uint, vars map[string]interface{}) error
|
||||
DeleteCert(opts ...DBOption) error
|
||||
}
|
||||
|
||||
func NewIHostRepo() IHostRepo {
|
||||
|
|
@ -63,3 +73,82 @@ func (h *HostRepo) DeleteFirewallRecordByID(id uint) error {
|
|||
func (h *HostRepo) DeleteFirewallRecord(fType, port, protocol, address, strategy string) error {
|
||||
return global.DB.Where("type = ? AND port = ? AND protocol = ? AND address = ? AND strategy = ?", fType, port, protocol, address, strategy).Delete(&model.Firewall{}).Error
|
||||
}
|
||||
|
||||
func (u *HostRepo) GetCert(opts ...DBOption) (model.RootCert, error) {
|
||||
var cert model.RootCert
|
||||
db := global.DB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.First(&cert).Error
|
||||
return cert, err
|
||||
}
|
||||
|
||||
func (u *HostRepo) PageCert(page, size int, opts ...DBOption) (int64, []model.RootCert, error) {
|
||||
var ops []model.RootCert
|
||||
db := global.DB.Model(&model.RootCert{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
count := int64(0)
|
||||
db = db.Count(&count)
|
||||
err := db.Limit(size).Offset(size * (page - 1)).Find(&ops).Error
|
||||
return count, ops, err
|
||||
}
|
||||
|
||||
func (u *HostRepo) ListCert(opts ...DBOption) ([]model.RootCert, error) {
|
||||
var ops []model.RootCert
|
||||
db := global.DB.Model(&model.RootCert{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
count := int64(0)
|
||||
db = db.Count(&count)
|
||||
err := db.Find(&ops).Error
|
||||
return ops, err
|
||||
}
|
||||
|
||||
func (u *HostRepo) CreateCert(cert *model.RootCert) error {
|
||||
return global.DB.Create(cert).Error
|
||||
}
|
||||
|
||||
func (u *HostRepo) UpdateCert(id uint, vars map[string]interface{}) error {
|
||||
return global.DB.Model(&model.RootCert{}).Where("id = ?", id).Updates(vars).Error
|
||||
}
|
||||
|
||||
func (u *HostRepo) DeleteCert(opts ...DBOption) error {
|
||||
db := global.DB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
return db.Delete(&model.RootCert{}).Error
|
||||
}
|
||||
|
||||
func (u *HostRepo) SyncCert(data []model.RootCert) error {
|
||||
tx := global.DB.Begin()
|
||||
var oldCerts []model.RootCert
|
||||
_ = tx.Where("1 = ?", 1).Find(&oldCerts).Error
|
||||
oldCertsMap := make(map[string]uint)
|
||||
for _, item := range oldCerts {
|
||||
oldCertsMap[item.Name] = item.ID
|
||||
}
|
||||
for _, item := range data {
|
||||
if _, ok := oldCertsMap[item.Name]; ok {
|
||||
delete(oldCertsMap, item.Name)
|
||||
continue
|
||||
}
|
||||
item.PassPhrase, _ = encrypt.StringEncrypt("<UN-SET>")
|
||||
if err := tx.Model(model.RootCert{}).Create(&item).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, val := range oldCertsMap {
|
||||
if err := tx.Model(&model.RootCert{}).Where("id = ?", val).Updates(map[string]interface{}{"status": constant.StatusDeleted}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
|
|
@ -9,16 +11,19 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/geo"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/systemctl"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
|
@ -32,11 +37,13 @@ type ISSHService interface {
|
|||
OperateSSH(operation string) error
|
||||
UpdateByFile(value string) error
|
||||
Update(req dto.SSHUpdate) error
|
||||
GenerateSSH(req dto.GenerateSSH) error
|
||||
LoadSSHSecret(mode string) (string, error)
|
||||
LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog, error)
|
||||
|
||||
LoadSSHConf() (string, error)
|
||||
|
||||
SyncRootCert() error
|
||||
CreateRootCert(req dto.CreateRootCert) error
|
||||
SearchRootCerts(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
DeleteRootCerts(req dto.ForceDelete) error
|
||||
}
|
||||
|
||||
func NewISSHService() ISSHService {
|
||||
|
|
@ -110,6 +117,14 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
|
|||
data.UseDNS = strings.ReplaceAll(line, "UseDNS ", "")
|
||||
}
|
||||
}
|
||||
|
||||
currentUser, err := user.Current()
|
||||
if err != nil || len(currentUser.Name) == 0 {
|
||||
data.CurrentUser = "root"
|
||||
} else {
|
||||
data.CurrentUser = currentUser.Name
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
|
|
@ -214,70 +229,172 @@ func (u *SSHService) UpdateByFile(value string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (u *SSHService) GenerateSSH(req dto.GenerateSSH) error {
|
||||
if cmd.CheckIllegal(req.EncryptionMode, req.Password) {
|
||||
func (u *SSHService) SyncRootCert() error {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load current user failed, err: %v", err)
|
||||
}
|
||||
sshDir := fmt.Sprintf("%s/.ssh", currentUser.HomeDir)
|
||||
authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys"
|
||||
authItem, err := os.ReadFile(authFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileList, err := os.ReadDir(sshDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var rootCerts []model.RootCert
|
||||
fileMap := make(map[string]bool)
|
||||
for _, item := range fileList {
|
||||
if !item.IsDir() {
|
||||
fileMap[item.Name()] = true
|
||||
}
|
||||
}
|
||||
for item := range fileMap {
|
||||
if !strings.HasSuffix(item, ".pub") {
|
||||
continue
|
||||
}
|
||||
if !fileMap[strings.TrimSuffix(item, ".pub")] {
|
||||
continue
|
||||
}
|
||||
cert := model.RootCert{Name: strings.TrimSuffix(item, ".pub"), PublicKeyPath: path.Join(sshDir, item), PrivateKeyPath: path.Join(sshDir, strings.TrimSuffix(item, ".pub"))}
|
||||
pubItem, err := os.ReadFile(path.Join(sshDir, item))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("read pubic key of %s for sync failed, err: %v", item, err)
|
||||
continue
|
||||
}
|
||||
cert.EncryptionMode = loadEncryptioMode(string(pubItem))
|
||||
if !bytes.Contains(authItem, pubItem) {
|
||||
global.LOG.Error("the public key is not in authorized_keys, skip...")
|
||||
continue
|
||||
}
|
||||
rootCerts = append(rootCerts, cert)
|
||||
}
|
||||
return hostRepo.SyncCert(rootCerts)
|
||||
}
|
||||
|
||||
func (u *SSHService) CreateRootCert(req dto.CreateRootCert) error {
|
||||
if cmd.CheckIllegal(req.EncryptionMode, req.PassPhrase) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
certItem, _ := hostRepo.GetCert(repo.WithByName(req.Name))
|
||||
if certItem.ID != 0 {
|
||||
return buserr.New("ErrRecordExist")
|
||||
}
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load current user failed, err: %v", err)
|
||||
}
|
||||
secretFile := fmt.Sprintf("%s/.ssh/id_item_%s", currentUser.HomeDir, req.EncryptionMode)
|
||||
secretPubFile := fmt.Sprintf("%s/.ssh/id_item_%s.pub", currentUser.HomeDir, req.EncryptionMode)
|
||||
var cert model.RootCert
|
||||
if err := copier.Copy(&cert, req); err != nil {
|
||||
return err
|
||||
}
|
||||
privatePath := fmt.Sprintf("%s/.ssh/%s", currentUser.HomeDir, req.Name)
|
||||
publicPath := fmt.Sprintf("%s/.ssh/%s.pub", currentUser.HomeDir, req.Name)
|
||||
authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys"
|
||||
|
||||
command := fmt.Sprintf("ssh-keygen -t %s -f %s/.ssh/id_item_%s | echo y", req.EncryptionMode, currentUser.HomeDir, req.EncryptionMode)
|
||||
if len(req.Password) != 0 {
|
||||
command = fmt.Sprintf("ssh-keygen -t %s -P %s -f %s/.ssh/id_item_%s | echo y", req.EncryptionMode, req.Password, currentUser.HomeDir, req.EncryptionMode)
|
||||
if req.Mode == "input" || req.Mode == "import" {
|
||||
if err := os.WriteFile(privatePath, []byte(req.PrivateKey), constant.FilePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(publicPath, []byte(req.PublicKey), constant.FilePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
command := fmt.Sprintf("ssh-keygen -t %s -f %s/.ssh/%s -N ''", req.EncryptionMode, currentUser.HomeDir, req.Name)
|
||||
if len(req.PassPhrase) != 0 {
|
||||
command = fmt.Sprintf("ssh-keygen -t %s -P %s -f %s/.ssh/%s | echo y", req.EncryptionMode, req.PassPhrase, currentUser.HomeDir, req.Name)
|
||||
}
|
||||
stdout, err := cmd.RunDefaultWithStdoutBashC(command)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate failed, err: %v, message: %s", err, stdout)
|
||||
}
|
||||
}
|
||||
stdout, err := cmd.RunDefaultWithStdoutBashC(command)
|
||||
|
||||
stdout, err := cmd.RunDefaultWithStdoutBashCf("cat %s >> %s", publicPath, authFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate failed, err: %v, message: %s", err, stdout)
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Remove(secretFile)
|
||||
}()
|
||||
defer func() {
|
||||
_ = os.Remove(secretPubFile)
|
||||
}()
|
||||
|
||||
if _, err := os.Stat(authFilePath); err != nil && errors.Is(err, os.ErrNotExist) {
|
||||
authFile, err := os.Create(authFilePath)
|
||||
if err != nil {
|
||||
cert.PrivateKeyPath = privatePath
|
||||
cert.PublicKeyPath = publicPath
|
||||
if len(cert.PassPhrase) != 0 {
|
||||
cert.PassPhrase, _ = encrypt.StringEncrypt(cert.PassPhrase)
|
||||
}
|
||||
return hostRepo.CreateCert(&cert)
|
||||
}
|
||||
|
||||
func (u *SSHService) SearchRootCerts(req dto.SearchWithPage) (int64, interface{}, error) {
|
||||
total, records, err := hostRepo.PageCert(req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
var datas []dto.RootCert
|
||||
for i := 0; i < len(records); i++ {
|
||||
publicItem, err := os.ReadFile(records[i].PublicKeyPath)
|
||||
var publicBase64 string
|
||||
if err == nil && len(publicItem) != 0 {
|
||||
publicBase64 = base64.StdEncoding.EncodeToString(publicItem)
|
||||
}
|
||||
privateItem, _ := os.ReadFile(records[i].PrivateKeyPath)
|
||||
var privateBase64 string
|
||||
if err == nil && len(publicItem) != 0 {
|
||||
privateBase64 = base64.StdEncoding.EncodeToString(privateItem)
|
||||
}
|
||||
passPhrase, _ := encrypt.StringDecryptWithBase64(records[i].PassPhrase)
|
||||
datas = append(datas, dto.RootCert{
|
||||
ID: records[i].ID,
|
||||
CreatedAt: records[i].CreatedAt,
|
||||
Name: records[i].Name,
|
||||
EncryptionMode: records[i].EncryptionMode,
|
||||
PassPhrase: passPhrase,
|
||||
PublicKey: publicBase64,
|
||||
PrivateKey: privateBase64,
|
||||
Description: records[i].Description,
|
||||
})
|
||||
}
|
||||
return total, datas, err
|
||||
}
|
||||
|
||||
func (u *SSHService) DeleteRootCerts(req dto.ForceDelete) error {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil && !req.ForceDelete {
|
||||
return fmt.Errorf("load current user failed, err: %v", err)
|
||||
}
|
||||
authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys"
|
||||
authItem, err := os.ReadFile(authFilePath)
|
||||
if err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
for _, id := range req.IDs {
|
||||
cert, _ := hostRepo.GetCert(repo.WithByID(id))
|
||||
if cert.ID == 0 {
|
||||
if !req.ForceDelete {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
publicItem, err := os.ReadFile(cert.PublicKeyPath)
|
||||
if err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
newFile := bytes.ReplaceAll(authItem, publicItem, nil)
|
||||
if err := os.WriteFile(authFilePath, newFile, constant.FilePerm); err != nil && !req.ForceDelete {
|
||||
return fmt.Errorf("refresh authorized_keys failed, err: %v", err)
|
||||
}
|
||||
_ = os.Remove(cert.PublicKeyPath)
|
||||
_ = os.Remove(cert.PrivateKeyPath)
|
||||
if err := hostRepo.DeleteCert(repo.WithByID(id)); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
defer authFile.Close()
|
||||
}
|
||||
stdout1, err := cmd.RunDefaultWithStdoutBashCf("cat %s >> %s/.ssh/authorized_keys", secretPubFile, currentUser.HomeDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate failed, err: %v, message: %s", err, stdout1)
|
||||
}
|
||||
|
||||
fileOp := files.NewFileOp()
|
||||
if err := fileOp.Rename(secretFile, fmt.Sprintf("%s/.ssh/id_%s%s", currentUser.HomeDir, req.EncryptionMode, req.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileOp.Rename(secretPubFile, fmt.Sprintf("%s/.ssh/id_%s%s.pub", currentUser.HomeDir, req.EncryptionMode, req.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SSHService) LoadSSHSecret(mode string) (string, error) {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("load current user failed, err: %v", err)
|
||||
}
|
||||
|
||||
homeDir := currentUser.HomeDir
|
||||
if _, err := os.Stat(fmt.Sprintf("%s/.ssh/id_%s", homeDir, mode)); err != nil {
|
||||
return "", nil
|
||||
}
|
||||
file, err := os.ReadFile(fmt.Sprintf("%s/.ssh/id_%s", homeDir, mode))
|
||||
return string(file), err
|
||||
}
|
||||
|
||||
type sshFileItem struct {
|
||||
Name string
|
||||
Year int
|
||||
|
|
@ -576,3 +693,19 @@ func analyzeDateStr(parts []string) (int, string) {
|
|||
}
|
||||
return 2, fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2])
|
||||
}
|
||||
|
||||
func loadEncryptioMode(content string) string {
|
||||
if strings.HasPrefix(content, "ssh-rsa") {
|
||||
return "rsa"
|
||||
}
|
||||
if strings.HasPrefix(content, "ssh-ed25519") {
|
||||
return "ed25519"
|
||||
}
|
||||
if strings.HasPrefix(content, "ssh-ecdsa") {
|
||||
return "ecdsa"
|
||||
}
|
||||
if strings.HasPrefix(content, "ssh-dsa") {
|
||||
return "dsa"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ var AddTable = &gormigrate.Migration{
|
|||
&model.Group{},
|
||||
&model.AppIgnoreUpgrade{},
|
||||
&model.McpServer{},
|
||||
&model.RootCert{},
|
||||
)
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,15 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
|
|||
hostRouter.GET("/ssh/conf", baseApi.LoadSSHConf)
|
||||
hostRouter.POST("/ssh/search", baseApi.GetSSHInfo)
|
||||
hostRouter.POST("/ssh/update", baseApi.UpdateSSH)
|
||||
hostRouter.POST("/ssh/generate", baseApi.GenerateSSH)
|
||||
hostRouter.POST("/ssh/secret", baseApi.LoadSSHSecret)
|
||||
hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs)
|
||||
hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile)
|
||||
hostRouter.POST("/ssh/operate", baseApi.OperateSSH)
|
||||
|
||||
hostRouter.POST("/ssh/cert", baseApi.CreateRootCert)
|
||||
hostRouter.POST("/ssh/cert/sync", baseApi.SyncRootCert)
|
||||
hostRouter.POST("/ssh/cert/search", baseApi.SearchRootCert)
|
||||
hostRouter.POST("/ssh/cert/delete", baseApi.DeleteRootCert)
|
||||
|
||||
hostRouter.POST("/tool", baseApi.GetToolStatus)
|
||||
hostRouter.POST("/tool/init", baseApi.InitToolConfig)
|
||||
hostRouter.POST("/tool/operate", baseApi.OperateTool)
|
||||
|
|
|
|||
|
|
@ -161,15 +161,31 @@ export namespace Host {
|
|||
primaryKey: string;
|
||||
permitRootLogin: string;
|
||||
useDNS: string;
|
||||
currentUser: string;
|
||||
}
|
||||
export interface SSHUpdate {
|
||||
key: string;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
}
|
||||
export interface SSHGenerate {
|
||||
export interface RootCert {
|
||||
name: string;
|
||||
mode: string;
|
||||
encryptionMode: string;
|
||||
password: string;
|
||||
passPhrase: string;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
description: string;
|
||||
}
|
||||
export interface RootCertInfo {
|
||||
id: number;
|
||||
createAt: Date;
|
||||
name: string;
|
||||
encryptionMode: string;
|
||||
passPhrase: string;
|
||||
description: string;
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
}
|
||||
export interface searchSSHLog extends ReqPage {
|
||||
info: string;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import http from '@/api';
|
||||
import { ResPage } from '../interface';
|
||||
import { ResPage, ReqPage } from '../interface';
|
||||
import { Host } from '../interface/host';
|
||||
import { TimeoutEnum } from '@/enums/http-enum';
|
||||
import { deepCopy } from '@/utils/util';
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
// firewall
|
||||
export const loadFireBaseInfo = () => {
|
||||
|
|
@ -71,11 +73,27 @@ export const updateSSH = (params: Host.SSHUpdate) => {
|
|||
export const updateSSHByfile = (file: string) => {
|
||||
return http.post(`/hosts/ssh/conffile/update`, { file: file }, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const generateSecret = (params: Host.SSHGenerate) => {
|
||||
return http.post(`/hosts/ssh/generate`, params);
|
||||
export const createCert = (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`, request);
|
||||
};
|
||||
export const loadSecret = (mode: string) => {
|
||||
return http.post<string>(`/hosts/ssh/secret`, { encryptionMode: mode });
|
||||
export const searchCert = (params: ReqPage) => {
|
||||
return http.post<ResPage<Host.RootCertInfo>>(`/hosts/ssh/cert/search`, params);
|
||||
};
|
||||
export const deleteCert = (ids: Array<number>, forceDelete: boolean) => {
|
||||
return http.post(`/hosts/ssh/cert/delete`, { ids: ids, forceDelete: forceDelete });
|
||||
};
|
||||
export const syncCert = () => {
|
||||
return http.post(`/hosts/ssh/cert/sync`);
|
||||
};
|
||||
export const loadSSHLogs = (params: Host.searchSSHLog) => {
|
||||
return http.post<Host.sshLog>(`/hosts/ssh/log`, params);
|
||||
|
|
|
|||
|
|
@ -1527,12 +1527,18 @@ const message = {
|
|||
passwordAuthentication: 'Password auth',
|
||||
pwdAuthHelper: 'Whether to enable password authentication. This parameter is enabled by default.',
|
||||
pubkeyAuthentication: 'Key auth',
|
||||
key: 'Key',
|
||||
privateKey: 'Private Key',
|
||||
publicKey: 'Public Key',
|
||||
password: 'Password',
|
||||
createMode: 'Creation Method',
|
||||
generate: 'Auto-generate',
|
||||
unSyncPass: 'Key password cannot be synchronized',
|
||||
input: 'Manual Input',
|
||||
import: 'File Upload',
|
||||
pubkey: 'Key info',
|
||||
pubKeyHelper: 'The current key information only takes effect for user {0}',
|
||||
encryptionMode: 'Encryption mode',
|
||||
passwordHelper: 'Can contain 6 to 10 digits and English cases',
|
||||
generate: 'Generate key',
|
||||
reGenerate: 'Regenerate key',
|
||||
keyAuthHelper: 'Whether to enable key authentication.',
|
||||
useDNS: 'useDNS',
|
||||
|
|
|
|||
|
|
@ -1471,12 +1471,18 @@ const message = {
|
|||
passwordAuthentication: 'パスワード認証',
|
||||
pwdAuthHelper: 'パスワード認証を有効にするかどうか。このパラメーターはデフォルトで有効になります。',
|
||||
pubkeyAuthentication: '重要な認証',
|
||||
key: '鍵',
|
||||
privateKey: '秘密鍵',
|
||||
publicKey: '公開鍵',
|
||||
password: 'パスワード',
|
||||
createMode: '作成方法',
|
||||
generate: '自動生成',
|
||||
unSyncPass: '鍵パスワードは同期できません',
|
||||
input: '手動入力',
|
||||
import: 'ファイルアップロード',
|
||||
pubkey: '重要な情報',
|
||||
encryptionMode: '暗号化モード',
|
||||
pubKeyHelper: '現在の鍵情報はユーザー {0} にのみ有効です',
|
||||
passwordHelper: '6〜10桁と英語のケースを含めることができます',
|
||||
generate: 'キーを生成します',
|
||||
reGenerate: 'キーを再生します',
|
||||
keyAuthHelper: 'キー認証を有効にするかどうか。',
|
||||
useDNS: '使用済み',
|
||||
|
|
|
|||
|
|
@ -1455,12 +1455,18 @@ const message = {
|
|||
passwordAuthentication: '비밀번호 인증',
|
||||
pwdAuthHelper: '비밀번호 인증을 활성화할지 여부입니다. 기본적으로 이 매개변수는 활성화되어 있습니다.',
|
||||
pubkeyAuthentication: '키 인증',
|
||||
key: '키',
|
||||
privateKey: '개인 키',
|
||||
publicKey: '공개 키',
|
||||
password: '비밀번호',
|
||||
createMode: '생성 방식',
|
||||
generate: '자동 생성',
|
||||
unSyncPass: '키 비밀번호 동기화 불가',
|
||||
input: '수동 입력',
|
||||
import: '파일 업로드',
|
||||
pubkey: '키 정보',
|
||||
encryptionMode: '암호화 모드',
|
||||
pubKeyHelper: '현재 키 정보는 사용자 {0}에게만 적용됩니다',
|
||||
passwordHelper: '6~10자리 숫자 및 영어 대소문자를 포함할 수 있습니다.',
|
||||
generate: '키 생성',
|
||||
reGenerate: '키 재생성',
|
||||
keyAuthHelper: '키 인증을 활성화할지 여부입니다.',
|
||||
useDNS: 'useDNS',
|
||||
|
|
|
|||
|
|
@ -1513,12 +1513,18 @@ const message = {
|
|||
passwordAuthentication: 'Pengesahan kata laluan',
|
||||
pwdAuthHelper: 'Sama ada untuk mengaktifkan pengesahan kata laluan. Parameter ini diaktifkan secara lalai.',
|
||||
pubkeyAuthentication: 'Pengesahan kunci',
|
||||
key: 'Kunci',
|
||||
password: 'Kata laluan',
|
||||
privateKey: 'Kunci Persendirian',
|
||||
publicKey: 'Kunci Awam',
|
||||
password: 'Kata Laluan',
|
||||
createMode: 'Kaedah Penciptaan',
|
||||
generate: 'Jana Automatik',
|
||||
unSyncPass: 'Kata laluan kunci tidak dapat diselaraskan',
|
||||
input: 'Input Manual',
|
||||
import: 'Muat Naik Fail',
|
||||
pubkey: 'Maklumat kunci',
|
||||
pubKeyHelper: 'Maklumat kunci semasa hanya berkuat kuasa untuk pengguna {0}',
|
||||
encryptionMode: 'Mod penyulitan',
|
||||
passwordHelper: 'Boleh mengandungi 6 hingga 10 angka dan huruf dalam kedua-dua huruf besar dan kecil',
|
||||
generate: 'Jana kunci',
|
||||
reGenerate: 'Jana semula kunci',
|
||||
keyAuthHelper: 'Sama ada untuk mengaktifkan pengesahan kunci.',
|
||||
useDNS: 'Gunakan DNS',
|
||||
|
|
|
|||
|
|
@ -1501,12 +1501,18 @@ const message = {
|
|||
passwordAuthentication: 'Autenticação por senha',
|
||||
pwdAuthHelper: 'Se deve ou não habilitar a autenticação por senha. Esse parâmetro está habilitado por padrão.',
|
||||
pubkeyAuthentication: 'Autenticação por chave',
|
||||
key: 'Chave',
|
||||
privateKey: 'Chave Privada',
|
||||
publicKey: 'Chave Pública',
|
||||
password: 'Senha',
|
||||
createMode: 'Método de Criação',
|
||||
generate: 'Gerar Automaticamente',
|
||||
unSyncPass: 'Senha da chave não pode ser sincronizada',
|
||||
input: 'Entrada Manual',
|
||||
import: 'Upload de Arquivo',
|
||||
pubkey: 'Informações da chave',
|
||||
pubKeyHelper: 'A informação da chave atual só tem efeito para o usuário {0}',
|
||||
encryptionMode: 'Modo de criptografia',
|
||||
passwordHelper: 'Pode conter de 6 a 10 dígitos e letras maiúsculas e minúsculas',
|
||||
generate: 'Gerar chave',
|
||||
reGenerate: 'Regenerar chave',
|
||||
keyAuthHelper: 'Se deve ou não habilitar a autenticação por chave.',
|
||||
useDNS: 'Usar DNS',
|
||||
|
|
|
|||
|
|
@ -1502,12 +1502,18 @@ const message = {
|
|||
passwordAuthentication: 'Аутентификация по паролю',
|
||||
pwdAuthHelper: 'Включить ли аутентификацию по паролю. Этот параметр включен по умолчанию.',
|
||||
pubkeyAuthentication: 'Аутентификация по ключу',
|
||||
key: 'Ключ',
|
||||
privateKey: 'Приватный ключ',
|
||||
publicKey: 'Публичный ключ',
|
||||
password: 'Пароль',
|
||||
createMode: 'Способ создания',
|
||||
generate: 'Автогенерация',
|
||||
unSyncPass: 'Пароль ключа не может быть синхронизирован',
|
||||
input: 'Ручной ввод',
|
||||
import: 'Загрузка файла',
|
||||
pubkey: 'Информация о ключе',
|
||||
pubKeyHelper: 'Текущая информация о ключе действительна только для пользователя {0}',
|
||||
encryptionMode: 'Режим шифрования',
|
||||
passwordHelper: 'Может содержать от 6 до 10 цифр и английских букв в разных регистрах',
|
||||
generate: 'Сгенерировать ключ',
|
||||
reGenerate: 'Перегенерировать ключ',
|
||||
keyAuthHelper: 'Включить ли аутентификацию по ключу.',
|
||||
useDNS: 'useDNS',
|
||||
|
|
|
|||
|
|
@ -1545,12 +1545,18 @@ const message = {
|
|||
pwdAuthHelper:
|
||||
'Parola kimlik doğrulamasının etkinleştirilip etkinleştirilmeyeceği. Bu parametre varsayılan olarak etkindir.',
|
||||
pubkeyAuthentication: 'Anahtar kimlik doğrulaması',
|
||||
key: 'Anahtar',
|
||||
privateKey: 'Özel Anahtar',
|
||||
publicKey: 'Genel Anahtar',
|
||||
password: 'Parola',
|
||||
createMode: 'Oluşturma Yöntemi',
|
||||
generate: 'Otomatik Oluştur',
|
||||
unSyncPass: 'Anahtar parolası senkronize edilemez',
|
||||
input: 'Manuel Giriş',
|
||||
import: 'Dosya Yükleme',
|
||||
pubkey: 'Anahtar bilgisi',
|
||||
pubKeyHelper: 'Mevcut anahtar bilgileri yalnızca {0} kullanıcısı için geçerlidir',
|
||||
encryptionMode: 'Şifreleme modu',
|
||||
passwordHelper: '6 ila 10 hane ve İngilizce harfler içerebilir',
|
||||
generate: 'Anahtar oluştur',
|
||||
reGenerate: 'Anahtarı yeniden oluştur',
|
||||
keyAuthHelper: 'Anahtar kimlik doğrulamasının etkinleştirilip etkinleştirilmeyeceği.',
|
||||
useDNS: 'DNS kullanımı',
|
||||
|
|
|
|||
|
|
@ -1454,12 +1454,18 @@ const message = {
|
|||
passwordAuthentication: '密碼認證',
|
||||
pwdAuthHelper: '是否啟用密碼認證,默認啟用。',
|
||||
pubkeyAuthentication: '密鑰認證',
|
||||
key: '密鑰',
|
||||
privateKey: '私鑰',
|
||||
publicKey: '公鑰',
|
||||
password: '密碼',
|
||||
createMode: '創建方式',
|
||||
generate: '自動生成',
|
||||
unSyncPass: '密鑰密碼無法同步',
|
||||
input: '手動輸入',
|
||||
import: '文件上傳',
|
||||
pubkey: '密鑰信息',
|
||||
pubKeyHelper: '當前密鑰信息僅對用戶 {0} 生效',
|
||||
encryptionMode: '加密方式',
|
||||
passwordHelper: '支持大小寫英文、數字,長度6-10',
|
||||
generate: '生成密鑰',
|
||||
reGenerate: '重新生成密鑰',
|
||||
keyAuthHelper: '是否啟用密鑰認證,默認啟用。',
|
||||
useDNS: '反向解析',
|
||||
|
|
|
|||
|
|
@ -1449,12 +1449,18 @@ const message = {
|
|||
passwordAuthentication: '密码认证',
|
||||
pwdAuthHelper: '是否启用密码认证,默认启用。',
|
||||
pubkeyAuthentication: '密钥认证',
|
||||
key: '密钥',
|
||||
privateKey: '私钥',
|
||||
publicKey: '公钥',
|
||||
password: '密码',
|
||||
createMode: '创建方式',
|
||||
generate: '自动生成',
|
||||
unSyncPass: '密钥密码无法同步',
|
||||
input: '手动输入',
|
||||
import: '文件上传',
|
||||
pubkey: '密钥信息',
|
||||
pubKeyHelper: '当前密钥信息仅对用户 {0} 生效',
|
||||
encryptionMode: '加密方式',
|
||||
passwordHelper: '支持大小写英文、数字,长度6-10',
|
||||
generate: '生成密钥',
|
||||
reGenerate: '重新生成密钥',
|
||||
keyAuthHelper: '是否启用密钥认证,默认启用。',
|
||||
useDNS: '反向解析',
|
||||
|
|
|
|||
476
frontend/src/views/host/ssh/ssh/certification/index.vue
Normal file
476
frontend/src/views/host/ssh/ssh/certification/index.vue
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
<template>
|
||||
<div>
|
||||
<DrawerPro v-model="drawerVisible" :header="$t('ssh.pubkey')" @close="handleClose" size="large">
|
||||
<div class="mb-4">
|
||||
<el-alert :closable="false">{{ $t('ssh.pubKeyHelper', [currentUser]) }}</el-alert>
|
||||
</div>
|
||||
<el-button type="primary" plain @click="onCreate()">
|
||||
{{ $t('commons.button.create') }}
|
||||
</el-button>
|
||||
<el-button plain @click="onSync()">
|
||||
{{ $t('commons.button.sync') }}
|
||||
</el-button>
|
||||
<el-button plain :disabled="selects.length === 0" @click="onDelete(null)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
<ComplexTable
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
@search="search"
|
||||
:heightDiff="370"
|
||||
>
|
||||
<el-table-column type="selection" fix />
|
||||
<el-table-column :label="$t('commons.table.name')" show-overflow-tooltip prop="name" />
|
||||
<el-table-column :label="$t('ssh.encryptionMode')" prop="encryptionMode" />
|
||||
<el-table-column :label="$t('commons.table.description')" prop="description" />
|
||||
<fu-table-operations width="200px" :buttons="buttons" :label="$t('commons.table.operate')" />
|
||||
</ComplexTable>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
</span>
|
||||
</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')">
|
||||
<div>
|
||||
<span>{{ loadPassPhrase() }}</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="currentRow.passPhrase && Base64.decode(currentRow.passPhrase) !== '<UN-SET>'"
|
||||
size="small"
|
||||
icon="CopyDocument"
|
||||
@click="onCopy(currentRow.passPhrase)"
|
||||
>
|
||||
{{ $t('commons.button.copy') }}
|
||||
</el-button>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item align="center" :label="$t('ssh.publicKey')">
|
||||
<el-button-group size="small">
|
||||
<el-button icon="CopyDocument" @click="onCopy(currentRow.publicKey)">
|
||||
{{ $t('commons.button.copy') }}
|
||||
</el-button>
|
||||
<el-button icon="Download" @click="onDownload(currentRow, 'publicKey')">
|
||||
{{ $t('commons.button.download') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item align="center" :label="$t('ssh.privateKey')">
|
||||
<el-button-group size="small">
|
||||
<el-button icon="CopyDocument" @click="onCopy(currentRow.privateKey)">
|
||||
{{ $t('commons.button.copy') }}
|
||||
</el-button>
|
||||
<el-button icon="Download" @click="onDownload(currentRow, 'privateKey')">
|
||||
{{ $t('commons.button.download') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="connOpen = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()">
|
||||
<template #content>
|
||||
<el-form ref="deleteForm" label-position="left">
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="forceDelete" :label="$t('website.forceDelete')" />
|
||||
<span class="input-help">
|
||||
{{ $t('website.forceDeleteHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</OpDialog>
|
||||
</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 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 { Base64 } from 'js-base64';
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
const loading = ref();
|
||||
const drawerVisible = ref();
|
||||
const data = ref();
|
||||
const selects = ref<any>([]);
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'login-log-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
small: true,
|
||||
});
|
||||
|
||||
const forceDelete = ref();
|
||||
const operateIDs = ref();
|
||||
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();
|
||||
currentUser.value = user || 'root';
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
const random = async () => {
|
||||
form.passPhrase = getRandomStr(10);
|
||||
};
|
||||
|
||||
const loadPassPhrase = () => {
|
||||
if (currentRow.value.passPhrase === '') {
|
||||
return '-';
|
||||
}
|
||||
let pass = Base64.decode(currentRow.value.passPhrase);
|
||||
return pass === '<UN-SET>' ? i18n.global.t('ssh.unSyncPass') : '';
|
||||
};
|
||||
|
||||
const onCopy = async (content: string) => {
|
||||
content = Base64.decode(content);
|
||||
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);
|
||||
}
|
||||
};
|
||||
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;
|
||||
};
|
||||
|
||||
const onDownload = async (row: Host.RootCertInfo, type: string) => {
|
||||
let name = row.name;
|
||||
let content;
|
||||
if (type === 'publicKey') {
|
||||
name = row.name + '.pub';
|
||||
content = Base64.decode(row.publicKey);
|
||||
} else {
|
||||
content = Base64.decode(row.privateKey);
|
||||
}
|
||||
const downloadUrl = window.URL.createObjectURL(new Blob([content], { type: 'application/octet-stream' }));
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = downloadUrl;
|
||||
a.download = name;
|
||||
const event = new MouseEvent('click');
|
||||
a.dispatchEvent(event);
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
let params = {
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
};
|
||||
loading.value = true;
|
||||
await searchCert(params)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
data.value = res.data.items;
|
||||
paginationConfig.total = res.data.total;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onSync = async () => {
|
||||
loading.value = true;
|
||||
await syncCert()
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async (row: Host.RootCertInfo | null) => {
|
||||
let names = [];
|
||||
let ids = [];
|
||||
forceDelete.value = false;
|
||||
if (row) {
|
||||
ids = [row.id];
|
||||
names = [row.name + ' - ' + row.encryptionMode];
|
||||
} else {
|
||||
for (const item of selects.value) {
|
||||
names.push(item.name + ' - ' + item.encryptionMode);
|
||||
ids.push(item.id);
|
||||
}
|
||||
}
|
||||
operateIDs.value = ids;
|
||||
opRef.value.acceptParams({
|
||||
title: i18n.global.t('commons.button.delete'),
|
||||
names: names,
|
||||
msg: i18n.global.t('commons.msg.operatorHelper', [
|
||||
i18n.global.t('menu.cronjob'),
|
||||
i18n.global.t('commons.button.delete'),
|
||||
]),
|
||||
api: null,
|
||||
params: null,
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmitDelete = async () => {
|
||||
loading.value = true;
|
||||
await deleteCert(operateIDs.value, forceDelete.value)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
|
||||
search();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.view'),
|
||||
click: (row: Host.RootCertInfo) => {
|
||||
currentRow.value = row;
|
||||
connOpen.value = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: Host.RootCertInfo) => {
|
||||
onDelete(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marginTop {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -43,6 +43,8 @@
|
|||
<el-radio-button value="base">{{ $t('database.baseConf') }}</el-radio-button>
|
||||
<el-radio-button value="all">{{ $t('database.allConf') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<el-button @click="onOpenDrawer" class="mt-2 ml-2">{{ $t('ssh.pubkey') }}</el-button>
|
||||
<el-row class="mt-10" v-if="confShowType === 'base'">
|
||||
<el-col :xs="24" :sm="20" :md="20" :lg="10" :xl="10">
|
||||
<el-form :model="form" label-position="right" ref="formRef" label-width="100px">
|
||||
|
|
@ -93,9 +95,6 @@
|
|||
v-model="form.pubkeyAuthentication"
|
||||
></el-switch>
|
||||
<span class="input-help">{{ $t('ssh.keyAuthHelper') }}</span>
|
||||
<el-button link @click="onOpenDrawer" type="primary">
|
||||
{{ $t('ssh.pubkey') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('ssh.useDNS')" prop="useDNS">
|
||||
<el-switch
|
||||
|
|
@ -125,7 +124,7 @@
|
|||
</template>
|
||||
</LayoutContent>
|
||||
|
||||
<PubKey ref="pubKeyRef" @search="search" />
|
||||
<Cert ref="pubKeyRef" @search="search" />
|
||||
<Port ref="portRef" @search="search" />
|
||||
<Address ref="addressRef" @search="search" />
|
||||
<Root ref="rootsRef" @search="search" />
|
||||
|
|
@ -135,7 +134,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import FireRouter from '@/views/host/ssh/index.vue';
|
||||
import PubKey from '@/views/host/ssh/ssh/pubkey/index.vue';
|
||||
import Cert from '@/views/host/ssh/ssh/certification/index.vue';
|
||||
import Root from '@/views/host/ssh/ssh/root/index.vue';
|
||||
import Port from '@/views/host/ssh/ssh/port/index.vue';
|
||||
import Address from '@/views/host/ssh/ssh/address/index.vue';
|
||||
|
|
@ -169,6 +168,7 @@ const form = reactive({
|
|||
permitRootLogin: 'yes',
|
||||
permitRootLoginItem: 'yes',
|
||||
useDNS: 'no',
|
||||
currentUser: 'root',
|
||||
});
|
||||
|
||||
const onSaveFile = async () => {
|
||||
|
|
@ -190,7 +190,7 @@ const onSaveFile = async () => {
|
|||
};
|
||||
|
||||
const onOpenDrawer = () => {
|
||||
pubKeyRef.value.acceptParams();
|
||||
pubKeyRef.value.acceptParams(form.currentUser);
|
||||
};
|
||||
|
||||
const onChangePort = () => {
|
||||
|
|
@ -312,6 +312,7 @@ const search = async () => {
|
|||
form.permitRootLogin = res.data.permitRootLogin;
|
||||
form.permitRootLoginItem = loadPermitLabel(res.data.permitRootLogin);
|
||||
form.useDNS = res.data.useDNS;
|
||||
form.currentUser = res.data.currentUser;
|
||||
};
|
||||
|
||||
const loadPermitLabel = (value: string) => {
|
||||
|
|
|
|||
|
|
@ -1,142 +0,0 @@
|
|||
<template>
|
||||
<DrawerPro v-model="drawerVisible" :header="$t('ssh.pubkey')" @close="handleClose" size="small">
|
||||
<el-form ref="formRef" label-position="top" :rules="rules" :model="form" v-loading="loading">
|
||||
<el-form-item :label="$t('ssh.encryptionMode')" prop="encryptionMode">
|
||||
<el-select v-model="form.encryptionMode" @change="onLoadSecret">
|
||||
<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-form-item :label="$t('commons.login.password')" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password>
|
||||
<template #append>
|
||||
<el-button @click="onCopy(form.password)">
|
||||
{{ $t('commons.button.copy') }}
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button @click="random">
|
||||
{{ $t('commons.button.random') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('ssh.key')" prop="primaryKey" v-if="form.encryptionMode">
|
||||
<el-input v-model="form.primaryKey" :rows="5" type="textarea" />
|
||||
<div v-if="form.primaryKey">
|
||||
<el-button icon="CopyDocument" class="marginTop" @click="onCopy(form.primaryKey)">
|
||||
{{ $t('commons.button.copy') }}
|
||||
</el-button>
|
||||
<el-button icon="Download" class="marginTop" @click="onDownload">
|
||||
{{ $t('commons.button.download') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button @click="onGenerate(formRef)" type="primary">
|
||||
{{ $t('ssh.generate') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { generateSecret, loadSecret } from '@/api/modules/host';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { copyText, dateFormatForName, getRandomStr } from '@/utils/util';
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
const loading = ref();
|
||||
const drawerVisible = ref();
|
||||
|
||||
const formRef = ref();
|
||||
const form = reactive({
|
||||
password: '',
|
||||
encryptionMode: '',
|
||||
primaryKey: '',
|
||||
});
|
||||
const rules = reactive({
|
||||
encryptionMode: Rules.requiredSelect,
|
||||
password: [{ validator: checkPassword, trigger: 'blur' }],
|
||||
});
|
||||
|
||||
function checkPassword(rule: any, value: any, callback: any) {
|
||||
if (form.password !== '') {
|
||||
const reg = /^[A-Za-z0-9]{6,15}$/;
|
||||
if (!reg.test(form.password)) {
|
||||
return callback(new Error(i18n.global.t('ssh.passwordHelper')));
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
const acceptParams = async (): Promise<void> => {
|
||||
form.password = '';
|
||||
form.encryptionMode = 'rsa';
|
||||
form.primaryKey = '';
|
||||
onLoadSecret();
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
const random = async () => {
|
||||
form.password = getRandomStr(10);
|
||||
};
|
||||
|
||||
const onLoadSecret = async () => {
|
||||
const res = await loadSecret(form.encryptionMode);
|
||||
form.primaryKey = res.data || '';
|
||||
};
|
||||
|
||||
const onCopy = async (str: string) => {
|
||||
copyText(str);
|
||||
};
|
||||
|
||||
const onGenerate = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
let param = {
|
||||
encryptionMode: form.encryptionMode,
|
||||
password: form.password,
|
||||
};
|
||||
await generateSecret(param).then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
onLoadSecret();
|
||||
});
|
||||
});
|
||||
};
|
||||
const onDownload = async () => {
|
||||
const downloadUrl = window.URL.createObjectURL(new Blob([form.primaryKey]));
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = downloadUrl;
|
||||
const href = window.location.href;
|
||||
const host = href.split('//')[1].split(':')[0];
|
||||
a.download = host + '_' + dateFormatForName(new Date()) + '_id_' + form.encryptionMode;
|
||||
const event = new MouseEvent('click');
|
||||
a.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marginTop {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Reference in a new issue