feat: Optimize SSH key management (#9782)

This commit is contained in:
ssongliu 2025-08-01 10:43:20 +08:00 committed by GitHub
parent c0a041d024
commit be0a460935
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 981 additions and 240 deletions

View file

@ -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)

View file

@ -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

View file

@ -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
View 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"`
}

View file

@ -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
}

View file

@ -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 ""
}

View file

@ -63,6 +63,7 @@ var AddTable = &gormigrate.Migration{
&model.Group{},
&model.AppIgnoreUpgrade{},
&model.McpServer{},
&model.RootCert{},
)
},
}

View file

@ -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)

View file

@ -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;

View file

@ -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);

View file

@ -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',

View file

@ -1471,12 +1471,18 @@ const message = {
passwordAuthentication: 'パスワード認証',
pwdAuthHelper: 'パスワード認証を有効にするかどうかこのパラメーターはデフォルトで有効になります',
pubkeyAuthentication: '重要な認証',
key: '鍵',
privateKey: '秘密鍵',
publicKey: '公開鍵',
password: 'パスワード',
createMode: '作成方法',
generate: '自動生成',
unSyncPass: '鍵パスワードは同期できません',
input: '手動入力',
import: 'ファイルアップロード',
pubkey: '重要な情報',
encryptionMode: '暗号化モード',
pubKeyHelper: '現在の鍵情報はユーザー {0} にのみ有効です',
passwordHelper: '610桁と英語のケースを含めることができます',
generate: 'キーを生成します',
reGenerate: 'キーを再生します',
keyAuthHelper: 'キー認証を有効にするかどうか',
useDNS: '使用済み',

View file

@ -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',

View file

@ -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',

View file

@ -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 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',

View file

@ -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',

View file

@ -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ı',

View file

@ -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: '反向解析',

View file

@ -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: '反向解析',

View 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>

View file

@ -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) => {

View file

@ -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>