mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-29 02:10:58 +08:00
feat: 重构面板证书逻辑
This commit is contained in:
+1
-1
@@ -69,7 +69,7 @@ func (r *Ace) Run() error {
|
||||
serverErr := make(chan error, 1)
|
||||
go func() {
|
||||
fmt.Println("[HTTP] listening and serving on port", r.conf.HTTP.Port)
|
||||
if r.conf.HTTP.TLS {
|
||||
if r.conf.HTTP.IsHTTPS() {
|
||||
if err := r.server.ListenAndServeTLS("", ""); !errors.Is(err, http.ErrServerClosed) {
|
||||
serverErr <- err
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func NewRouter(t *gotext.Locale, middlewares *middleware.Middlewares, http *rout
|
||||
}
|
||||
|
||||
func NewTLSReloader(conf *config.Config) (*tlscert.Reloader, error) {
|
||||
if !conf.HTTP.TLS {
|
||||
if !conf.HTTP.IsHTTPS() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func NewHttp(conf *config.Config, mux *chi.Mux, reloader *tlscert.Reloader) (*hl
|
||||
})
|
||||
srv.Listen80RedirectTo443 = true
|
||||
|
||||
if conf.HTTP.TLS && reloader != nil {
|
||||
if conf.HTTP.IsHTTPS() && reloader != nil {
|
||||
srv.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
GetCertificate: reloader.GetCertificate,
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/firewall"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/os"
|
||||
"github.com/acepanel/panel/v3/pkg/tools"
|
||||
)
|
||||
|
||||
const settingCacheTTL = 5 * time.Minute
|
||||
@@ -254,8 +255,7 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
|
||||
IPDBURL: ipdbURL,
|
||||
IPDBPath: ipdbPath,
|
||||
Port: r.conf.HTTP.Port,
|
||||
HTTPS: r.conf.HTTP.TLS,
|
||||
ACME: r.conf.HTTP.ACME,
|
||||
TLS: r.conf.HTTP.TLS,
|
||||
PublicIP: publicIP,
|
||||
Cert: crt,
|
||||
Key: key,
|
||||
@@ -326,6 +326,25 @@ func (r *settingRepo) UpdatePanel(ctx context.Context, req *request.SettingPanel
|
||||
// 下面是需要需要重启的设置
|
||||
// 面板HTTPS
|
||||
restartFlag := false
|
||||
|
||||
// 自签模式
|
||||
if req.TLS == "self-signed" {
|
||||
needGen := req.Cert == "" || req.Key == ""
|
||||
if !needGen {
|
||||
if _, err := cert.ParseCert([]byte(req.Cert)); err != nil {
|
||||
needGen = true
|
||||
}
|
||||
}
|
||||
if needGen {
|
||||
crt, key, err := cert.GenerateSelfSigned(tools.CollectLocalNames())
|
||||
if err != nil {
|
||||
return false, errors.New(r.t.Get("failed to generate self-signed certificate: %v", err))
|
||||
}
|
||||
req.Cert = string(crt)
|
||||
req.Key = string(key)
|
||||
}
|
||||
}
|
||||
|
||||
oldCert, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
|
||||
oldKey, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.key"))
|
||||
if oldCert != req.Cert || oldKey != req.Key {
|
||||
@@ -334,11 +353,14 @@ func (r *settingRepo) UpdatePanel(ctx context.Context, req *request.SettingPanel
|
||||
}
|
||||
restartFlag = true
|
||||
}
|
||||
if _, err := cert.ParseCert([]byte(req.Cert)); err != nil && req.HTTPS {
|
||||
return false, errors.New(r.t.Get("failed to parse certificate: %v", err))
|
||||
}
|
||||
if _, err := cert.ParseKey([]byte(req.Key)); err != nil && req.HTTPS {
|
||||
return false, errors.New(r.t.Get("failed to parse private key: %v", err))
|
||||
// custom 模式需要验证证书格式
|
||||
if req.TLS == "custom" {
|
||||
if _, err := cert.ParseCert([]byte(req.Cert)); err != nil {
|
||||
return false, errors.New(r.t.Get("failed to parse certificate: %v", err))
|
||||
}
|
||||
if _, err := cert.ParseKey([]byte(req.Key)); err != nil {
|
||||
return false, errors.New(r.t.Get("failed to parse private key: %v", err))
|
||||
}
|
||||
}
|
||||
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), req.Cert, 0600); err != nil {
|
||||
return false, err
|
||||
@@ -379,8 +401,7 @@ func (r *settingRepo) UpdatePanel(ctx context.Context, req *request.SettingPanel
|
||||
conf.HTTP.Entrance = req.Entrance
|
||||
conf.HTTP.EntranceError = req.EntranceError
|
||||
conf.HTTP.LoginCaptcha = req.LoginCaptcha
|
||||
conf.HTTP.TLS = req.HTTPS
|
||||
conf.HTTP.ACME = req.ACME
|
||||
conf.HTTP.TLS = req.TLS
|
||||
conf.HTTP.IPHeader = req.IPHeader
|
||||
conf.HTTP.BindDomain = req.BindDomain
|
||||
conf.HTTP.BindIP = req.BindIP
|
||||
|
||||
@@ -97,7 +97,7 @@ func MustLogin(t *gotext.Locale, conf *config.Config, session *sessions.Manager,
|
||||
Expires: time.Now().Add(time.Duration(session.Lifetime) * time.Minute),
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: conf.HTTP.TLS,
|
||||
Secure: conf.HTTP.IsHTTPS(),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,14 +21,13 @@ type SettingPanel struct {
|
||||
BackupPath string `json:"backup_path" validate:"required"`
|
||||
ProjectPath string `json:"project_path" validate:"required"`
|
||||
ContainerSock string `json:"container_sock"`
|
||||
HiddenMenu []string `json:"hidden_menu"` // 隐藏的菜单项
|
||||
CustomLogo string `json:"custom_logo" validate:"isFullURL"` // 自定义 Logo URL
|
||||
IPDBType string `json:"ipdb_type"` // IPDB 来源类型: "" / "custom" / "subscribe"
|
||||
IPDBURL string `json:"ipdb_url"` // IPDB 订阅链接
|
||||
IPDBPath string `json:"ipdb_path"` // IPDB 地理位置库路径
|
||||
Port uint `json:"port" validate:"required|min:1|max:65535"`
|
||||
HTTPS bool `json:"https"`
|
||||
ACME bool `json:"acme"`
|
||||
HiddenMenu []string `json:"hidden_menu"` // 隐藏的菜单项
|
||||
CustomLogo string `json:"custom_logo" validate:"isFullURL"` // 自定义 Logo URL
|
||||
IPDBType string `json:"ipdb_type"` // IPDB 来源类型: "" / "custom" / "subscribe"
|
||||
IPDBURL string `json:"ipdb_url"` // IPDB 订阅链接
|
||||
IPDBPath string `json:"ipdb_path"` // IPDB 地理位置库路径
|
||||
Port uint `json:"port" validate:"required|min:1|max:65535"` // 面板端口
|
||||
TLS string `json:"tls"` // 面板 TLS: off, acme, self-signed, custom
|
||||
PublicIP []string `json:"public_ip"`
|
||||
Cert string `json:"cert"`
|
||||
Key string `json:"key"`
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
pkgcert "github.com/acepanel/panel/v3/pkg/cert"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/tools"
|
||||
)
|
||||
|
||||
// CertRenew 证书续签
|
||||
@@ -73,7 +74,38 @@ func (r *CertRenew) Run() {
|
||||
}
|
||||
|
||||
// 面板证书续签
|
||||
if r.conf.HTTP.ACME {
|
||||
switch r.conf.HTTP.TLS {
|
||||
case "self-signed":
|
||||
// 自签证书续签
|
||||
crt, _ := os.ReadFile(filepath.Join(app.Root, "panel/storage/cert.pem"))
|
||||
decode, err := pkgcert.ParseCert(crt)
|
||||
if err == nil {
|
||||
if time.Until(decode.NotAfter) > 30*24*time.Hour {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
r.log.Warn("failed to parse panel certificate", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
}
|
||||
|
||||
newCrt, newKey, err := pkgcert.GenerateSelfSigned(tools.CollectLocalNames())
|
||||
if err != nil {
|
||||
r.log.Warn("failed to generate self-signed certificate", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
if err = r.settingRepo.UpdateCert(&request.SettingCert{
|
||||
Cert: string(newCrt),
|
||||
Key: string(newKey),
|
||||
}); err != nil {
|
||||
r.log.Warn("failed to update panel certificate", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
r.log.Info("panel self-signed certificate renewed", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0))
|
||||
|
||||
case "off", "custom":
|
||||
// off/custom 不需要自动续签
|
||||
|
||||
default:
|
||||
// ACME 模式
|
||||
crt, _ := os.ReadFile(filepath.Join(app.Root, "panel/storage/cert.pem"))
|
||||
decode, err := pkgcert.ParseCert(crt)
|
||||
if err == nil {
|
||||
|
||||
+23
-27
@@ -9,7 +9,6 @@ import (
|
||||
"math/rand/v2"
|
||||
stdos "os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -178,7 +177,7 @@ func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
protocol := "http"
|
||||
if s.conf.HTTP.TLS {
|
||||
if s.conf.HTTP.IsHTTPS() {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
@@ -362,7 +361,7 @@ func (s *CliService) HTTPSOn(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
conf.HTTP.TLS = true
|
||||
conf.HTTP.TLS = "acme"
|
||||
|
||||
if err = config.Save(conf); err != nil {
|
||||
return err
|
||||
@@ -378,7 +377,7 @@ func (s *CliService) HTTPSOff(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
conf.HTTP.TLS = false
|
||||
conf.HTTP.TLS = "off"
|
||||
|
||||
if err = config.Save(conf); err != nil {
|
||||
return err
|
||||
@@ -389,32 +388,26 @@ func (s *CliService) HTTPSOff(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
func (s *CliService) HTTPSGenerate(ctx context.Context, cmd *cli.Command) error {
|
||||
var names []string
|
||||
if lv4, err := tools.GetLocalIPv4(); err == nil {
|
||||
if !slices.Contains(names, lv4) {
|
||||
names = append(names, lv4)
|
||||
}
|
||||
}
|
||||
if lv6, err := tools.GetLocalIPv6(); err == nil {
|
||||
if !slices.Contains(names, lv6) {
|
||||
names = append(names, lv6)
|
||||
}
|
||||
}
|
||||
if rv4, err := tools.GetPublicIPv4(); err == nil {
|
||||
if !slices.Contains(names, rv4) {
|
||||
names = append(names, rv4)
|
||||
}
|
||||
}
|
||||
if rv6, err := tools.GetPublicIPv6(); err == nil {
|
||||
if !slices.Contains(names, rv6) {
|
||||
names = append(names, rv6)
|
||||
}
|
||||
}
|
||||
names := tools.CollectLocalNames()
|
||||
|
||||
var crt, key []byte
|
||||
var err error
|
||||
|
||||
if s.conf.HTTP.ACME {
|
||||
switch s.conf.HTTP.TLS {
|
||||
case "self-signed":
|
||||
// 自签模式
|
||||
crt, key, err = cert.GenerateSelfSigned(names)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "off", "custom":
|
||||
// off/custom 不需要自动生成,回退到自签
|
||||
crt, key, err = cert.GenerateSelfSigned(names)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// ACME 模式
|
||||
ip, err := s.settingRepo.Get(biz.SettingKeyPublicIPs)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -440,6 +433,7 @@ func (s *CliService) HTTPSGenerate(ctx context.Context, cmd *cli.Command) error
|
||||
}
|
||||
}
|
||||
|
||||
// ACME 失败回退到自签
|
||||
if crt == nil || key == nil {
|
||||
crt, key, err = cert.GenerateSelfSigned(names)
|
||||
if err != nil {
|
||||
@@ -1080,7 +1074,9 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
|
||||
conf.App.APIEndpoint = "api.acepanel.net"
|
||||
conf.App.DownloadEndpoint = "dl.acepanel.net"
|
||||
conf.HTTP.Entrance = "/" + str.Random(6)
|
||||
conf.HTTP.ACME = acme
|
||||
if acme {
|
||||
conf.HTTP.TLS = "acme"
|
||||
}
|
||||
|
||||
// 随机默认端口
|
||||
checkPort:
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/pkg/cert"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/tools"
|
||||
)
|
||||
|
||||
@@ -64,6 +66,30 @@ func (s *SettingService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *SettingService) ObtainCert(w http.ResponseWriter, r *http.Request) {
|
||||
// 自签模式
|
||||
conf, err := config.Load()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if conf.HTTP.TLS == "self-signed" {
|
||||
crt, key, err := cert.GenerateSelfSigned(tools.CollectLocalNames())
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = s.settingRepo.UpdateCert(&request.SettingCert{
|
||||
Cert: string(crt),
|
||||
Key: string(key),
|
||||
}); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
Success(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// ACME 模式
|
||||
ip, err := s.settingRepo.Get(biz.SettingKeyPublicIPs)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
|
||||
@@ -163,7 +163,7 @@ func (s *UserService) Login(w http.ResponseWriter, r *http.Request) {
|
||||
ip = r.RemoteAddr
|
||||
}
|
||||
|
||||
if req.SafeLogin && !s.conf.HTTP.TLS {
|
||||
if req.SafeLogin && !s.conf.HTTP.IsHTTPS() {
|
||||
sess.Put("safe_login", true)
|
||||
sess.Put("safe_client", fmt.Sprintf("%x", sha256.Sum256([]byte(ip))))
|
||||
} else {
|
||||
|
||||
@@ -356,7 +356,7 @@ func (s *UserPasskeyService) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
// isCertTrusted 检查面板证书是否由 CA 签发(非自签名)
|
||||
// 用证书自身公钥验签自身签名,成功说明是自签名证书
|
||||
func (s *UserPasskeyService) isCertTrusted() bool {
|
||||
if !s.conf.HTTP.TLS {
|
||||
if !s.conf.HTTP.IsHTTPS() {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -33,8 +33,7 @@ type HTTPConfig struct {
|
||||
Port uint `yaml:"port"`
|
||||
Entrance string `yaml:"entrance"`
|
||||
EntranceError string `yaml:"entrance_error"`
|
||||
TLS bool `yaml:"tls"`
|
||||
ACME bool `yaml:"acme"`
|
||||
TLS string `yaml:"tls"` // off, acme, self-signed, custom
|
||||
LoginCaptcha bool `yaml:"login_captcha"`
|
||||
IPHeader string `yaml:"ip_header"`
|
||||
BindDomain []string `yaml:"bind_domain"`
|
||||
@@ -42,6 +41,12 @@ type HTTPConfig struct {
|
||||
BindUA []string `yaml:"bind_ua"`
|
||||
}
|
||||
|
||||
// IsHTTPS 判断是否启用 HTTPS
|
||||
func (c *HTTPConfig) IsHTTPS() bool {
|
||||
// TODO remove false in v4
|
||||
return c.TLS != "off" && c.TLS != "false" && c.TLS != ""
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Debug bool `yaml:"debug"`
|
||||
}
|
||||
|
||||
@@ -284,6 +284,24 @@ func GetLocalIPv6() (string, error) {
|
||||
return local.IP.String(), nil
|
||||
}
|
||||
|
||||
// CollectLocalNames 收集本机所有 IP 地址
|
||||
func CollectLocalNames() []string {
|
||||
var names []string
|
||||
if lv4, err := GetLocalIPv4(); err == nil && !slices.Contains(names, lv4) {
|
||||
names = append(names, lv4)
|
||||
}
|
||||
if lv6, err := GetLocalIPv6(); err == nil && !slices.Contains(names, lv6) {
|
||||
names = append(names, lv6)
|
||||
}
|
||||
if rv4, err := GetPublicIPv4(); err == nil && !slices.Contains(names, rv4) {
|
||||
names = append(names, rv4)
|
||||
}
|
||||
if rv6, err := GetPublicIPv6(); err == nil && !slices.Contains(names, rv6) {
|
||||
names = append(names, rv6)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// FormatBytes 格式化bytes
|
||||
func FormatBytes(size float64) string {
|
||||
units := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
||||
|
||||
@@ -24,18 +24,16 @@ const isObtainCert = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
|
||||
// 记录已保存的 HTTPS 相关设置,用于判断是否有未保存的修改
|
||||
const savedHttpsState = ref({ https: false, acme: false, public_ip: '[]' })
|
||||
const savedHttpsState = ref({ tls: 'off', public_ip: '[]' })
|
||||
const httpsSettingsDirty = computed(() => {
|
||||
return (
|
||||
model.value.https !== savedHttpsState.value.https ||
|
||||
model.value.acme !== savedHttpsState.value.acme ||
|
||||
model.value.tls !== savedHttpsState.value.tls ||
|
||||
JSON.stringify(model.value.public_ip) !== savedHttpsState.value.public_ip
|
||||
)
|
||||
})
|
||||
const snapshotHttpsState = () => {
|
||||
savedHttpsState.value = {
|
||||
https: model.value.https,
|
||||
acme: model.value.acme,
|
||||
tls: model.value.tls,
|
||||
public_ip: JSON.stringify(model.value.public_ip)
|
||||
}
|
||||
}
|
||||
@@ -65,8 +63,7 @@ const { data: model } = useRequest(setting.list, {
|
||||
ipdb_type: '',
|
||||
ipdb_url: '',
|
||||
ipdb_path: '',
|
||||
https: false,
|
||||
acme: false,
|
||||
tls: 'off',
|
||||
public_ip: [],
|
||||
cert: '',
|
||||
key: ''
|
||||
@@ -101,7 +98,7 @@ const handleSave = () => {
|
||||
if (data.restart) {
|
||||
window.$message.info($gettext('Panel is restarting, page will refresh in 5 seconds'))
|
||||
setTimeout(() => {
|
||||
const protocol = model.value.https ? 'https:' : 'http:'
|
||||
const protocol = model.value.tls !== 'off' ? 'https:' : 'http:'
|
||||
const hostname = window.location.hostname
|
||||
const port = model.value.port
|
||||
const entrance = model.value.entrance || '/'
|
||||
@@ -171,13 +168,17 @@ const handleCreate = () => {
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab === 'safe' && model.https && model.acme"
|
||||
v-if="currentTab === 'safe' && (model.tls === 'acme' || model.tls === 'self-signed')"
|
||||
type="info"
|
||||
:loading="isObtainCert"
|
||||
:disabled="httpsSettingsDirty || isObtainCert"
|
||||
@click="handleObtainCert"
|
||||
>
|
||||
{{ $gettext('Refresh Certificate') }}
|
||||
{{
|
||||
model.tls === 'acme'
|
||||
? $gettext('Refresh Certificate')
|
||||
: $gettext('Regenerate Certificate')
|
||||
}}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
@@ -6,27 +6,11 @@ const { $gettext } = useGettext()
|
||||
|
||||
const model = defineModel<any>('model', { type: Object, required: true })
|
||||
|
||||
// HTTPS 模式:off / acme / custom
|
||||
// HTTPS 模式:off / acme / self-signed / custom
|
||||
const httpsMode = computed({
|
||||
get: () => {
|
||||
if (!model.value.https) return 'off'
|
||||
return model.value.acme ? 'acme' : 'custom'
|
||||
},
|
||||
set: (value: string) => {
|
||||
switch (value) {
|
||||
case 'off':
|
||||
model.value.https = false
|
||||
model.value.acme = false
|
||||
break
|
||||
case 'acme':
|
||||
model.value.https = true
|
||||
model.value.acme = true
|
||||
break
|
||||
case 'custom':
|
||||
model.value.https = true
|
||||
model.value.acme = false
|
||||
break
|
||||
}
|
||||
get: () => model.value.tls || 'off',
|
||||
set: (v: string) => {
|
||||
model.value.tls = v
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -251,7 +235,7 @@ const httpsMode = computed({
|
||||
</template>
|
||||
{{
|
||||
$gettext(
|
||||
'Enable HTTPS for the panel. ACME will automatically obtain and renew the certificate daily (requires panel accessible via public IP). Custom allows you to provide your own certificate'
|
||||
'Enable HTTPS for the panel. ACME will automatically obtain and renew the certificate daily (requires panel accessible via public IP). Self-Signed generates a certificate automatically (browsers will show a warning). Custom allows you to provide your own certificate'
|
||||
)
|
||||
}}
|
||||
</n-tooltip>
|
||||
@@ -261,6 +245,7 @@ const httpsMode = computed({
|
||||
v-for="option in [
|
||||
{ label: $gettext('Disabled'), value: 'off' },
|
||||
{ label: $gettext('ACME (Auto)'), value: 'acme' },
|
||||
{ label: $gettext('Self-Signed'), value: 'self-signed' },
|
||||
{ label: $gettext('Custom Certificate'), value: 'custom' }
|
||||
]"
|
||||
:key="option.value"
|
||||
|
||||
Reference in New Issue
Block a user