diff --git a/backend/app/api/v1/setting.go b/backend/app/api/v1/setting.go index 5c09a500f..3d1714de1 100644 --- a/backend/app/api/v1/setting.go +++ b/backend/app/api/v1/setting.go @@ -119,6 +119,48 @@ func (b *BaseApi) UpdatePassword(c *gin.Context) { helper.SuccessWithData(c, nil) } +// @Tags System Setting +// @Summary Update system ssl +// @Description 修改系统 ssl 登录 +// @Accept json +// @Param request body dto.SSLUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/ssl/update [post] +// @x-panel-log {"bodyKeys":[ssl],"paramKeys":[],"BeforeFuntions":[],"formatZH":"修改系统 ssl => [ssl]","formatEN":"update system ssl => [ssl]"} +func (b *BaseApi) UpdateSSL(c *gin.Context) { + var req dto.SSLUpdate + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := global.VALID.Struct(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + if err := settingService.UpdateSSL(c, req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags System Setting +// @Summary Load system cert info +// @Description 获取证书信息 +// @Success 200 {object} dto.SettingInfo +// @Security ApiKeyAuth +// @Router /settings/ssl/info [get] +func (b *BaseApi) LoadFromCert(c *gin.Context) { + info, err := settingService.LoadFromCert() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, info) +} + // @Tags System Setting // @Summary Update system port // @Description 更新系统端口 diff --git a/backend/app/dto/setting.go b/backend/app/dto/setting.go index e4ad1fa4d..c3483bf78 100644 --- a/backend/app/dto/setting.go +++ b/backend/app/dto/setting.go @@ -17,6 +17,8 @@ type SettingInfo struct { ServerPort string `json:"serverPort"` SecurityEntranceStatus string `json:"securityEntranceStatus"` + SSL string `json:"ssl"` + SSLType string `json:"sslType"` SecurityEntrance string `json:"securityEntrance"` ExpirationDays string `json:"expirationDays"` ExpirationTime string `json:"expirationTime"` @@ -40,6 +42,23 @@ type SettingUpdate struct { Value string `json:"value"` } +type SSLUpdate struct { + SSLType string `json:"sslType"` + Domain string `json:"domain"` + SSL string `json:"ssl" validate:"required,oneof=enable disable"` + Cert string `json:"cert"` + Key string `json:"key"` + SSLID uint `json:"sslID"` +} +type SSLInfo struct { + Domain string `json:"domain"` + Timeout string `json:"timeout"` + RootPath string `json:"rootPath"` + Cert string `json:"cert"` + Key string `json:"key"` + SSLID uint `json:"sslID"` +} + type PasswordUpdate struct { OldPassword string `json:"oldPassword" validate:"required"` NewPassword string `json:"newPassword" validate:"required"` diff --git a/backend/app/service/setting.go b/backend/app/service/setting.go index e645787ec..ee1a4cb9f 100644 --- a/backend/app/service/setting.go +++ b/backend/app/service/setting.go @@ -1,8 +1,14 @@ package service import ( + "crypto/tls" + "crypto/x509" "encoding/json" + "encoding/pem" + "fmt" + "os" "strconv" + "strings" "time" "github.com/1Panel-dev/1Panel/backend/app/dto" @@ -12,7 +18,9 @@ import ( "github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/encrypt" + "github.com/1Panel-dev/1Panel/backend/utils/ssl" "github.com/gin-gonic/gin" + "github.com/pkg/errors" ) type SettingService struct{} @@ -23,6 +31,8 @@ type ISettingService interface { UpdateEntrance(value string) error UpdatePassword(c *gin.Context, old, new string) error UpdatePort(port uint) error + UpdateSSL(c *gin.Context, req dto.SSLUpdate) error + LoadFromCert() (*dto.SSLInfo, error) HandlePasswordExpired(c *gin.Context, old, new string) error } @@ -111,6 +121,126 @@ func (u *SettingService) UpdatePort(port uint) error { return nil } +func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error { + if req.SSL == "disable" { + if err := settingRepo.Update("SSL", "disable"); err != nil { + return err + } + if err := settingRepo.Update("SSLType", "self"); err != nil { + return err + } + _ = os.Remove(global.CONF.System.BaseDir + "/1panel/secret/server.crt") + _ = os.Remove(global.CONF.System.BaseDir + "/1panel/secret/server.key") + go func() { + _, err := cmd.Exec("systemctl restart 1panel.service") + if err != nil { + global.LOG.Errorf("restart system failed, err: %v", err) + } + }() + return nil + } + + if _, err := os.Stat(global.CONF.System.BaseDir + "/1panel/secret"); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(global.CONF.System.BaseDir+"/1panel/secret", os.ModePerm); err != nil { + return err + } + } + if err := settingRepo.Update("SSLType", req.SSLType); err != nil { + return err + } + if req.SSLType == "self" { + if len(req.Domain) == 0 { + return fmt.Errorf("load domain failed") + } + if err := ssl.GenerateSSL(req.Domain); err != nil { + return err + } + } + if req.SSLType == "select" { + sslInfo, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID)) + if err != nil { + return err + } + req.Cert = sslInfo.Pem + req.Key = sslInfo.PrivateKey + req.SSLType = "import" + if err := settingRepo.Update("SSLID", strconv.Itoa(int(req.SSLID))); err != nil { + return err + } + } + if req.SSLType == "import" { + cert, err := os.OpenFile("/opt/1panel/secret/server.crt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer cert.Close() + if _, err := cert.WriteString(req.Cert); err != nil { + return err + } + key, err := os.OpenFile("/opt/1panel/secret/server.key", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + if _, err := key.WriteString(req.Key); err != nil { + return err + } + defer key.Close() + } + if err := checkCertValid(req.Domain); err != nil { + return err + } + if err := settingRepo.Update("SSL", req.SSL); err != nil { + return err + } + go func() { + _, err := cmd.Exec("systemctl restart 1panel.service") + if err != nil { + global.LOG.Errorf("restart system failed, err: %v", err) + } + }() + return nil +} + +func (u *SettingService) LoadFromCert() (*dto.SSLInfo, error) { + ssl, err := settingRepo.Get(settingRepo.WithByKey("SSL")) + if err != nil { + return nil, err + } + if ssl.Value == "disable" { + return &dto.SSLInfo{}, nil + } + sslType, err := settingRepo.Get(settingRepo.WithByKey("SSLType")) + if err != nil { + return nil, err + } + data, err := loadInfoFromCert() + if err != nil { + return nil, err + } + switch sslType.Value { + case "import": + if _, err := os.Stat(global.CONF.System.BaseDir + "/1panel/secret/server.crt"); err != nil { + return nil, fmt.Errorf("load server.crt file failed, err: %v", err) + } + certFile, _ := os.ReadFile(global.CONF.System.BaseDir + "/1panel/secret/server.crt") + data.Cert = string(certFile) + + if _, err := os.Stat(global.CONF.System.BaseDir + "/1panel/secret/server.key"); err != nil { + return nil, fmt.Errorf("load server.key file failed, err: %v", err) + } + keyFile, _ := os.ReadFile(global.CONF.System.BaseDir + "/1panel/secret/server.key") + data.Key = string(keyFile) + case "select": + sslID, err := settingRepo.Get(settingRepo.WithByKey("SSLID")) + if err != nil { + return nil, err + } + id, _ := strconv.Atoi(sslID.Value) + data.SSLID = uint(id) + } + return data, nil +} + func (u *SettingService) HandlePasswordExpired(c *gin.Context, old, new string) error { setting, err := settingRepo.Get(settingRepo.WithByKey("Password")) if err != nil { @@ -149,3 +279,71 @@ func (u *SettingService) UpdatePassword(c *gin.Context, old, new string) error { _ = global.SESSION.Clean() return nil } + +func loadInfoFromCert() (*dto.SSLInfo, error) { + var info dto.SSLInfo + certFile := global.CONF.System.BaseDir + "/1panel/secret/server.crt" + if _, err := os.Stat(certFile); err != nil { + return &info, err + } + certData, err := os.ReadFile(certFile) + if err != nil { + return &info, err + } + certBlock, _ := pem.Decode(certData) + if certBlock == nil { + return &info, err + } + certObj, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + return &info, err + } + var domains []string + if len(certObj.IPAddresses) != 0 { + for _, ip := range certObj.IPAddresses { + domains = append(domains, ip.String()) + } + } + if len(certObj.DNSNames) != 0 { + domains = append(domains, certObj.DNSNames...) + } + return &dto.SSLInfo{ + Domain: strings.Join(domains, ","), + Timeout: certObj.NotAfter.Format("2006-01-02 15:04:05"), + RootPath: global.CONF.System.BaseDir + "/1panel/secret/server.crt", + }, nil +} + +func checkCertValid(domain string) error { + certificate, err := os.ReadFile(global.CONF.System.BaseDir + "/1panel/secret/server.crt") + if err != nil { + return err + } + key, err := os.ReadFile(global.CONF.System.BaseDir + "/1panel/secret/server.key") + if err != nil { + return err + } + if _, err = tls.X509KeyPair(certificate, key); err != nil { + return err + } + certObj, err := x509.ParseCertificate(certificate) + if err != nil { + return err + } + + if len(certObj.IPAddresses) != 0 { + for _, ip := range certObj.IPAddresses { + if ip.String() == domain { + return nil + } + } + } + if len(certObj.DNSNames) != 0 { + for _, ip := range certObj.DNSNames { + if ip == domain { + return nil + } + } + } + return errors.New("The domain name or ip address does not match") +} diff --git a/backend/configs/system.go b/backend/configs/system.go index 2aa26bc7b..02c73ef97 100644 --- a/backend/configs/system.go +++ b/backend/configs/system.go @@ -2,6 +2,7 @@ package configs type System struct { Port string `mapstructure:"port"` + SSL string `mapstructure:"ssl"` DbFile string `mapstructure:"db_file"` DbPath string `mapstructure:"db_path"` LogPath string `mapstructure:"log_path"` diff --git a/backend/init/hook/hook.go b/backend/init/hook/hook.go index c5affaa61..a2fdc989d 100644 --- a/backend/init/hook/hook.go +++ b/backend/init/hook/hook.go @@ -17,6 +17,11 @@ func Init() { global.LOG.Errorf("load service encrypt key from setting failed, err: %v", err) } global.CONF.System.EncryptKey = enptrySetting.Value + sslSetting, err := settingRepo.Get(settingRepo.WithByKey("SSL")) + if err != nil { + global.LOG.Errorf("load service ssl from setting failed, err: %v", err) + } + global.CONF.System.SSL = sslSetting.Value if _, err := settingRepo.Get(settingRepo.WithByKey("SystemStatus")); err != nil { _ = settingRepo.Create("SystemStatus", "Free") diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index b1d79d7b4..992795602 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -25,7 +25,7 @@ func Init() { migrations.UpdateTableApp, migrations.UpdateTableHost, migrations.UpdateTableWebsite, - migrations.AddEntranceStatus, + migrations.AddEntranceAndSSL, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index f6dc843b5..f16517a26 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -288,8 +288,8 @@ var UpdateTableWebsite = &gormigrate.Migration{ }, } -var AddEntranceStatus = &gormigrate.Migration{ - ID: "20230414-add-entrance-status", +var AddEntranceAndSSL = &gormigrate.Migration{ + ID: "20230414-add-entrance-and-ssl", Migrate: func(tx *gorm.DB) error { if err := tx.Create(&model.Setting{Key: "SecurityEntranceStatus", Value: "disable"}).Error; err != nil { return err @@ -297,6 +297,15 @@ var AddEntranceStatus = &gormigrate.Migration{ if err := tx.Model(&model.Setting{}).Where("key = ?", "SecurityEntrance").Updates(map[string]interface{}{"value": ""}).Error; err != nil { return err } + if err := tx.Create(&model.Setting{Key: "SSLType", Value: "self"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "SSLID", Value: "0"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "SSL", Value: "disable"}).Error; err != nil { + return err + } return tx.AutoMigrate(&model.Website{}) }, } diff --git a/backend/router/ro_setting.go b/backend/router/ro_setting.go index cb2220c87..2ca54f689 100644 --- a/backend/router/ro_setting.go +++ b/backend/router/ro_setting.go @@ -24,6 +24,8 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) { settingRouter.POST("/update", baseApi.UpdateSetting) settingRouter.POST("/entrance/enable", baseApi.UpdateEntrance) settingRouter.POST("/port/update", baseApi.UpdatePort) + settingRouter.POST("/ssl/update", baseApi.UpdateSSL) + settingRouter.GET("/ssl/info", baseApi.LoadFromCert) settingRouter.POST("/password/update", baseApi.UpdatePassword) settingRouter.POST("/time/sync", baseApi.SyncTime) settingRouter.POST("/monitor/clean", baseApi.CleanMonitor) diff --git a/backend/server/server.go b/backend/server/server.go index c46beebcd..032a06a3b 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -1,8 +1,11 @@ package server import ( + "crypto/tls" "encoding/gob" "fmt" + "net/http" + "os" "time" "github.com/1Panel-dev/1Panel/backend/init/app" @@ -43,22 +46,42 @@ func Start() { rootRouter := router.Routers() address := fmt.Sprintf(":%s", global.CONF.System.Port) - s := initServer(address, rootRouter) - global.LOG.Infof("server run success on %s", global.CONF.System.Port) - if err := s.ListenAndServe(); err != nil { - global.LOG.Error(err) - panic(err) - } -} - -type server interface { - ListenAndServe() error -} - -func initServer(address string, router *gin.Engine) server { - s := endless.NewServer(address, router) + s := endless.NewServer(address, rootRouter) s.ReadHeaderTimeout = 20 * time.Second s.WriteTimeout = 60 * time.Second s.MaxHeaderBytes = 1 << 20 - return s + + if global.CONF.System.SSL == "disable" { + global.LOG.Infof("server run success on %s with http", global.CONF.System.Port) + if err := s.ListenAndServe(); err != nil { + global.LOG.Error(err) + panic(err) + } + } else { + certificate, err := os.ReadFile(global.CONF.System.BaseDir + "/1panel/secret/server.crt") + if err != nil { + panic(err) + } + key, err := os.ReadFile(global.CONF.System.BaseDir + "/1panel/secret/server.key") + if err != nil { + panic(err) + } + cert, err := tls.X509KeyPair(certificate, key) + if err != nil { + panic(err) + } + s := &http.Server{ + Addr: address, + Handler: rootRouter, + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, + } + + global.LOG.Infof("server run success on %s with https", global.CONF.System.Port) + if err := s.ListenAndServeTLS("", ""); err != nil { + global.LOG.Error(err) + panic(err) + } + } } diff --git a/backend/utils/ssl/ssl.go b/backend/utils/ssl/ssl.go new file mode 100644 index 000000000..34f59792d --- /dev/null +++ b/backend/utils/ssl/ssl.go @@ -0,0 +1,112 @@ +package ssl + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "os" + "time" + + "github.com/1Panel-dev/1Panel/backend/global" +) + +func GenerateSSL(domain string) error { + rootPrivateKey, _ := rsa.GenerateKey(rand.Reader, 2048) + ipItem := net.ParseIP(domain) + isIP := false + if len(ipItem) != 0 { + isIP = true + } + + rootTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "1Panel Root CA"}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + BasicConstraintsValid: true, + IsCA: true, + KeyUsage: x509.KeyUsageCertSign, + } + if isIP { + rootTemplate.IPAddresses = []net.IP{ipItem} + } else { + rootTemplate.DNSNames = []string{domain} + } + + rootCertBytes, _ := x509.CreateCertificate(rand.Reader, &rootTemplate, &rootTemplate, &rootPrivateKey.PublicKey, rootPrivateKey) + rootCertBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: rootCertBytes, + } + + interPrivateKey, _ := rsa.GenerateKey(rand.Reader, 2048) + interTemplate := x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "1Panel Intermediate CA"}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + BasicConstraintsValid: true, + IsCA: true, + KeyUsage: x509.KeyUsageCertSign, + } + if isIP { + interTemplate.IPAddresses = []net.IP{ipItem} + } else { + interTemplate.DNSNames = []string{domain} + } + + interCertBytes, _ := x509.CreateCertificate(rand.Reader, &interTemplate, &rootTemplate, &interPrivateKey.PublicKey, rootPrivateKey) + interCertBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: interCertBytes, + } + + clientPrivateKey, _ := rsa.GenerateKey(rand.Reader, 2048) + clientTemplate := x509.Certificate{ + SerialNumber: big.NewInt(3), + Subject: pkix.Name{CommonName: domain}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + if isIP { + clientTemplate.IPAddresses = []net.IP{ipItem} + } else { + clientTemplate.DNSNames = []string{domain} + } + + clientCertBytes, _ := x509.CreateCertificate(rand.Reader, &clientTemplate, &interTemplate, &clientPrivateKey.PublicKey, interPrivateKey) + clientCertBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: clientCertBytes, + } + + pemBytes := []byte{} + pemBytes = append(pemBytes, pem.EncodeToMemory(clientCertBlock)...) + pemBytes = append(pemBytes, pem.EncodeToMemory(interCertBlock)...) + pemBytes = append(pemBytes, pem.EncodeToMemory(rootCertBlock)...) + certOut, err := os.OpenFile(global.CONF.System.BaseDir+"/1panel/secret/server.crt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer certOut.Close() + if _, err := certOut.Write(pemBytes); err != nil { + return err + } + + keyOut, err := os.OpenFile(global.CONF.System.BaseDir+"/1panel/secret/server.key", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer keyOut.Close() + if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientPrivateKey)}); err != nil { + return err + } + + return nil +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index f2cececf5..a6a96a64c 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -26,7 +26,6 @@ class RequestHttp { ...config.headers, }; } - return { ...config, }; diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 51e7f2f14..d243657fe 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -16,6 +16,8 @@ export namespace Setting { serverPort: number; securityEntranceStatus: string; + ssl: string; + sslType: string; securityEntrance: string; expirationDays: number; expirationTime: string; @@ -35,6 +37,22 @@ export namespace Setting { key: string; value: string; } + export interface SSLUpdate { + ssl: string; + domain: string; + sslType: string; + cert: string; + key: string; + sslID: number; + } + export interface SSLInfo { + domain: string; + timeout: string; + rootPath: string; + cert: string; + key: string; + sslID: number; + } export interface PasswordUpdate { oldPassword: string; newPassword: string; diff --git a/frontend/src/api/modules/setting.ts b/frontend/src/api/modules/setting.ts index d075cf659..76e7c8df4 100644 --- a/frontend/src/api/modules/setting.ts +++ b/frontend/src/api/modules/setting.ts @@ -28,6 +28,13 @@ export const updatePort = (param: Setting.PortUpdate) => { return http.post(`/settings/port/update`, param); }; +export const updateSSL = (param: Setting.SSLUpdate) => { + return http.post(`/settings/ssl/update`, param); +}; +export const loadSSLInfo = () => { + return http.get(`/settings/ssl/info`); +}; + export const handleExpired = (param: Setting.PasswordUpdate) => { return http.post(`/settings/expired/handle`, param); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index c0ee585e4..0b9958449 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -872,6 +872,18 @@ const message = { mfaHelper2: 'Scan the following QR code using the mobile app to obtain the 6-digit verification code', mfaHelper3: 'Enter six digits from the app', + https: 'Setting up HTTPS protocol access for the panel can enhance the security of panel access.', + selfSigned: 'Self signed', + selfSignedHelper: + 'It is normal for self-signed certificates to be not trusted by browsers and display a security warning as the certificate is not issued by a trusted third party.', + import: 'Import', + select: 'Select', + domainOrIP: 'Domain/IP:', + timeOut: 'Timeout', + rootCrtDownload: 'Root certificate download', + primaryKey: 'Primary key', + certificate: 'Certificate', + snapshot: 'Snapshot', thirdPartySupport: 'Only third-party accounts are supported', recoverDetail: 'Recover detail', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 5e1812bba..bc1c4a755 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -857,6 +857,17 @@ const message = { password: '密码', path: '路径', + https: '为面板设置 https 协议访问,提升面板访问安全性', + selfSigned: '自签名', + selfSignedHelper: '自签证书,不被浏览器信任,显示不安全是正常现象', + import: '导入', + select: '选择已有', + domainOrIP: '域名/IP:', + timeOut: '过期时间:', + rootCrtDownload: '根证书下载', + primaryKey: '密钥', + certificate: '证书', + snapshot: '快照', thirdPartySupport: '仅支持第三方账号', recoverDetail: '恢复详情', @@ -909,6 +920,8 @@ const message = { mfaHelper1: '下载两步验证手机应用 如:', mfaHelper2: '使用手机应用扫描以下二维码,获取 6 位验证码', mfaHelper3: '输入手机应用上的 6 位数字', + sslDisable: '禁用', + sslDisableHelper: '禁用 https 服务,需要重启面板才能生效,是否继续!', monitor: '监控', enableMonitor: '监控状态', diff --git a/frontend/src/views/setting/safe/index.vue b/frontend/src/views/setting/safe/index.vue index 2c44da8a3..5b325b740 100644 --- a/frontend/src/views/setting/safe/index.vue +++ b/frontend/src/views/setting/safe/index.vue @@ -5,7 +5,7 @@
- +