From 9319f187b557cb8f2a09c68a7267c63d129ebeb0 Mon Sep 17 00:00:00 2001 From: guoyuqi Date: Thu, 24 Sep 2026 15:06:32 +0800 Subject: [PATCH 1/9] fix: fix container list --- agent/app/api/v2/container.go | 21 +++- agent/app/service/container.go | 113 +++++++++++++----- frontend/src/api/modules/container.ts | 35 +++--- .../src/views/container/container/index.vue | 106 +++++++++------- 4 files changed, 180 insertions(+), 95 deletions(-) diff --git a/agent/app/api/v2/container.go b/agent/app/api/v2/container.go index 7f378619745f..2489819cf318 100644 --- a/agent/app/api/v2/container.go +++ b/agent/app/api/v2/container.go @@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) { return } - total, list, err := containerService.Page(req) + total, list, err := containerService.Page(c.Request.Context(), req) if err != nil { helper.InternalServer(c, err) return @@ -249,9 +249,10 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) { // @Success 200 {object} dto.ContainerStatus // @Security ApiKeyAuth // @Security Timestamp +// @Param containersOnly query boolean false "Only count containers" // @Router /containers/status [get] func (b *BaseApi) LoadContainerStatus(c *gin.Context) { - data, err := containerService.LoadStatus() + data, err := containerService.LoadStatus(c.Request.Context(), c.Query("containersOnly") == "true") if err != nil { helper.InternalServer(c, err) return @@ -415,9 +416,14 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) { // @Success 200 {array} dto.ContainerListStats // @Security ApiKeyAuth // @Security Timestamp +// @Param ids query string false "Comma-separated container IDs; omitted selects all containers" // @Router /containers/list/stats [get] func (b *BaseApi) ContainerListStats(c *gin.Context) { - data, err := containerService.ContainerListStats() + var ids []string + if _, supplied := c.Request.URL.Query()["ids"]; supplied { + ids = strings.FieldsFunc(c.Query("ids"), func(r rune) bool { return r == ',' }) + } + data, err := containerService.ContainerListStats(c.Request.Context(), ids) if err != nil { helper.InternalServer(c, err) return @@ -964,3 +970,12 @@ func (b *BaseApi) ContainerStreamLogs(c *gin.Context) { containerService.StreamLogs(c, streamLog) } + +func (b *BaseApi) CleanNetworks(c *gin.Context) { + result, err := containerService.CleanNetworks() + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, result) +} diff --git a/agent/app/service/container.go b/agent/app/service/container.go index d65ef4f1fac4..def33c550cfb 100644 --- a/agent/app/service/container.go +++ b/agent/app/service/container.go @@ -58,10 +58,10 @@ type ContainerService struct{} var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>") type IContainerService interface { - Page(req dto.PageContainer) (int64, interface{}, error) + Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error) List() []dto.ContainerOptions ListByImage(imageName string) []dto.ContainerOptions - LoadStatus() (dto.ContainerStatus, error) + LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) ListNetwork() ([]dto.Options, error) PageVolume(req dto.SearchWithPage) (int64, interface{}, error) @@ -80,7 +80,7 @@ type IContainerService interface { ContainerUpdate(req dto.ContainerOperate) error ContainerUpgrade(req dto.ContainerUpgrade) error ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) - ContainerListStats() ([]dto.ContainerListStats, error) + ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error) ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error) LoadResourceLimit() (*dto.ResourceLimit, error) ContainerRename(req dto.ContainerRename) error @@ -112,7 +112,9 @@ func NewIContainerService() IContainerService { return &ContainerService{} } -func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) { +func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error) { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() client, err := docker.NewDockerClient() if err != nil { return 0, nil, err @@ -123,7 +125,7 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro options.Filters = filters.NewArgs() options.Filters.Add("label", req.Filters) } - containers, err := client.ContainerList(context.Background(), options) + containers, err := client.ContainerList(ctx, options) if err != nil { return 0, nil, err } @@ -206,27 +208,32 @@ func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions return options } -func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) { +func (u *ContainerService) LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error) { var data dto.ContainerStatus client, err := docker.NewDockerClient() if err != nil { return data, err } defer client.Close() - c := context.Background() - - images, _ := client.ImageList(c, image.ListOptions{All: true}) - data.ImageCount = len(images) - repo, _ := imageRepoRepo.List() - data.RepoCount = len(repo) - templates, _ := composeRepo.List() - data.ComposeTemplateCount = len(templates) - networks, _ := client.NetworkList(c, network.ListOptions{}) - data.NetworkCount = len(networks) - volumes, _ := client.VolumeList(c, volume.ListOptions{}) - data.VolumeCount = len(volumes.Volumes) - data.ComposeCount = loadComposeCount(client) - containers, _ := client.ContainerList(c, container.ListOptions{All: true}) + c, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + if !containersOnly { + images, _ := client.ImageList(c, image.ListOptions{All: true}) + data.ImageCount = len(images) + repo, _ := imageRepoRepo.List() + data.RepoCount = len(repo) + templates, _ := composeRepo.List() + data.ComposeTemplateCount = len(templates) + networks, _ := client.NetworkList(c, network.ListOptions{}) + data.NetworkCount = len(networks) + volumes, _ := client.VolumeList(c, volume.ListOptions{}) + data.VolumeCount = len(volumes.Volumes) + data.ComposeCount = loadComposeCount(c, client) + } + containers, err := client.ContainerList(c, container.ListOptions{All: true}) + if err != nil { + return data, err + } data.ContainerCount = len(containers) for _, item := range containers { switch item.State { @@ -292,27 +299,67 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera } return data, nil } -func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error) { +func (u *ContainerService) ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error) { client, err := docker.NewDockerClient() if err != nil { return nil, err } defer client.Close() - list, err := client.ContainerList(context.Background(), container.ListOptions{All: true}) + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + options := container.ListOptions{All: true} + if ids != nil { + if len(ids) == 0 { + return []dto.ContainerListStats{}, nil + } + options.Filters = filters.NewArgs() + for _, id := range ids { + options.Filters.Add("id", id) + } + } + list, err := client.ContainerList(ctx, options) if err != nil { return nil, err } + return collectContainerStats(ctx, list, func(ctx context.Context, id string) dto.ContainerListStats { + return loadCpuAndMem(ctx, client, id) + }), nil +} + +// A fixed worker pool bounds Docker stats requests, including for legacy callers +// that request all containers. Stopped containers do not need a stats sample. +func collectContainerStats(ctx context.Context, list []container.Summary, load func(context.Context, string) dto.ContainerListStats) []dto.ContainerListStats { datas := make([]dto.ContainerListStats, len(list)) + for i, item := range list { + datas[i].ContainerID = item.ID + } + jobs := make(chan int) var wg sync.WaitGroup - wg.Add(len(list)) - for i := 0; i < len(list); i++ { - go func(index int, item container.Summary) { - datas[index] = loadCpuAndMem(client, item.ID) - wg.Done() - }(i, list[i]) + for worker := 0; worker < min(8, len(list)); worker++ { + wg.Add(1) + go func() { + defer wg.Done() + for index := range jobs { + if ctx.Err() != nil || list[index].State != "running" { + continue + } + sampleCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + datas[index] = load(sampleCtx, list[index].ID) + cancel() + } + }() } +dispatch: + for index := range list { + select { + case <-ctx.Done(): + break dispatch + case jobs <- index: + } + } + close(jobs) wg.Wait() - return datas, nil + return datas } func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) { @@ -1715,11 +1762,11 @@ func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo return selected } -func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats { +func loadCpuAndMem(ctx context.Context, client *client.Client, containerItem string) dto.ContainerListStats { data := dto.ContainerListStats{ ContainerID: containerItem, } - res, err := client.ContainerStats(context.Background(), containerItem, false) + res, err := client.ContainerStats(ctx, containerItem, false) if err != nil { return data } @@ -1942,11 +1989,11 @@ func transPortToStr(ports []container.Port) []string { return docker.SimplifyPorts(ports) } -func loadComposeCount(client *client.Client) int { +func loadComposeCount(ctx context.Context, client *client.Client) int { options := container.ListOptions{All: true} options.Filters = filters.NewArgs() options.Filters.Add("label", composeProjectLabel) - list, err := client.ContainerList(context.Background(), options) + list, err := client.ContainerList(ctx, options) if err != nil { return 0 } diff --git a/frontend/src/api/modules/container.ts b/frontend/src/api/modules/container.ts index e5176cfa515a..793d4bc32325 100644 --- a/frontend/src/api/modules/container.ts +++ b/frontend/src/api/modules/container.ts @@ -3,13 +3,12 @@ import { ResPage, SearchWithPage } from '../interface'; import { Container } from '../interface/container'; import { TimeoutEnum } from '@/enums/http-enum'; -export const searchContainer = (params: Container.ContainerSearch, currentNode?: string) => { - return http.post>( - `/containers/search`, - params, - TimeoutEnum.T_40S, - currentNode ? { CurrentNode: currentNode } : undefined, - ); +export const searchContainer = (params: Container.ContainerSearch, currentNode?: string, signal?: AbortSignal) => { + return http.postWithConfig>(`/containers/search`, params, { + timeout: TimeoutEnum.T_40S, + headers: currentNode ? { CurrentNode: currentNode } : undefined, + signal, + }); }; export const listContainer = () => { return http.post>(`/containers/list`, {}); @@ -44,12 +43,11 @@ export const downloadContainerFile = (params: { containerID: string; path: strin timeout: TimeoutEnum.T_40S, }); }; -export const loadContainerStatus = (currentNode?: string) => { - return http.get( - `/containers/status`, - {}, - currentNode ? { headers: { CurrentNode: currentNode } } : {}, - ); +export const loadContainerStatus = (currentNode?: string, containersOnly = false, signal?: AbortSignal) => { + return http.get(`/containers/status`, containersOnly ? { containersOnly: true } : {}, { + headers: currentNode ? { CurrentNode: currentNode } : undefined, + signal, + }); }; export const loadResourceLimit = () => { return http.get(`/containers/limit`); @@ -90,12 +88,11 @@ export const containerItemStats = (containerID: string, currentNode?: string) => currentNode ? { CurrentNode: currentNode } : undefined, ); }; -export const containerListStats = (currentNode?: string) => { - return http.get>( - `/containers/list/stats`, - {}, - currentNode ? { headers: { CurrentNode: currentNode } } : {}, - ); +export const containerListStats = (currentNode?: string, ids?: string[], signal?: AbortSignal) => { + return http.get>(`/containers/list/stats`, ids ? { ids: ids.join(',') } : {}, { + headers: currentNode ? { CurrentNode: currentNode } : undefined, + signal, + }); }; export const containerStats = (id: string) => { return http.get(`/containers/stats/${id}`); diff --git a/frontend/src/views/container/container/index.vue b/frontend/src/views/container/container/index.vue index e0dbc46df528..0c0b83807121 100644 --- a/frontend/src/views/container/container/index.vue +++ b/frontend/src/views/container/container/index.vue @@ -488,7 +488,7 @@ import Uploads from '@/components/upload/index.vue'; import DockerStatus from '@/views/container/docker-status/index.vue'; import ContainerLogDialog from '@/components/log/container-drawer/index.vue'; import Status from '@/components/status/index.vue'; -import { computed, reactive, ref } from 'vue'; +import { computed, onBeforeUnmount, reactive, ref } from 'vue'; import { containerItemStats, containerListStats, @@ -700,47 +700,87 @@ const dialogContainerLogRef = ref(); const dialogRenameRef = ref(); const dialogPruneRef = ref(); -const search = async (column?: any) => { - if (!isActive.value || !isExist.value) { +let requestController: AbortController | undefined; +let requestVersion = 0; +let refreshPending = false; + +onBeforeUnmount(() => { + requestVersion++; + requestController?.abort(); +}); + +const loadContainers = async (background = false, column?: any) => { + if (!isActive.value || !isExist.value || (background && refreshPending)) { return; } + requestController?.abort(); + const controller = new AbortController(); + requestController = controller; + const version = ++requestVersion; + const isCurrent = () => version === requestVersion && !controller.signal.aborted; + refreshPending = true; + loading.value = !background; localStorage.setItem('includeAppStore', includeAppStore.value ? 'true' : 'false'); - let filterItem = (router.currentRoute.value.query?.filters as string) || ''; paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy; paginationConfig.order = column?.order ? column.order : paginationConfig.order; - let params = { + const params = { name: searchName.value, state: paginationConfig.state || 'all', page: paginationConfig.currentPage, pageSize: paginationConfig.pageSize, - filters: filterItem, + filters: props.filters || (router.currentRoute.value.query?.filters as string) || '', orderBy: paginationConfig.orderBy, order: paginationConfig.order, excludeAppStore: !includeAppStore.value, }; - loading.value = true; - const [containerResult, statsResult, statusResult] = await Promise.allSettled([ - searchContainer(params), - containerListStats(), - loadContainerStatus(), - ]); - loading.value = false; - - if (containerResult.status === 'fulfilled') { - const containers = containerResult.value.data.items || []; + // Counts and resource samples must never hold up the table itself. + const statusRequest = loadContainerStatus(undefined, true, controller.signal) + .then((res) => { + if (isCurrent()) { + updateTags(res.data || {}); + } + }) + .catch(() => {}); + try { + const res = await searchContainer(params, undefined, controller.signal); + if (!isCurrent()) { + return; + } + const containers = res.data.items || []; syncContainerRows(containers); - paginationConfig.total = containerResult.value.data.total; - } - - if (statsResult.status === 'fulfilled') { - applyStatsToRows(statsResult.value.data || []); - } - - if (statusResult.status === 'fulfilled') { - updateTags(statusResult.value.data || {}); + paginationConfig.total = res.data.total; + loading.value = false; + // Clear old usage when a previously running container has stopped. + applyStatsToRows( + containers + .filter((item) => item.state !== 'running') + .map((item) => ({ + containerID: item.containerID, + ...Object.fromEntries(statFields.map((field) => [field, 0])), + })), + ); + const ids = containers.filter((item) => item.state === 'running').map((item) => item.containerID); + if (ids.length > 0) { + const stats = await containerListStats(undefined, ids, controller.signal); + if (isCurrent()) { + applyStatsToRows(stats.data || []); + } + } + } catch { + // The HTTP layer reports failures; keep the last successful rows visible. + } finally { + if (isCurrent()) { + loading.value = false; + } + await statusRequest; + if (isCurrent()) { + refreshPending = false; + } } }; +const search = (column?: any) => loadContainers(false, column); + const searchWithStatus = (item: string) => { activeTag.value = item; paginationConfig.state = activeTag.value; @@ -769,21 +809,7 @@ const changePinned = (row: any, isPinned: boolean) => { }); }; -const refresh = async () => { - let filterItem = props.filters ? props.filters : ''; - let params = { - name: searchName.value, - state: paginationConfig.state || 'all', - page: paginationConfig.currentPage, - pageSize: paginationConfig.pageSize, - filters: filterItem, - orderBy: paginationConfig.orderBy, - order: paginationConfig.order, - }; - const [containerResult, statsResult] = await Promise.all([searchContainer(params), containerListStats()]); - syncContainerRows(containerResult.data.items || []); - applyStatsToRows(statsResult.data || []); -}; +const refresh = () => loadContainers(true); const loadSize = async (row: any) => { containerItemStats(row.containerID).then((res) => { From 0bfde3dab89d67f8dcd163839695c86bf5b05102 Mon Sep 17 00:00:00 2001 From: Yuki Guo Date: Thu, 8 Oct 2026 11:08:04 +0800 Subject: [PATCH 2/9] fix: fix operation log (#13989) --- agent/app/api/v2/runtime.go | 2 +- agent/cmd/server/docs/x-log.json | 17 +++++++++++++---- core/cmd/server/docs/x-log.json | 17 +++++++++++++---- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/agent/app/api/v2/runtime.go b/agent/app/api/v2/runtime.go index fbe22a9e4a9b..64c8b3b22805 100644 --- a/agent/app/api/v2/runtime.go +++ b/agent/app/api/v2/runtime.go @@ -169,7 +169,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) { // @Security ApiKeyAuth // @Security Timestamp // @Router /runtimes/operate [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [id]","formatEN":"Operate runtime [id]"} +// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"runtimes","output_column":"name","output_value":"name"}],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"} func (b *BaseApi) OperateRuntime(c *gin.Context) { var req request.RuntimeOperate if err := helper.CheckBindAndValidate(&req, c); err != nil { diff --git a/agent/cmd/server/docs/x-log.json b/agent/cmd/server/docs/x-log.json index 5b1d13eb02d1..a62cd236f8d1 100644 --- a/agent/cmd/server/docs/x-log.json +++ b/agent/cmd/server/docs/x-log.json @@ -4039,12 +4039,21 @@ }, "/runtimes/operate": { "bodyKeys": [ - "id" + "ID" ], "paramKeys": [], - "beforeFunctions": [], - "formatZH": "操作运行环境 [id]", - "formatEN": "Operate runtime [id]" + "beforeFunctions": [ + { + "input_column": "id", + "input_value": "ID", + "isList": false, + "db": "runtimes", + "output_column": "name", + "output_value": "name" + } + ], + "formatZH": "操作运行环境 [name]", + "formatEN": "Operate runtime [name]" }, "/runtimes/php/config": { "bodyKeys": [ diff --git a/core/cmd/server/docs/x-log.json b/core/cmd/server/docs/x-log.json index 5b1d13eb02d1..a62cd236f8d1 100644 --- a/core/cmd/server/docs/x-log.json +++ b/core/cmd/server/docs/x-log.json @@ -4039,12 +4039,21 @@ }, "/runtimes/operate": { "bodyKeys": [ - "id" + "ID" ], "paramKeys": [], - "beforeFunctions": [], - "formatZH": "操作运行环境 [id]", - "formatEN": "Operate runtime [id]" + "beforeFunctions": [ + { + "input_column": "id", + "input_value": "ID", + "isList": false, + "db": "runtimes", + "output_column": "name", + "output_value": "name" + } + ], + "formatZH": "操作运行环境 [name]", + "formatEN": "Operate runtime [name]" }, "/runtimes/php/config": { "bodyKeys": [ From 6e44a985afe9a96c9b67a29b23d9a867583ce26c Mon Sep 17 00:00:00 2001 From: Yuki Guo Date: Thu, 8 Oct 2026 11:32:09 +0800 Subject: [PATCH 3/9] fix: fix log text (#13990) --- agent/app/task/task.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/app/task/task.go b/agent/app/task/task.go index 5fa01d58036a..2425d80c7729 100644 --- a/agent/app/task/task.go +++ b/agent/app/task/task.go @@ -154,7 +154,7 @@ func NewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task, e logPath := path.Join(global.Dir.TaskDir, taskScope, taskID+".log") logger := logrus.New() logger.SetFormatter(&SimpleFormatter{}) - logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, constant.FilePerm) + logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, constant.FilePerm) if err != nil { return nil, fmt.Errorf("failed to open log file: %w", err) } From 3c5282931f9d44a019615b6780c4ef5a1c4330c3 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Thu, 8 Oct 2026 14:06:40 +0800 Subject: [PATCH 4/9] fix(ssh): validate imported key pairs and cancel stale file reads (#13991) --- agent/app/api/v2/ssh.go | 2 + agent/app/dto/ssh.go | 2 +- agent/app/service/ssh.go | 83 ++++++-- agent/i18n/lang/en.yaml | 4 + agent/i18n/lang/es-ES.yaml | 4 + agent/i18n/lang/fa.yaml | 4 + agent/i18n/lang/ja.yaml | 4 + agent/i18n/lang/ko.yaml | 4 + agent/i18n/lang/lo.yaml | 4 + agent/i18n/lang/ms.yaml | 4 + agent/i18n/lang/pt-BR.yaml | 4 + agent/i18n/lang/ru.yaml | 4 + agent/i18n/lang/tr.yaml | 4 + agent/i18n/lang/zh-Hant.yaml | 4 + agent/i18n/lang/zh.yaml | 4 + frontend/src/lang/modules/en.ts | 3 + frontend/src/lang/modules/es-es.ts | 3 + frontend/src/lang/modules/fa.ts | 3 + frontend/src/lang/modules/ja.ts | 3 + frontend/src/lang/modules/ko.ts | 3 + frontend/src/lang/modules/lo.ts | 3 + frontend/src/lang/modules/ms.ts | 3 + frontend/src/lang/modules/pt-br.ts | 3 + frontend/src/lang/modules/ru.ts | 3 + frontend/src/lang/modules/tr.ts | 3 + frontend/src/lang/modules/zh-Hant.ts | 2 + frontend/src/lang/modules/zh.ts | 2 + .../ssh/ssh/certification/operate/index.vue | 178 ++++++++++++------ 28 files changed, 280 insertions(+), 67 deletions(-) diff --git a/agent/app/api/v2/ssh.go b/agent/app/api/v2/ssh.go index 6dbd1011a6d7..5f6e1d78fd98 100644 --- a/agent/app/api/v2/ssh.go +++ b/agent/app/api/v2/ssh.go @@ -83,6 +83,7 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) { } if err := loadCertAfterDecrypt(&req); err != nil { helper.BadRequest(c, err) + return } if err := sshService.CreateRootCert(req); err != nil { helper.InternalServer(c, err) @@ -107,6 +108,7 @@ func (b *BaseApi) EditRootCert(c *gin.Context) { } if err := loadCertAfterDecrypt(&req); err != nil { helper.BadRequest(c, err) + return } if err := sshService.EditRootCert(req); err != nil { helper.InternalServer(c, err) diff --git a/agent/app/dto/ssh.go b/agent/app/dto/ssh.go index de965539f06c..4b07386f9fd7 100644 --- a/agent/app/dto/ssh.go +++ b/agent/app/dto/ssh.go @@ -25,7 +25,7 @@ type RootCertOperate struct { ID uint `json:"id"` Name string `json:"name"` Mode string `json:"mode"` - EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"` + EncryptionMode string `json:"encryptionMode"` PassPhrase string `json:"passPhrase"` PublicKey string `json:"publicKey"` PrivateKey string `json:"privateKey"` diff --git a/agent/app/service/ssh.go b/agent/app/service/ssh.go index d69ef9dfefe4..2f970d719fdf 100644 --- a/agent/app/service/ssh.go +++ b/agent/app/service/ssh.go @@ -7,6 +7,7 @@ import ( "context" "encoding/base64" "encoding/json" + "encoding/pem" "fmt" "io" "net" @@ -37,6 +38,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/re" "github.com/pkg/errors" + "golang.org/x/crypto/ssh" ) const sshPath = "/etc/ssh/sshd_config" @@ -438,15 +440,33 @@ func (u *SSHService) SyncRootCert() error { global.LOG.Errorf("read pubic key of %s for sync failed, err: %v", item, err) continue } - cert.EncryptionMode = loadEncryptioMode(string(pubItem)) + publicKey, _, _, rest, err := ssh.ParseAuthorizedKey(pubItem) + if err != nil || len(bytes.TrimSpace(rest)) != 0 { + continue + } + cert.EncryptionMode = loadSSHKeyType(publicKey) + if cert.EncryptionMode == "" { + continue + } rootCerts = append(rootCerts, cert) } return hostRepo.SyncCert(rootCerts) } func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error { - if cmd.CheckIllegal(req.EncryptionMode, req.PassPhrase) { - return buserr.New("ErrCmdIllegal") + switch req.Mode { + case "generate": + switch req.EncryptionMode { + case "rsa", "ed25519", "ecdsa", "dsa": + default: + return buserr.WithName("ErrNotSupportType", req.EncryptionMode) + } + case "input", "import": + if err := validateSSHKeyPair(&req); err != nil { + return err + } + default: + return buserr.WithName("ErrNotSupportType", req.Mode) } certItem, _ := hostRepo.GetCert(repo.WithByName(req.Name)) if certItem.ID != 0 { @@ -543,6 +563,9 @@ func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error { } func (u *SSHService) EditRootCert(req dto.RootCertOperate) error { + if err := validateSSHKeyPair(&req); err != nil { + return err + } currentUser, err := user.Current() if err != nil { return fmt.Errorf("load current user failed, err: %v", err) @@ -1681,22 +1704,60 @@ func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time { return itemDate } -func loadEncryptioMode(content string) string { - if strings.HasPrefix(content, "ssh-rsa") { +func loadSSHKeyType(publicKey ssh.PublicKey) string { + switch publicKey.Type() { + case ssh.KeyAlgoRSA: return "rsa" - } - if strings.HasPrefix(content, "ssh-ed25519") { + case ssh.KeyAlgoED25519: return "ed25519" - } - if strings.HasPrefix(content, "ssh-ecdsa") { + case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521: return "ecdsa" - } - if strings.HasPrefix(content, "ssh-dsa") { + case ssh.KeyAlgoDSA: return "dsa" } return "" } +func validateSSHKeyPair(req *dto.RootCertOperate) error { + req.PublicKey = strings.TrimSpace(req.PublicKey) + req.PrivateKey = strings.TrimSpace(req.PrivateKey) + if req.PublicKey == "" || strings.ContainsAny(req.PublicKey, "\r\n") { + return buserr.New("ErrSSHPublicKey") + } + publicKey, comment, options, rest, err := ssh.ParseAuthorizedKey([]byte(req.PublicKey)) + if err != nil || len(options) != 0 || len(bytes.TrimSpace(rest)) != 0 { + return buserr.New("ErrSSHPublicKey") + } + req.EncryptionMode = loadSSHKeyType(publicKey) + if req.EncryptionMode == "" { + return buserr.WithName("ErrNotSupportType", publicKey.Type()) + } + block, rest := pem.Decode([]byte(req.PrivateKey)) + if !strings.HasPrefix(req.PrivateKey, "-----BEGIN ") || block == nil || len(bytes.TrimSpace(rest)) != 0 { + return buserr.New("ErrSSHPrivateKey") + } + signer, err := ssh.ParsePrivateKey([]byte(req.PrivateKey)) + var missingPassphrase *ssh.PassphraseMissingError + if errors.As(err, &missingPassphrase) { + signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(req.PrivateKey), []byte(req.PassPhrase)) + } else if err == nil { + req.PassPhrase = "" + } + if err != nil { + return buserr.New("ErrSSHPrivateKey") + } + if !bytes.Equal(signer.PublicKey().Marshal(), publicKey.Marshal()) { + return buserr.New("ErrSSHKeyMismatch") + } + req.PublicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey))) + if comment != "" { + req.PublicKey += " " + comment + } + req.PublicKey += "\n" + req.PrivateKey += "\n" + return nil +} + func updateLocalConn(newPort uint) error { conn, _ := settingRepo.GetValueByKey("LocalSSHConn") if len(conn) == 0 { diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index e208725c915c..0c6dad9adb5d 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -731,3 +731,7 @@ FileTaskRename: 'Target filename: {{ .name }}' ErrFirewallBackendCleanupRequired: "The current backend {{ .current }} still contains 1Panel rules. Clean it up before switching to {{ .target }}." ErrDockerIPv4ForwardingDisabled: "IPv4 forwarding is disabled. Set net.ipv4.ip_forward=1 before using Docker's firewall backend." ErrFirewallRuleSavedApplyFailed: "The new rule configuration was saved, but applying it to the firewall failed. Retry by synchronizing: {{ .detail }}" + +ErrSSHPublicKey: "Enter a valid SSH public key" +ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase" +ErrSSHKeyMismatch: "The SSH public and private keys do not match" diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 91467f1b0b14..4a4a52204c90 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -731,3 +731,7 @@ FileTaskRename: 'Nombre del archivo de destino: {{ .name }}' ErrFirewallBackendCleanupRequired: "El backend actual {{ .current }} aún contiene reglas de 1Panel. Elimínelas antes de cambiar a {{ .target }}." ErrDockerIPv4ForwardingDisabled: "El reenvío IPv4 está desactivado. Configure net.ipv4.ip_forward=1 antes de usar el backend de cortafuegos de Docker." ErrFirewallRuleSavedApplyFailed: "Se guardó la configuración de la nueva regla, pero no se pudo aplicar al cortafuegos. Vuelva a intentarlo mediante la sincronización: {{ .detail }}" + +ErrSSHPublicKey: "Introduce una clave pública SSH válida" +ErrSSHPrivateKey: "Clave privada SSH no válida o contraseña incorrecta; las claves cifradas requieren la contraseña original" +ErrSSHKeyMismatch: "Las claves pública y privada SSH no coinciden" diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index 78ff0019a4cf..a974edfc3509 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -732,3 +732,7 @@ FileTaskRename: 'نام فایل مقصد: {{ .name }}' ErrFirewallBackendCleanupRequired: "بک‌اند فعلی {{ .current }} هنوز شامل قوانین 1Panel است. پیش از تغییر به {{ .target }} آن‌ها را پاک کنید." ErrDockerIPv4ForwardingDisabled: "ارسال IPv4 غیرفعال است. پیش از استفاده از بک‌اند فایروال Docker، مقدار net.ipv4.ip_forward=1 را تنظیم کنید." ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگام‌سازی دوباره تلاش کنید: {{ .detail }}" + +ErrSSHPublicKey: "یک کلید عمومی SSH معتبر وارد کنید" +ErrSSHPrivateKey: "کلید خصوصی SSH نامعتبر یا عبارت عبور نادرست است؛ کلیدهای رمزگذاری‌شده به عبارت عبور اصلی نیاز دارند" +ErrSSHKeyMismatch: "کلید عمومی و خصوصی SSH مطابقت ندارند" diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 9653bfebbb7c..47d72c8154db 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -731,3 +731,7 @@ FileTaskRename: '保存先ファイル名:{{ .name }}' ErrFirewallBackendCleanupRequired: "現在のバックエンド {{ .current }} に 1Panel ルールが残っています。{{ .target }} に切り替える前に削除してください。" ErrDockerIPv4ForwardingDisabled: "IPv4 転送が無効です。Docker のファイアウォールバックエンドを使用する前に net.ipv4.ip_forward=1 を設定してください。" ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}" + +ErrSSHPublicKey: "有効な SSH 公開鍵を入力してください" +ErrSSHPrivateKey: "SSH 秘密鍵の形式が無効か、パスフレーズが間違っています。暗号化された鍵には元のパスフレーズが必要です" +ErrSSHKeyMismatch: "SSH 公開鍵と秘密鍵が一致しません" diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index bd9aef6128c3..1f72e42e971b 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -731,3 +731,7 @@ FileTaskRename: '대상 파일 이름: {{ .name }}' ErrFirewallBackendCleanupRequired: "현재 백엔드 {{ .current }}에 1Panel 규칙이 남아 있습니다. {{ .target }}로 전환하기 전에 정리하세요." ErrDockerIPv4ForwardingDisabled: "IPv4 전달이 비활성화되어 있습니다. Docker 방화벽 백엔드를 사용하기 전에 net.ipv4.ip_forward=1을 설정하세요." ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}" + +ErrSSHPublicKey: "유효한 SSH 공개 키를 입력하세요" +ErrSSHPrivateKey: "SSH 개인 키 형식이 잘못되었거나 암호가 틀립니다. 암호화된 키에는 원래 암호가 필요합니다" +ErrSSHKeyMismatch: "SSH 공개 키와 개인 키가 일치하지 않습니다" diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml index 1875c8c7f6fb..77fd89f8b16e 100644 --- a/agent/i18n/lang/lo.yaml +++ b/agent/i18n/lang/lo.yaml @@ -722,3 +722,7 @@ FileTaskRename: 'ຊື່ໄຟລ໌ປາຍທາງ: {{ .name }}' ErrFirewallBackendCleanupRequired: "ແບັກເອນປັດຈຸບັນ {{ .current }} ຍັງມີກົດຂອງ 1Panel. ກະລຸນາລຶບອອກກ່ອນປ່ຽນໄປ {{ .target }}." ErrDockerIPv4ForwardingDisabled: "ການສົ່ງຕໍ່ IPv4 ຖືກປິດ. ກະລຸນາຕັ້ງ net.ipv4.ip_forward=1 ກ່ອນໃຊ້ແບັກເອນໄຟວໍຂອງ Docker." ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}" + +ErrSSHPublicKey: "ກະລຸນາປ້ອນຄີສາທາລະນະ SSH ທີ່ຖືກຕ້ອງ" +ErrSSHPrivateKey: "ຄີສ່ວນຕົວ SSH ບໍ່ຖືກຕ້ອງ ຫຼື ລະຫັດຜ່ານຜິດ; ຄີທີ່ເຂົ້າລະຫັດຕ້ອງໃຊ້ລະຫັດຜ່ານເດີມ" +ErrSSHKeyMismatch: "ຄີສາທາລະນະ ແລະ ຄີສ່ວນຕົວ SSH ບໍ່ກົງກັນ" diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index dd261ff9443e..2e90ec6762dc 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -731,3 +731,7 @@ FileTaskRename: 'Nama fail sasaran: {{ .name }}' ErrFirewallBackendCleanupRequired: "Bahagian belakang semasa {{ .current }} masih mengandungi peraturan 1Panel. Buangkannya sebelum beralih kepada {{ .target }}." ErrDockerIPv4ForwardingDisabled: "Pemajuan IPv4 dilumpuhkan. Tetapkan net.ipv4.ip_forward=1 sebelum menggunakan bahagian belakang tembok api Docker." ErrFirewallRuleSavedApplyFailed: "Konfigurasi peraturan baharu telah disimpan, tetapi gagal digunakan pada tembok api. Cuba lagi melalui penyegerakan: {{ .detail }}" + +ErrSSHPublicKey: "Masukkan kunci awam SSH yang sah" +ErrSSHPrivateKey: "Kunci peribadi SSH tidak sah atau frasa laluan salah; kunci yang disulitkan memerlukan frasa laluan asal" +ErrSSHKeyMismatch: "Kunci awam dan peribadi SSH tidak sepadan" diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index a8a2544a769c..9b9d7da92592 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -731,3 +731,7 @@ FileTaskRename: 'Nome do arquivo de destino: {{ .name }}' ErrFirewallBackendCleanupRequired: "O backend atual {{ .current }} ainda contém regras do 1Panel. Remova-as antes de mudar para {{ .target }}." ErrDockerIPv4ForwardingDisabled: "O encaminhamento IPv4 está desativado. Defina net.ipv4.ip_forward=1 antes de usar o backend de firewall do Docker." ErrFirewallRuleSavedApplyFailed: "A configuração da nova regra foi salva, mas não pôde ser aplicada ao firewall. Tente novamente por meio da sincronização: {{ .detail }}" + +ErrSSHPublicKey: "Informe uma chave pública SSH válida" +ErrSSHPrivateKey: "Chave privada SSH inválida ou senha incorreta; chaves criptografadas exigem a senha original" +ErrSSHKeyMismatch: "As chaves pública e privada SSH não correspondem" diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 89ed5305ee1c..ad6838b3143e 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -731,3 +731,7 @@ FileTaskRename: 'Имя целевого файла: {{ .name }}' ErrFirewallBackendCleanupRequired: "В текущем бэкенде {{ .current }} остались правила 1Panel. Удалите их перед переключением на {{ .target }}." ErrDockerIPv4ForwardingDisabled: "Пересылка IPv4 отключена. Перед использованием бэкенда межсетевого экрана Docker установите net.ipv4.ip_forward=1." ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}" + +ErrSSHPublicKey: "Введите действительный открытый ключ SSH" +ErrSSHPrivateKey: "Недопустимый закрытый ключ SSH или неверная парольная фраза; для зашифрованных ключей требуется исходная парольная фраза" +ErrSSHKeyMismatch: "Открытый и закрытый ключи SSH не соответствуют друг другу" diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index ca814031479b..adf567a5bc5b 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -731,3 +731,7 @@ FileTaskRename: 'Hedef dosya adı: {{ .name }}' ErrFirewallBackendCleanupRequired: "Mevcut {{ .current }} arka ucunda hâlâ 1Panel kuralları var. {{ .target }} arka ucuna geçmeden önce bunları temizleyin." ErrDockerIPv4ForwardingDisabled: "IPv4 yönlendirmesi devre dışı. Docker güvenlik duvarı arka ucunu kullanmadan önce net.ipv4.ip_forward=1 ayarını yapın." ErrFirewallRuleSavedApplyFailed: "Yeni kural yapılandırması kaydedildi, ancak güvenlik duvarına uygulanamadı. Eşitleme yaparak yeniden deneyin: {{ .detail }}" + +ErrSSHPublicKey: "Geçerli bir SSH açık anahtarı girin" +ErrSSHPrivateKey: "SSH özel anahtarı geçersiz veya parola yanlış; şifreli anahtarlar mevcut parolayı gerektirir" +ErrSSHKeyMismatch: "SSH açık ve özel anahtarları eşleşmiyor" diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index ae7d9b747ccd..79d1900760b4 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -731,3 +731,7 @@ FileTaskRename: '目標檔名:{{ .name }}' ErrFirewallBackendCleanupRequired: "目前後端 {{ .current }} 中仍有 1Panel 規則,請先清理後再切換至 {{ .target }}。" ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv4.ip_forward=1,再使用 Docker 防火牆後端。" ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}" + +ErrSSHPublicKey: "請輸入有效的 SSH 公鑰" +ErrSSHPrivateKey: "SSH 私鑰格式無效或密碼錯誤;加密私鑰需要填寫原密碼" +ErrSSHKeyMismatch: "SSH 公鑰與私鑰不相符" diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 00852883dfd6..4a7ef3db242f 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -731,3 +731,7 @@ FileTaskRename: '目标文件名:{{ .name }}' ErrFirewallBackendCleanupRequired: "当前后端 {{ .current }} 中仍有 1Panel 规则,请先清理后再切换到 {{ .target }}。" ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.ip_forward=1,再使用 Docker 防火墙后端。" ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}" + +ErrSSHPublicKey: "请输入有效的 SSH 公钥" +ErrSSHPrivateKey: "SSH 私钥格式无效或密码错误;加密私钥需要填写原密码" +ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配" diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index f2a43c34a983..14b75ce5fad2 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -2888,6 +2888,9 @@ const message = { privateKey: 'Private Key', publicKey: 'Public Key', password: 'Password', + existingPassPhrase: 'Existing private key passphrase', + existingPassPhraseHelper: + 'Enter the original passphrase if the private key is encrypted; otherwise leave blank. This does not change the passphrase.', createMode: 'Creation Method', generate: 'Auto-generate', unSyncPass: 'Key password cannot be synchronized', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index cdabe72a42e1..4be0207ce370 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -2930,6 +2930,9 @@ const message = { privateKey: 'Clave privada', publicKey: 'Clave pública', password: 'Contraseña', + existingPassPhrase: 'Contraseña actual de la clave privada', + existingPassPhraseHelper: + 'Introduce la contraseña original si la clave privada está cifrada; en caso contrario, deja el campo vacío. Esto no cambia la contraseña.', createMode: 'Método de creación', generate: 'Autogenerar', unSyncPass: 'La contraseña de la clave no se puede sincronizar', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index d47aecd2ac99..1ef3bc82a2a1 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -2861,6 +2861,9 @@ const message = { privateKey: 'کلید خصوصی', publicKey: 'کلید عمومی', password: 'رمز عبور', + existingPassPhrase: 'عبارت عبور فعلی کلید خصوصی', + existingPassPhraseHelper: + 'اگر کلید خصوصی رمزگذاری شده است، عبارت عبور اصلی را وارد کنید؛ در غیر این صورت خالی بگذارید. این کار عبارت عبور را تغییر نمی‌دهد.', createMode: 'روش ایجاد', generate: 'تولید خودکار', unSyncPass: 'رمز عبور کلید قابل همگام‌سازی نیست', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 819866e7a430..ba3336f960c6 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -2865,6 +2865,9 @@ const message = { privateKey: '秘密鍵', publicKey: '公開鍵', password: 'パスワード', + existingPassPhrase: '既存の秘密鍵のパスフレーズ', + existingPassPhraseHelper: + '秘密鍵が暗号化されている場合は元のパスフレーズを入力し、それ以外は空欄にしてください。パスフレーズは変更されません。', createMode: '作成方法', generate: '自動生成', unSyncPass: '鍵パスワードは同期できません', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 22cf61f590c8..24725ebe716e 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -2827,6 +2827,9 @@ const message = { privateKey: '개인 키', publicKey: '공개 키', password: '비밀번호', + existingPassPhrase: '기존 개인 키 암호', + existingPassPhraseHelper: + '개인 키가 암호화되어 있으면 원래 암호를 입력하고, 그렇지 않으면 비워 두세요. 이 작업은 암호를 변경하지 않습니다.', createMode: '생성 방식', generate: '자동 생성', unSyncPass: '키 비밀번호 동기화 불가', diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 12e16868aefb..40038f6bdf4c 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -2807,6 +2807,9 @@ const message = { privateKey: 'ຄີສ່ວນຕົວ', publicKey: 'ຄີສາທາລະນະ', password: 'ລະຫັດຜ່ານ', + existingPassPhrase: 'ລະຫັດຜ່ານເດີມຂອງຄີສ່ວນຕົວ', + existingPassPhraseHelper: + 'ຖ້າຄີສ່ວນຕົວຖືກເຂົ້າລະຫັດ ໃຫ້ປ້ອນລະຫັດຜ່ານເດີມ; ຖ້າບໍ່ ໃຫ້ປະໄວ້ຫວ່າງ. ການດຳເນີນການນີ້ບໍ່ປ່ຽນລະຫັດຜ່ານ.', createMode: 'ວິທີການສ້າງ', generate: 'ສ້າງອັດຕະໂນມັດ', unSyncPass: 'ບໍ່ສາມາດຊິງຄ໌ລະຫັດຜ່ານຄີໄດ້', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 2a6054108834..ae6b54ebe163 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -2930,6 +2930,9 @@ const message = { privateKey: 'Kunci Persendirian', publicKey: 'Kunci Awam', password: 'Kata Laluan', + existingPassPhrase: 'Frasa laluan kunci peribadi sedia ada', + existingPassPhraseHelper: + 'Masukkan frasa laluan asal jika kunci peribadi disulitkan; jika tidak, biarkan kosong. Ini tidak mengubah frasa laluan.', createMode: 'Kaedah Penciptaan', generate: 'Jana Automatik', unSyncPass: 'Kata laluan kunci tidak dapat diselaraskan', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index fd5a60def20e..fc4d6a39f0ba 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -2929,6 +2929,9 @@ const message = { privateKey: 'Chave Privada', publicKey: 'Chave Pública', password: 'Senha', + existingPassPhrase: 'Senha atual da chave privada', + existingPassPhraseHelper: + 'Informe a senha original se a chave privada estiver criptografada; caso contrário, deixe em branco. Isso não altera a senha.', createMode: 'Método de Criação', generate: 'Gerar Automaticamente', unSyncPass: 'Senha da chave não pode ser sincronizada', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index f7b45cba4cf2..feef1be0211d 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -2903,6 +2903,9 @@ const message = { privateKey: 'Приватный ключ', publicKey: 'Публичный ключ', password: 'Пароль', + existingPassPhrase: 'Текущая парольная фраза закрытого ключа', + existingPassPhraseHelper: + 'Если закрытый ключ зашифрован, введите исходную парольную фразу; иначе оставьте поле пустым. Это не изменяет парольную фразу.', createMode: 'Способ создания', generate: 'Автогенерация', unSyncPass: 'Пароль ключа не может быть синхронизирован', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 965ebd93704e..a8f2887fa0a3 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -2921,6 +2921,9 @@ const message = { privateKey: 'Özel Anahtar', publicKey: 'Genel Anahtar', password: 'Parola', + existingPassPhrase: 'Mevcut özel anahtar parolası', + existingPassPhraseHelper: + 'Özel anahtar şifreliyse mevcut parolayı girin; değilse boş bırakın. Bu işlem parolayı değiştirmez.', createMode: 'Oluşturma Yöntemi', generate: 'Otomatik Oluştur', unSyncPass: 'Anahtar parolası senkronize edilemez', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 537457e7fc2f..216b070a49b8 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -2720,6 +2720,8 @@ const message = { privateKey: '私鑰', publicKey: '公鑰', password: '密碼', + existingPassPhrase: '現有私鑰密碼', + existingPassPhraseHelper: '私鑰已加密時填寫原密碼,未加密則留空;此處不會修改私鑰密碼。', createMode: '建立方式', generate: '自動生成', unSyncPass: '金鑰密碼無法同步', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 8fc0dafc6d10..dd0e77f5ce26 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -2751,6 +2751,8 @@ const message = { privateKey: '私钥', publicKey: '公钥', password: '密码', + existingPassPhrase: '已有私钥密码', + existingPassPhraseHelper: '私钥已加密时填写原密码,未加密则留空;此处不会修改私钥密码。', createMode: '创建方式', generate: '自动生成', unSyncPass: '密钥密码无法同步', diff --git a/frontend/src/views/host/ssh/ssh/certification/operate/index.vue b/frontend/src/views/host/ssh/ssh/certification/operate/index.vue index 097a60a2cbbc..5a28ac7eae15 100644 --- a/frontend/src/views/host/ssh/ssh/certification/operate/index.vue +++ b/frontend/src/views/host/ssh/ssh/certification/operate/index.vue @@ -16,8 +16,15 @@ + + + {{ $t('ssh.generate') }} + {{ $t('ssh.input') }} + {{ $t('ssh.import') }} + + - + @@ -28,50 +35,38 @@ - + -