mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 06:47:20 +08:00
feat: ssh管理, close #350
This commit is contained in:
@@ -118,6 +118,7 @@ func initWeb() (*app.Web, error) {
|
||||
systemctlService := service.NewSystemctlService(locale)
|
||||
toolboxSystemService := service.NewToolboxSystemService(locale)
|
||||
toolboxBenchmarkService := service.NewToolboxBenchmarkService(locale)
|
||||
toolboxSSHService := service.NewToolboxSSHService(locale)
|
||||
webHookRepo := data.NewWebHookRepo(locale, db)
|
||||
webHookService := service.NewWebHookService(webHookRepo)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
@@ -141,7 +142,7 @@ func initWeb() (*app.Web, error) {
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, webHookService, loader)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, webHookService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo)
|
||||
ws := route.NewWs(wsService)
|
||||
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
|
||||
|
||||
7
go.sum
7
go.sum
@@ -118,6 +118,8 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
@@ -267,6 +269,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
@@ -375,6 +378,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -447,6 +452,8 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
|
||||
26
internal/http/request/toolbox_ssh.go
Normal file
26
internal/http/request/toolbox_ssh.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package request
|
||||
|
||||
// ToolboxSSHPort SSH 端口设置
|
||||
type ToolboxSSHPort struct {
|
||||
Port uint `form:"port" json:"port" validate:"required|min:1|max:65535"`
|
||||
}
|
||||
|
||||
// ToolboxSSHPasswordAuth SSH 密码认证设置
|
||||
type ToolboxSSHPasswordAuth struct {
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
}
|
||||
|
||||
// ToolboxSSHPubKeyAuth SSH 密钥认证设置
|
||||
type ToolboxSSHPubKeyAuth struct {
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
}
|
||||
|
||||
// ToolboxSSHRootLogin Root 登录设置
|
||||
type ToolboxSSHRootLogin struct {
|
||||
Mode string `form:"mode" json:"mode" validate:"required|in:yes,no,without-password,prohibit-password"`
|
||||
}
|
||||
|
||||
// ToolboxSSHRootPassword Root 密码设置
|
||||
type ToolboxSSHRootPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
}
|
||||
@@ -48,6 +48,7 @@ type Http struct {
|
||||
systemctl *service.SystemctlService
|
||||
toolboxSystem *service.ToolboxSystemService
|
||||
toolboxBenchmark *service.ToolboxBenchmarkService
|
||||
toolboxSSH *service.ToolboxSSHService
|
||||
webhook *service.WebHookService
|
||||
apps *apploader.Loader
|
||||
}
|
||||
@@ -85,6 +86,7 @@ func NewHttp(
|
||||
systemctl *service.SystemctlService,
|
||||
toolboxSystem *service.ToolboxSystemService,
|
||||
toolboxBenchmark *service.ToolboxBenchmarkService,
|
||||
toolboxSSH *service.ToolboxSSHService,
|
||||
webhook *service.WebHookService,
|
||||
apps *apploader.Loader,
|
||||
) *Http {
|
||||
@@ -121,6 +123,7 @@ func NewHttp(
|
||||
systemctl: systemctl,
|
||||
toolboxSystem: toolboxSystem,
|
||||
toolboxBenchmark: toolboxBenchmark,
|
||||
toolboxSSH: toolboxSSH,
|
||||
webhook: webhook,
|
||||
apps: apps,
|
||||
}
|
||||
@@ -438,13 +441,26 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Post("/hostname", route.toolboxSystem.UpdateHostname)
|
||||
r.Get("/hosts", route.toolboxSystem.GetHosts)
|
||||
r.Post("/hosts", route.toolboxSystem.UpdateHosts)
|
||||
r.Post("/root_password", route.toolboxSystem.UpdateRootPassword)
|
||||
})
|
||||
|
||||
r.Route("/toolbox_benchmark", func(r chi.Router) {
|
||||
r.Post("/test", route.toolboxBenchmark.Test)
|
||||
})
|
||||
|
||||
r.Route("/toolbox_ssh", func(r chi.Router) {
|
||||
r.Get("/info", route.toolboxSSH.GetInfo)
|
||||
r.Post("/start", route.toolboxSSH.Start)
|
||||
r.Post("/stop", route.toolboxSSH.Stop)
|
||||
r.Post("/restart", route.toolboxSSH.Restart)
|
||||
r.Post("/port", route.toolboxSSH.UpdatePort)
|
||||
r.Post("/password_auth", route.toolboxSSH.UpdatePasswordAuth)
|
||||
r.Post("/pubkey_auth", route.toolboxSSH.UpdatePubKeyAuth)
|
||||
r.Post("/root_login", route.toolboxSSH.UpdateRootLogin)
|
||||
r.Post("/root_password", route.toolboxSSH.UpdateRootPassword)
|
||||
r.Get("/root_key", route.toolboxSSH.GetRootKey)
|
||||
r.Post("/root_key", route.toolboxSSH.GenerateRootKey)
|
||||
})
|
||||
|
||||
r.Route("/webhook", func(r chi.Router) {
|
||||
r.Get("/", route.webhook.List)
|
||||
r.Post("/", route.webhook.Create)
|
||||
|
||||
@@ -37,5 +37,6 @@ var ProviderSet = wire.NewSet(
|
||||
NewWebsiteService,
|
||||
NewToolboxSystemService,
|
||||
NewToolboxBenchmarkService,
|
||||
NewToolboxSSHService,
|
||||
NewWsService,
|
||||
)
|
||||
|
||||
339
internal/service/toolbox_ssh.go
Normal file
339
internal/service/toolbox_ssh.go
Normal file
@@ -0,0 +1,339 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type ToolboxSSHService struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewToolboxSSHService(t *gotext.Locale) *ToolboxSSHService {
|
||||
return &ToolboxSSHService{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfo 获取 SSH 信息
|
||||
func (s *ToolboxSSHService) GetInfo(w http.ResponseWriter, r *http.Request) {
|
||||
// 读取 sshd_config
|
||||
sshdConfig, err := io.Read("/etc/ssh/sshd_config")
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to read sshd_config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 SSH 服务状态
|
||||
status, err := systemctl.Status("sshd")
|
||||
if err != nil {
|
||||
// 尝试 ssh 服务名
|
||||
status, err = systemctl.Status("ssh")
|
||||
if err != nil {
|
||||
status = false
|
||||
}
|
||||
}
|
||||
|
||||
// 解析端口
|
||||
port := 22
|
||||
portMatch := regexp.MustCompile(`(?m)^Port\s+(\d+)`).FindStringSubmatch(sshdConfig)
|
||||
if len(portMatch) >= 2 {
|
||||
port = cast.ToInt(portMatch[1])
|
||||
}
|
||||
|
||||
// 解析密码认证
|
||||
passwordAuth := true
|
||||
passwordAuthMatch := regexp.MustCompile(`(?m)^PasswordAuthentication\s+(\S+)`).FindStringSubmatch(sshdConfig)
|
||||
if len(passwordAuthMatch) >= 2 {
|
||||
passwordAuth = strings.ToLower(passwordAuthMatch[1]) == "yes"
|
||||
}
|
||||
|
||||
// 解析密钥认证
|
||||
pubKeyAuth := true
|
||||
pubKeyAuthMatch := regexp.MustCompile(`(?m)^PubkeyAuthentication\s+(\S+)`).FindStringSubmatch(sshdConfig)
|
||||
if len(pubKeyAuthMatch) >= 2 {
|
||||
pubKeyAuth = strings.ToLower(pubKeyAuthMatch[1]) == "yes"
|
||||
}
|
||||
|
||||
// 解析 Root 登录设置
|
||||
rootLogin := "yes"
|
||||
rootLoginMatch := regexp.MustCompile(`(?m)^PermitRootLogin\s+(\S+)`).FindStringSubmatch(sshdConfig)
|
||||
if len(rootLoginMatch) >= 2 {
|
||||
rootLogin = strings.ToLower(rootLoginMatch[1])
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"status": status,
|
||||
"port": port,
|
||||
"password_auth": passwordAuth,
|
||||
"pubkey_auth": pubKeyAuth,
|
||||
"root_login": rootLogin,
|
||||
})
|
||||
}
|
||||
|
||||
// Start 启动 SSH 服务
|
||||
func (s *ToolboxSSHService) Start(w http.ResponseWriter, r *http.Request) {
|
||||
err := systemctl.Start("sshd")
|
||||
if err != nil {
|
||||
err = systemctl.Start("ssh")
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to start SSH service: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// Stop 停止 SSH 服务
|
||||
func (s *ToolboxSSHService) Stop(w http.ResponseWriter, r *http.Request) {
|
||||
err := systemctl.Stop("sshd")
|
||||
if err != nil {
|
||||
err = systemctl.Stop("ssh")
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to stop SSH service: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// Restart 重启 SSH 服务
|
||||
func (s *ToolboxSSHService) Restart(w http.ResponseWriter, r *http.Request) {
|
||||
err := systemctl.Restart("sshd")
|
||||
if err != nil {
|
||||
err = systemctl.Restart("ssh")
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to restart SSH service: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdatePort 修改 SSH 端口
|
||||
func (s *ToolboxSSHService) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxSSHPort](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.updateSSHConfig("Port", cast.ToString(req.Port)); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to update SSH port: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 重启 SSH 服务
|
||||
if err = s.restartSSH(); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to restart SSH service: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdatePasswordAuth 设置密码认证
|
||||
func (s *ToolboxSSHService) UpdatePasswordAuth(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxSSHPasswordAuth](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
value := "no"
|
||||
if req.Enabled {
|
||||
value = "yes"
|
||||
}
|
||||
|
||||
if err = s.updateSSHConfig("PasswordAuthentication", value); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to update password authentication: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.restartSSH(); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to restart SSH service: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdatePubKeyAuth 设置密钥认证
|
||||
func (s *ToolboxSSHService) UpdatePubKeyAuth(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxSSHPubKeyAuth](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
value := "no"
|
||||
if req.Enabled {
|
||||
value = "yes"
|
||||
}
|
||||
|
||||
if err = s.updateSSHConfig("PubkeyAuthentication", value); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to update pubkey authentication: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.restartSSH(); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to restart SSH service: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdateRootLogin 设置 Root 登录
|
||||
func (s *ToolboxSSHService) UpdateRootLogin(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxSSHRootLogin](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.updateSSHConfig("PermitRootLogin", req.Mode); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to update root login setting: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.restartSSH(); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to restart SSH service: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdateRootPassword 修改 Root 密码
|
||||
func (s *ToolboxSSHService) UpdateRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxSSHRootPassword](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
password := strings.ReplaceAll(req.Password, `'`, `\'`)
|
||||
if _, err = shell.Execf(`yes '%s' | passwd root`, password); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to update root password: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// GetRootKey 获取 Root 私钥
|
||||
func (s *ToolboxSSHService) GetRootKey(w http.ResponseWriter, r *http.Request) {
|
||||
var privateKey string
|
||||
|
||||
// 优先尝试 ed25519 密钥
|
||||
if io.Exists("/root/.ssh/id_ed25519") {
|
||||
privateKey, _ = io.Read("/root/.ssh/id_ed25519")
|
||||
} else if io.Exists("/root/.ssh/id_rsa") {
|
||||
privateKey, _ = io.Read("/root/.ssh/id_rsa")
|
||||
}
|
||||
|
||||
Success(w, strings.TrimSpace(privateKey))
|
||||
}
|
||||
|
||||
// GenerateRootKey 生成 Root 密钥对
|
||||
func (s *ToolboxSSHService) GenerateRootKey(w http.ResponseWriter, r *http.Request) {
|
||||
// 确保 .ssh 目录存在
|
||||
if _, err := shell.Execf("mkdir -p /root/.ssh && chmod 700 /root/.ssh"); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to create .ssh directory: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 优先生成 ED25519 密钥对
|
||||
keyType := "ed25519"
|
||||
if _, err := shell.Execf(`yes 'y' | ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ""`); err != nil {
|
||||
// 不行再生成 RSA 密钥
|
||||
keyType = "rsa"
|
||||
if _, err = shell.Execf(`yes 'y' | ssh-keygen -t rsa -b 4096 -f /root/.ssh/id_rsa -N ""`); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to generate SSH key: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 读取生成的密钥
|
||||
var pubKey, privateKey string
|
||||
var err error
|
||||
if keyType == "ed25519" {
|
||||
pubKey, err = io.Read("/root/.ssh/id_ed25519.pub")
|
||||
if err == nil {
|
||||
privateKey, _ = io.Read("/root/.ssh/id_ed25519")
|
||||
}
|
||||
} else {
|
||||
pubKey, err = io.Read("/root/.ssh/id_rsa.pub")
|
||||
if err == nil {
|
||||
privateKey, _ = io.Read("/root/.ssh/id_rsa")
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to read generated key: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 将公钥添加到 authorized_keys
|
||||
pubKey = strings.TrimSpace(pubKey)
|
||||
privateKey = strings.TrimSpace(privateKey)
|
||||
authorizedKeysPath := "/root/.ssh/authorized_keys"
|
||||
authorizedKeys, _ := io.Read(authorizedKeysPath)
|
||||
|
||||
// 检查公钥是否已存在
|
||||
if !strings.Contains(authorizedKeys, pubKey) {
|
||||
if authorizedKeys != "" && !strings.HasSuffix(authorizedKeys, "\n") {
|
||||
authorizedKeys += "\n"
|
||||
}
|
||||
authorizedKeys += pubKey + "\n"
|
||||
if err = io.Write(authorizedKeysPath, authorizedKeys, 0600); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to update authorized_keys: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_ = s.restartSSH()
|
||||
|
||||
Success(w, privateKey)
|
||||
}
|
||||
|
||||
// updateSSHConfig 更新 SSH 配置项
|
||||
func (s *ToolboxSSHService) updateSSHConfig(key, value string) error {
|
||||
sshdConfig, err := io.Read("/etc/ssh/sshd_config")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查配置项是否存在(包括注释的)
|
||||
configRegex := regexp.MustCompile(`(?m)^#?\s*` + key + `\s+.*$`)
|
||||
|
||||
if configRegex.MatchString(sshdConfig) {
|
||||
// 替换现有配置
|
||||
sshdConfig = configRegex.ReplaceAllString(sshdConfig, key+" "+value)
|
||||
} else {
|
||||
// 添加新配置
|
||||
sshdConfig = sshdConfig + "\n" + key + " " + value + "\n"
|
||||
}
|
||||
|
||||
return io.Write("/etc/ssh/sshd_config", sshdConfig, 0600)
|
||||
}
|
||||
|
||||
// restartSSH 重启 SSH 服务
|
||||
func (s *ToolboxSSHService) restartSSH() error {
|
||||
err := systemctl.Restart("sshd")
|
||||
if err != nil {
|
||||
err = systemctl.Restart("ssh")
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -305,20 +305,3 @@ func (s *ToolboxSystemService) UpdateHosts(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdateRootPassword 设置 root 密码
|
||||
func (s *ToolboxSystemService) UpdateRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxSystemPassword](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Password = strings.ReplaceAll(req.Password, `'`, `\'`)
|
||||
if _, err = shell.Execf(`yes '%s' | passwd root`, req.Password); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", s.t.Get("failed to set root password: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
28
web/src/api/panel/toolbox-ssh/index.ts
Normal file
28
web/src/api/panel/toolbox-ssh/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { http } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取 SSH 信息
|
||||
info: (): any => http.Get('/toolbox_ssh/info'),
|
||||
// 启动 SSH 服务
|
||||
start: (): any => http.Post('/toolbox_ssh/start'),
|
||||
// 停止 SSH 服务
|
||||
stop: (): any => http.Post('/toolbox_ssh/stop'),
|
||||
// 重启 SSH 服务
|
||||
restart: (): any => http.Post('/toolbox_ssh/restart'),
|
||||
// 设置 SSH 端口
|
||||
updatePort: (port: number): any => http.Post('/toolbox_ssh/port', { port }),
|
||||
// 设置密码认证
|
||||
updatePasswordAuth: (enabled: boolean): any =>
|
||||
http.Post('/toolbox_ssh/password_auth', { enabled }),
|
||||
// 设置密钥认证
|
||||
updatePubkeyAuth: (enabled: boolean): any => http.Post('/toolbox_ssh/pubkey_auth', { enabled }),
|
||||
// 设置 Root 登录
|
||||
updateRootLogin: (mode: string): any => http.Post('/toolbox_ssh/root_login', { mode }),
|
||||
// 设置 Root 密码
|
||||
updateRootPassword: (password: string): any =>
|
||||
http.Post('/toolbox_ssh/root_password', { password }),
|
||||
// 获取 Root 公钥
|
||||
rootKey: (): any => http.Get('/toolbox_ssh/root_key'),
|
||||
// 生成 Root 密钥对
|
||||
generateRootKey: (): any => http.Post('/toolbox_ssh/root_key')
|
||||
}
|
||||
@@ -5,6 +5,7 @@ defineOptions({
|
||||
|
||||
import BenchmarkView from '@/views/toolbox/BenchmarkView.vue'
|
||||
import ProcessView from '@/views/toolbox/ProcessView.vue'
|
||||
import SSHView from '@/views/toolbox/SSHView.vue'
|
||||
import SystemView from '@/views/toolbox/SystemView.vue'
|
||||
import WebHookView from '@/views/toolbox/WebHookView.vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
@@ -19,6 +20,7 @@ const current = ref('process')
|
||||
<n-tabs v-model:value="current" animated>
|
||||
<n-tab name="process" :tab="$gettext('Process')" />
|
||||
<n-tab name="system" :tab="$gettext('System')" />
|
||||
<n-tab name="ssh" tab="SSH" />
|
||||
<n-tab name="webhook" :tab="$gettext('WebHook')" />
|
||||
<n-tab name="benchmark" :tab="$gettext('Benchmark')" />
|
||||
</n-tabs>
|
||||
@@ -26,6 +28,7 @@ const current = ref('process')
|
||||
<n-flex vertical>
|
||||
<process-view v-if="current === 'process'" />
|
||||
<system-view v-if="current === 'system'" />
|
||||
<s-s-h-view v-if="current === 'ssh'" />
|
||||
<web-hook-view v-if="current === 'webhook'" />
|
||||
<benchmark-view v-if="current === 'benchmark'" />
|
||||
</n-flex>
|
||||
|
||||
393
web/src/views/toolbox/SSHView.vue
Normal file
393
web/src/views/toolbox/SSHView.vue
Normal file
@@ -0,0 +1,393 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'toolbox-ssh'
|
||||
})
|
||||
|
||||
import toolboxSSH from '@/api/panel/toolbox-ssh'
|
||||
import TheIcon from '@/components/custom/TheIcon.vue'
|
||||
import { generateRandomString } from '@/utils'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
// SSH 基础设置
|
||||
const sshStatus = ref(false)
|
||||
const sshPort = ref(22)
|
||||
const passwordAuth = ref(false)
|
||||
const pubkeyAuth = ref(true)
|
||||
|
||||
// Root 设置
|
||||
const rootLogin = ref('without-password')
|
||||
const rootPassword = ref('')
|
||||
const rootKey = ref('')
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const portLoading = ref(false)
|
||||
const passwordLoading = ref(false)
|
||||
const pubkeyLoading = ref(false)
|
||||
const rootLoginLoading = ref(false)
|
||||
const rootPasswordLoading = ref(false)
|
||||
const keyLoading = ref(false)
|
||||
|
||||
// Root 登录选项
|
||||
const rootLoginOptions = [
|
||||
{ label: 'yes - ' + $gettext('Allow password and key login'), value: 'yes' },
|
||||
{ label: 'no - ' + $gettext('Disable root login'), value: 'no' },
|
||||
{
|
||||
label: 'prohibit-password - ' + $gettext('Only allow key login (recommended)'),
|
||||
value: 'prohibit-password'
|
||||
},
|
||||
{
|
||||
label: 'forced-commands-only - ' + $gettext('Only allow key login with forced commands'),
|
||||
value: 'forced-commands-only'
|
||||
}
|
||||
]
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const info = await toolboxSSH.info()
|
||||
sshStatus.value = info.status
|
||||
sshPort.value = info.port
|
||||
passwordAuth.value = info.password_auth
|
||||
pubkeyAuth.value = info.pubkey_auth
|
||||
rootLogin.value = info.root_login
|
||||
|
||||
// 加载 root 私钥
|
||||
const key = await toolboxSSH.rootKey()
|
||||
rootKey.value = key || ''
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换 SSH 服务状态
|
||||
const handleToggleSSH = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
if (sshStatus.value) {
|
||||
await toolboxSSH.stop()
|
||||
window.$message.success($gettext('SSH service stopped'))
|
||||
} else {
|
||||
await toolboxSSH.start()
|
||||
window.$message.success($gettext('SSH service started'))
|
||||
}
|
||||
sshStatus.value = !sshStatus.value
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重启 SSH 服务
|
||||
const handleRestartSSH = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await toolboxSSH.restart()
|
||||
window.$message.success($gettext('SSH service restarted'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新端口
|
||||
const handleUpdatePort = async () => {
|
||||
portLoading.value = true
|
||||
try {
|
||||
await toolboxSSH.updatePort(sshPort.value)
|
||||
window.$message.success($gettext('SSH port updated'))
|
||||
} finally {
|
||||
portLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成随机端口
|
||||
const handleRandomPort = () => {
|
||||
// 生成 10000-65535 之间的随机端口
|
||||
sshPort.value = Math.floor(Math.random() * (65535 - 10000 + 1)) + 10000
|
||||
}
|
||||
|
||||
// 切换密码认证
|
||||
const handleTogglePasswordAuth = async () => {
|
||||
passwordLoading.value = true
|
||||
try {
|
||||
await toolboxSSH.updatePasswordAuth(!passwordAuth.value)
|
||||
passwordAuth.value = !passwordAuth.value
|
||||
window.$message.success($gettext('Password authentication updated'))
|
||||
} finally {
|
||||
passwordLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换密钥认证
|
||||
const handleTogglePubkeyAuth = async () => {
|
||||
pubkeyLoading.value = true
|
||||
try {
|
||||
await toolboxSSH.updatePubkeyAuth(!pubkeyAuth.value)
|
||||
pubkeyAuth.value = !pubkeyAuth.value
|
||||
window.$message.success($gettext('Key authentication updated'))
|
||||
} finally {
|
||||
pubkeyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 Root 登录设置
|
||||
const handleUpdateRootLogin = async (value: string) => {
|
||||
rootLoginLoading.value = true
|
||||
try {
|
||||
await toolboxSSH.updateRootLogin(value)
|
||||
rootLogin.value = value
|
||||
window.$message.success($gettext('Root login setting updated'))
|
||||
} finally {
|
||||
rootLoginLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 Root 密码
|
||||
const handleUpdateRootPassword = async () => {
|
||||
if (!rootPassword.value) {
|
||||
window.$message.warning($gettext('Please enter a password'))
|
||||
return
|
||||
}
|
||||
rootPasswordLoading.value = true
|
||||
try {
|
||||
await toolboxSSH.updateRootPassword(rootPassword.value)
|
||||
rootPassword.value = ''
|
||||
window.$message.success($gettext('Root password updated'))
|
||||
} finally {
|
||||
rootPasswordLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成随机 Root 密码
|
||||
const handleGeneratePassword = () => {
|
||||
rootPassword.value = generateRandomString(16)
|
||||
}
|
||||
|
||||
// 查看密钥
|
||||
const showKeyModal = ref(false)
|
||||
const handleViewKey = async () => {
|
||||
if (!rootKey.value) {
|
||||
// 没有密钥,先生成一个
|
||||
keyLoading.value = true
|
||||
try {
|
||||
const key = await toolboxSSH.generateRootKey()
|
||||
rootKey.value = key
|
||||
window.$message.success($gettext('SSH key generated'))
|
||||
} finally {
|
||||
keyLoading.value = false
|
||||
}
|
||||
}
|
||||
showKeyModal.value = true
|
||||
}
|
||||
|
||||
// 生成密钥
|
||||
const handleGenerateKey = async () => {
|
||||
keyLoading.value = true
|
||||
try {
|
||||
const key = await toolboxSSH.generateRootKey()
|
||||
rootKey.value = key
|
||||
window.$message.success($gettext('SSH key generated'))
|
||||
} finally {
|
||||
keyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 下载私钥
|
||||
const handleDownloadKey = () => {
|
||||
if (!rootKey.value) {
|
||||
window.$message.warning($gettext('No SSH key found'))
|
||||
return
|
||||
}
|
||||
const blob = new Blob([rootKey.value], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
// 根据私钥内容判断文件名
|
||||
if (rootKey.value.includes('OPENSSH PRIVATE KEY')) {
|
||||
link.download = 'id_ed25519'
|
||||
} else if (rootKey.value.includes('RSA PRIVATE KEY')) {
|
||||
link.download = 'id_rsa'
|
||||
} else {
|
||||
link.download = 'id_key'
|
||||
}
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-spin :show="loading">
|
||||
<n-flex vertical :size="24">
|
||||
<!-- SSH 服务状态 -->
|
||||
<n-card :title="$gettext('SSH Service')">
|
||||
<n-flex align="center" :size="12">
|
||||
<n-text strong>{{ $gettext('SSH Service Status') }}</n-text>
|
||||
<n-switch :value="sshStatus" :loading="loading" @update:value="handleToggleSSH" />
|
||||
<n-button :loading="loading" @click="handleRestartSSH">
|
||||
{{ $gettext('Restart') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<!-- SSH 基础设置 -->
|
||||
<n-card :title="$gettext('SSH Basic Settings')">
|
||||
<n-flex vertical :size="16">
|
||||
<!-- SSH 密码登录 -->
|
||||
<n-flex vertical :size="4">
|
||||
<n-flex align="center" :size="12">
|
||||
<n-text strong>{{ $gettext('SSH Password Login') }}</n-text>
|
||||
<n-switch
|
||||
:value="passwordAuth"
|
||||
:loading="passwordLoading"
|
||||
@update:value="handleTogglePasswordAuth"
|
||||
/>
|
||||
</n-flex>
|
||||
<n-text depth="3">{{ $gettext('Allow password authentication for SSH login') }}</n-text>
|
||||
</n-flex>
|
||||
|
||||
<!-- SSH 密钥登录 -->
|
||||
<n-flex vertical :size="4">
|
||||
<n-flex align="center" :size="12">
|
||||
<n-text strong>{{ $gettext('SSH Key Login') }}</n-text>
|
||||
<n-switch
|
||||
:value="pubkeyAuth"
|
||||
:loading="pubkeyLoading"
|
||||
@update:value="handleTogglePubkeyAuth"
|
||||
/>
|
||||
</n-flex>
|
||||
<n-text depth="3">{{
|
||||
$gettext('Allow public key authentication for SSH login')
|
||||
}}</n-text>
|
||||
</n-flex>
|
||||
|
||||
<!-- SSH 端口 -->
|
||||
<n-flex vertical :size="4">
|
||||
<n-flex align="center" :size="12">
|
||||
<n-text strong>{{ $gettext('SSH Port') }}</n-text>
|
||||
<n-input-number v-model:value="sshPort" :min="1" :max="65535" style="width: 120px" />
|
||||
<n-button @click="handleRandomPort">
|
||||
<template #icon>
|
||||
<the-icon :size="16" icon="mdi:refresh" />
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button type="primary" :loading="portLoading" @click="handleUpdatePort">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-text depth="3">{{ $gettext('Current SSH port, default is 22') }}</n-text>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<!-- Root 设置 -->
|
||||
<n-card :title="$gettext('Root Settings')">
|
||||
<n-flex vertical :size="16">
|
||||
<!-- Root 密码登录设置 -->
|
||||
<n-flex vertical :size="8">
|
||||
<n-text strong>{{ $gettext('Root Password Login Setting') }}</n-text>
|
||||
<n-select
|
||||
:value="rootLogin"
|
||||
:options="rootLoginOptions"
|
||||
:loading="rootLoginLoading"
|
||||
style="max-width: 400px"
|
||||
@update:value="handleUpdateRootLogin"
|
||||
/>
|
||||
</n-flex>
|
||||
|
||||
<!-- Root 密码 -->
|
||||
<n-flex vertical :size="8">
|
||||
<n-text strong>{{ $gettext('Root Password') }}</n-text>
|
||||
<n-flex align="center" :size="12">
|
||||
<n-input
|
||||
v-model:value="rootPassword"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$gettext('Enter new password')"
|
||||
style="max-width: 300px"
|
||||
/>
|
||||
<n-button @click="handleGeneratePassword">
|
||||
<template #icon>
|
||||
<the-icon :size="16" icon="mdi:refresh" />
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button
|
||||
type="warning"
|
||||
:loading="rootPasswordLoading"
|
||||
@click="handleUpdateRootPassword"
|
||||
>
|
||||
{{ $gettext('Reset') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-text depth="3">
|
||||
{{
|
||||
$gettext(
|
||||
'It is recommended to use a complex password. Save after modification. Refresh will clear the password field.'
|
||||
)
|
||||
}}
|
||||
</n-text>
|
||||
</n-flex>
|
||||
|
||||
<!-- Root 密钥 -->
|
||||
<n-flex vertical :size="4">
|
||||
<n-flex align="center" :size="12">
|
||||
<n-text strong>{{ $gettext('Root Key') }}</n-text>
|
||||
<n-button type="primary" :loading="keyLoading" @click="handleViewKey">
|
||||
{{ $gettext('View Key') }}
|
||||
</n-button>
|
||||
<n-button :loading="keyLoading" @click="handleDownloadKey">
|
||||
{{ $gettext('Download') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-text depth="3">
|
||||
{{
|
||||
$gettext('Recommended to use key login with password disabled for higher security')
|
||||
}}
|
||||
</n-text>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</n-flex>
|
||||
</n-spin>
|
||||
|
||||
<!-- 查看私钥弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="showKeyModal"
|
||||
preset="card"
|
||||
:title="$gettext('Root Private Key')"
|
||||
style="width: 60vw"
|
||||
:bordered="false"
|
||||
>
|
||||
<n-flex vertical :size="16">
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'This is the private key of the root user. Keep it safe and use it to login to this server.'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<n-input
|
||||
:value="rootKey"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
readonly
|
||||
:placeholder="$gettext('No private key generated')"
|
||||
/>
|
||||
<n-flex justify="end" :size="12">
|
||||
<n-button :loading="keyLoading" @click="handleGenerateKey">
|
||||
{{ $gettext('Regenerate') }}
|
||||
</n-button>
|
||||
<n-button type="primary" @click="handleDownloadKey">
|
||||
{{ $gettext('Download Private Key') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -21,7 +21,6 @@ const hosts = ref('')
|
||||
const timezone = ref('')
|
||||
const timezones = ref<any[]>([])
|
||||
const time = ref(DateTime.now().toMillis())
|
||||
const rootPassword = ref('')
|
||||
|
||||
useRequest(system.dns()).onSuccess(({ data }) => {
|
||||
dns1.value = data[0]
|
||||
@@ -65,12 +64,6 @@ const handleUpdateHost = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdateRootPassword = () => {
|
||||
useRequest(system.updateRootPassword(rootPassword.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdateTime = async () => {
|
||||
await Promise.all([
|
||||
useRequest(system.updateTime(String(DateTime.fromMillis(time.value).toISO()))),
|
||||
@@ -180,15 +173,5 @@ const handleSyncTime = () => {
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="root-password" :tab="$gettext('Root Password')">
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('Root Password')">
|
||||
<n-input v-model:value="rootPassword" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button type="primary" @click="handleUpdateRootPassword">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user