From 01a228f3ad6c35b6b661478cb6d0e2a9674595ac Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 05:21:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81=E5=92=8C=E5=AE=89=E5=85=A8=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E9=94=99=E8=AF=AF=E9=A1=B5=E4=BC=AA=E8=A3=85=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=20(#1206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * feat: 实现登录验证码和安全入口错误页伪装功能 - 后端:添加登录验证码功能(密码错误3次后触发) - 后端:支持3种安全入口错误页伪装(418/nginx/close) - 后端:添加验证码API和更新设置项 - 前端:登录页支持验证码输入和刷新 - 前端:设置页添加登录验证码和错误页伪装选项 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: 修复代码审查问题 - hijack失败时回退到418错误页而非返回200 - 验证码输入去除空格 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: 优化细节 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> Co-authored-by: 耗子 --- go.mod | 1 + go.sum | 2 + internal/data/setting.go | 46 ++++++------ internal/http/middleware/entrance.go | 49 +++++++++++- internal/http/middleware/must_login.go | 1 + internal/http/request/setting.go | 46 ++++++------ internal/http/request/user.go | 9 ++- internal/route/http.go | 1 + internal/service/user.go | 47 ++++++++++++ pkg/embed/embed.go | 3 + pkg/embed/error/418.html | 17 +++++ pkg/embed/error/418_zh_CN.html | 17 +++++ pkg/embed/error/nginx_404.html | 7 ++ web/src/api/panel/user/index.ts | 13 +++- web/src/views/login/IndexView.vue | 100 +++++++++++++++++++------ web/src/views/setting/IndexView.vue | 2 + web/src/views/setting/SettingSafe.vue | 56 ++++++++++++-- 17 files changed, 335 insertions(+), 82 deletions(-) create mode 100644 pkg/embed/error/418.html create mode 100644 pkg/embed/error/418_zh_CN.html create mode 100644 pkg/embed/error/nginx_404.html diff --git a/go.mod b/go.mod index 04ef999d..cd50c7bf 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/beevik/ntp v1.5.0 github.com/coder/websocket v1.8.14 github.com/creack/pty v1.1.24 + github.com/dchest/captcha v1.1.0 github.com/expr-lang/expr v1.17.7 github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/httplog/v3 v3.3.0 diff --git a/go.sum b/go.sum index 2b43e86e..38008f95 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/captcha v1.1.0 h1:2kt47EoYUUkaISobUdTbqwx55xvKOJxyScVfw25xzhQ= +github.com/dchest/captcha v1.1.0/go.mod h1:7zoElIawLp7GUMLcj54K9kbw+jEyvz2K0FDdRRYhvWo= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= diff --git a/internal/data/setting.go b/internal/data/setting.go index 4e6f2fc0..4893cd58 100644 --- a/internal/data/setting.go +++ b/internal/data/setting.go @@ -248,27 +248,29 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) { } return &request.SettingPanel{ - Name: name, - Channel: channel, - Locale: r.conf.App.Locale, - Entrance: r.conf.HTTP.Entrance, - OfflineMode: offlineMode, - AutoUpdate: autoUpdate, - Lifetime: r.conf.Session.Lifetime, - IPHeader: r.conf.HTTP.IPHeader, - BindDomain: r.conf.HTTP.BindDomain, - BindIP: r.conf.HTTP.BindIP, - BindUA: r.conf.HTTP.BindUA, - WebsitePath: websitePath, - BackupPath: backupPath, - HiddenMenu: hiddenMenu, - CustomLogo: customLogo, - Port: r.conf.HTTP.Port, - HTTPS: r.conf.HTTP.TLS, - ACME: r.conf.HTTP.ACME, - PublicIP: publicIP, - Cert: crt, - Key: key, + Name: name, + Channel: channel, + Locale: r.conf.App.Locale, + Entrance: r.conf.HTTP.Entrance, + EntranceError: r.conf.HTTP.EntranceError, + LoginCaptcha: r.conf.HTTP.LoginCaptcha, + OfflineMode: offlineMode, + AutoUpdate: autoUpdate, + Lifetime: r.conf.Session.Lifetime, + IPHeader: r.conf.HTTP.IPHeader, + BindDomain: r.conf.HTTP.BindDomain, + BindIP: r.conf.HTTP.BindIP, + BindUA: r.conf.HTTP.BindUA, + WebsitePath: websitePath, + BackupPath: backupPath, + HiddenMenu: hiddenMenu, + CustomLogo: customLogo, + Port: r.conf.HTTP.Port, + HTTPS: r.conf.HTTP.TLS, + ACME: r.conf.HTTP.ACME, + PublicIP: publicIP, + Cert: crt, + Key: key, }, nil } @@ -354,6 +356,8 @@ func (r *settingRepo) UpdatePanel(req *request.SettingPanel) (bool, error) { conf.App.Locale = req.Locale conf.HTTP.Port = req.Port 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.IPHeader = req.IPHeader diff --git a/internal/http/middleware/entrance.go b/internal/http/middleware/entrance.go index 76eb04c3..297ea7a1 100644 --- a/internal/http/middleware/entrance.go +++ b/internal/http/middleware/entrance.go @@ -12,6 +12,7 @@ import ( "github.com/libtnb/sessions" "github.com/acepanel/panel/pkg/config" + "github.com/acepanel/panel/pkg/embed" "github.com/acepanel/panel/pkg/punycode" ) @@ -41,7 +42,7 @@ func Entrance(t *gotext.Locale, conf *config.Config, session *sessions.Manager) } } if len(conf.HTTP.BindDomain) > 0 && !slices.Contains(conf.HTTP.BindDomain, host) { - Abort(w, http.StatusTeapot, t.Get("invalid request domain: %s", r.Host)) + abortEntrance(w, r, conf, conf.App.Locale) return } @@ -77,12 +78,12 @@ func Entrance(t *gotext.Locale, conf *config.Config, session *sessions.Manager) } } if !allowed { - Abort(w, http.StatusTeapot, t.Get("invalid request ip: %s", ip)) + abortEntrance(w, r, conf, conf.App.Locale) return } } if len(conf.HTTP.BindUA) > 0 && !slices.Contains(conf.HTTP.BindUA, r.UserAgent()) { - Abort(w, http.StatusTeapot, t.Get("invalid request user agent: %s", r.UserAgent())) + abortEntrance(w, r, conf, conf.App.Locale) return } @@ -122,7 +123,7 @@ func Entrance(t *gotext.Locale, conf *config.Config, session *sessions.Manager) if !conf.App.Debug && sess.Missing("verify_entrance") && r.URL.Path != "/robots.txt" { - Abort(w, http.StatusTeapot, t.Get("invalid access entrance")) + abortEntrance(w, r, conf, conf.App.Locale) return } @@ -130,3 +131,43 @@ func Entrance(t *gotext.Locale, conf *config.Config, session *sessions.Manager) }) } } + +func abortEntrance(w http.ResponseWriter, r *http.Request, conf *config.Config, locale string) { + errorType := conf.HTTP.EntranceError + + switch errorType { + case "close": + hj, ok := w.(http.Hijacker) + if ok { + conn, _, err := hj.Hijack() + if err == nil { + _ = conn.Close() + return + } + } + // 如果无法 hijack,则返回空响应 + w.WriteHeader(http.StatusTeapot) + return + case "nginx": + content, err := embed.ErrorFS.ReadFile("error/nginx_404.html") + if err != nil { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Server", "nginx") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write(content) + return + default: + fileName := "error/418.html" + if locale == "zh_CN" { + fileName = "error/418_zh_CN.html" + } + content, _ := embed.ErrorFS.ReadFile(fileName) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusTeapot) + _, _ = w.Write(content) + return + } +} diff --git a/internal/http/middleware/must_login.go b/internal/http/middleware/must_login.go index 39820a83..016d736f 100644 --- a/internal/http/middleware/must_login.go +++ b/internal/http/middleware/must_login.go @@ -23,6 +23,7 @@ func MustLogin(t *gotext.Locale, conf *config.Config, session *sessions.Manager, // 白名单 whiteList := []string{ "/api/user/key", + "/api/user/captcha", "/api/user/login", "/api/user/logout", "/api/user/is_login", diff --git a/internal/http/request/setting.go b/internal/http/request/setting.go index e7821a76..075f1825 100644 --- a/internal/http/request/setting.go +++ b/internal/http/request/setting.go @@ -3,28 +3,30 @@ package request import "net/http" type SettingPanel struct { - Name string `json:"name" validate:"required"` - Channel string `json:"channel" validate:"required|in:stable,beta"` - Locale string `json:"locale" validate:"required"` - Entrance string `json:"entrance" validate:"required"` - OfflineMode bool `json:"offline_mode"` - AutoUpdate bool `json:"auto_update"` - TwoFA bool `json:"two_fa"` - Lifetime uint `json:"lifetime" validate:"required|min:10|max:43200"` // 登录超时,单位:分 - IPHeader string `json:"ip_header"` - BindDomain []string `json:"bind_domain"` - BindIP []string `json:"bind_ip"` - BindUA []string `json:"bind_ua"` - WebsitePath string `json:"website_path" validate:"required"` - BackupPath string `json:"backup_path" validate:"required"` - HiddenMenu []string `json:"hidden_menu"` // 隐藏的菜单项 - CustomLogo string `json:"custom_logo" validate:"isFullURL"` // 自定义 Logo URL - Port uint `json:"port" validate:"required|min:1|max:65535"` - HTTPS bool `json:"https"` - ACME bool `json:"acme"` - PublicIP []string `json:"public_ip"` - Cert string `json:"cert" validate:"required"` - Key string `json:"key" validate:"required"` + Name string `json:"name" validate:"required"` + Channel string `json:"channel" validate:"required|in:stable,beta"` + Locale string `json:"locale" validate:"required"` + Entrance string `json:"entrance" validate:"required"` + EntranceError string `json:"entrance_error" validate:"in:418,nginx,close"` // 安全入口错误页伪装类型 + LoginCaptcha bool `json:"login_captcha"` // 登录验证码 + OfflineMode bool `json:"offline_mode"` + AutoUpdate bool `json:"auto_update"` + TwoFA bool `json:"two_fa"` + Lifetime uint `json:"lifetime" validate:"required|min:10|max:43200"` // 登录超时,单位:分 + IPHeader string `json:"ip_header"` + BindDomain []string `json:"bind_domain"` + BindIP []string `json:"bind_ip"` + BindUA []string `json:"bind_ua"` + WebsitePath string `json:"website_path" validate:"required"` + BackupPath string `json:"backup_path" validate:"required"` + HiddenMenu []string `json:"hidden_menu"` // 隐藏的菜单项 + CustomLogo string `json:"custom_logo" validate:"isFullURL"` // 自定义 Logo URL + Port uint `json:"port" validate:"required|min:1|max:65535"` + HTTPS bool `json:"https"` + ACME bool `json:"acme"` + PublicIP []string `json:"public_ip"` + Cert string `json:"cert" validate:"required"` + Key string `json:"key" validate:"required"` } func (r *SettingPanel) Rules(_ *http.Request) map[string]string { diff --git a/internal/http/request/user.go b/internal/http/request/user.go index 68a0c179..4dec2af6 100644 --- a/internal/http/request/user.go +++ b/internal/http/request/user.go @@ -5,10 +5,11 @@ type UserID struct { } type UserLogin struct { - Username string `json:"username" validate:"required"` // encrypted with RSA-OAEP - Password string `json:"password" validate:"required"` // encrypted with RSA-OAEP - SafeLogin bool `json:"safe_login"` - PassCode string `json:"pass_code"` + Username string `json:"username" validate:"required"` // encrypted with RSA-OAEP + Password string `json:"password" validate:"required"` // encrypted with RSA-OAEP + SafeLogin bool `json:"safe_login"` + PassCode string `json:"pass_code"` // 2FA + CaptchaCode string `json:"captcha_code"` // 验证码 } type UserIsTwoFA struct { diff --git a/internal/route/http.go b/internal/route/http.go index e2d8bd8d..e385f536 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -136,6 +136,7 @@ func (route *Http) Register(r *chi.Mux) { r.Route("/api", func(r chi.Router) { r.Route("/user", func(r chi.Router) { r.Get("/key", route.user.GetKey) + r.Get("/captcha", route.user.GetCaptcha) r.With(middleware.Throttle(route.conf.HTTP.IPHeader, 5, time.Minute)).Post("/login", route.user.Login) r.Post("/logout", route.user.Logout) r.Get("/is_login", route.user.IsLogin) diff --git a/internal/service/user.go b/internal/service/user.go index 57c2ae32..3ec6caf4 100644 --- a/internal/service/user.go +++ b/internal/service/user.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "github.com/dchest/captcha" "github.com/leonelquinteros/gotext" "github.com/libtnb/chix" "github.com/libtnb/sessions" @@ -25,6 +26,9 @@ import ( "github.com/acepanel/panel/pkg/rsacrypto" ) +// 登录失败次数阈值,超过此次数需要验证码 +const loginFailThreshold = 3 + type UserService struct { t *gotext.Locale conf *config.Config @@ -65,6 +69,37 @@ func (s *UserService) GetKey(w http.ResponseWriter, r *http.Request) { Success(w, pk) } +// GetCaptcha 获取登录验证码 +func (s *UserService) GetCaptcha(w http.ResponseWriter, r *http.Request) { + sess, err := s.session.GetSession(r) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + failCount := cast.ToInt(sess.Get("login_fail_count")) + if !s.conf.HTTP.LoginCaptcha || failCount < loginFailThreshold { + Success(w, chix.M{ + "required": false, + }) + return + } + + captchaID := captcha.NewLen(4) + sess.Put("captcha_id", captchaID) + + var buf bytes.Buffer + if err := captcha.WriteImage(&buf, captchaID, 150, 50); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, chix.M{ + "required": true, + "image": base64.StdEncoding.EncodeToString(buf.Bytes()), + }) +} + func (s *UserService) Login(w http.ResponseWriter, r *http.Request) { sess, err := s.session.GetSession(r) if err != nil { @@ -78,6 +113,16 @@ func (s *UserService) Login(w http.ResponseWriter, r *http.Request) { return } + failCount := cast.ToInt(sess.Get("login_fail_count")) + if s.conf.HTTP.LoginCaptcha && failCount >= loginFailThreshold { + captchaID, ok := sess.Get("captcha_id").(string) + if !ok || captchaID == "" || !captcha.VerifyString(captchaID, req.CaptchaCode) { + Error(w, http.StatusForbidden, s.t.Get("invalid captcha code")) + return + } + sess.Forget("captcha_id") + } + key, ok := sess.Get("key").(rsa.PrivateKey) if !ok { Error(w, http.StatusForbidden, s.t.Get("invalid key, please refresh the page")) @@ -88,6 +133,7 @@ func (s *UserService) Login(w http.ResponseWriter, r *http.Request) { decryptedPassword, _ := rsacrypto.DecryptData(&key, req.Password) user, err := s.userRepo.CheckPassword(string(decryptedUsername), string(decryptedPassword)) if err != nil { + sess.Put("login_fail_count", failCount+1) Error(w, http.StatusForbidden, "%v", err) return } @@ -128,6 +174,7 @@ func (s *UserService) Login(w http.ResponseWriter, r *http.Request) { sess.Put("user_id", user.ID) sess.Put("refresh_at", time.Now().Unix()) sess.Forget("key") + sess.Forget("login_fail_count") Success(w, nil) } diff --git a/pkg/embed/embed.go b/pkg/embed/embed.go index 5a1d309d..e8fb0a38 100644 --- a/pkg/embed/embed.go +++ b/pkg/embed/embed.go @@ -10,3 +10,6 @@ var WebsiteFS embed.FS //go:embed all:locales/* var LocalesFS embed.FS + +//go:embed all:error/* +var ErrorFS embed.FS diff --git a/pkg/embed/error/418.html b/pkg/embed/error/418.html new file mode 100644 index 00000000..d6083f9b --- /dev/null +++ b/pkg/embed/error/418.html @@ -0,0 +1,17 @@ + + + + + + 418 I'm a teapot + + + +
+

418 I'm a teapot

+

You are accessing AcePanel through the wrong entrance.

+

If you have forgotten the entrance, please run acepanel entrance off to disable it.

+

Powered by AcePanel

+
+ + diff --git a/pkg/embed/error/418_zh_CN.html b/pkg/embed/error/418_zh_CN.html new file mode 100644 index 00000000..cde51c5e --- /dev/null +++ b/pkg/embed/error/418_zh_CN.html @@ -0,0 +1,17 @@ + + + + + + 418 I'm a teapot + + + +
+

418 I'm a teapot

+

您正在使用错误的入口访问 AcePanel。

+

如果您忘记了入口,请运行 acepanel entrance off 将其关闭。

+

AcePanel 强力驱动

+
+ + diff --git a/pkg/embed/error/nginx_404.html b/pkg/embed/error/nginx_404.html new file mode 100644 index 00000000..99d83c01 --- /dev/null +++ b/pkg/embed/error/nginx_404.html @@ -0,0 +1,7 @@ + +404 Not Found + +

404 Not Found

+
nginx
+ + diff --git a/web/src/api/panel/user/index.ts b/web/src/api/panel/user/index.ts index 85466234..c27fce29 100644 --- a/web/src/api/panel/user/index.ts +++ b/web/src/api/panel/user/index.ts @@ -3,13 +3,22 @@ import { http } from '@/utils' export default { // 公钥 key: () => http.Get('/user/key'), + // 获取验证码 + captcha: () => http.Get('/user/captcha'), // 登录 - login: (username: string, password: string, pass_code: string, safe_login: boolean) => + login: ( + username: string, + password: string, + pass_code: string, + safe_login: boolean, + captcha_code: string + ) => http.Post('/user/login', { username, password, pass_code, - safe_login + safe_login, + captcha_code }), // 登出 logout: () => http.Post('/user/logout'), diff --git a/web/src/views/login/IndexView.vue b/web/src/views/login/IndexView.vue index 11055890..80437d33 100644 --- a/web/src/views/login/IndexView.vue +++ b/web/src/views/login/IndexView.vue @@ -20,13 +20,15 @@ interface LoginInfo { password: string safe_login: boolean pass_code: string + captcha_code: string } const loginInfo = ref({ username: '', password: '', safe_login: true, - pass_code: '' + pass_code: '', + captcha_code: '' }) const localLoginInfo = getLocal('loginInfo') as LoginInfo @@ -41,14 +43,42 @@ const logining = ref(false) const isRemember = useStorage('isRemember', false) const showTwoFA = ref(false) +// 验证码相关 +const captchaRequired = ref(false) +const captchaImage = ref('') + const logo = computed(() => themeStore.logo || logoImg) +// 刷新验证码 +const refreshCaptcha = () => { + useRequest(user.captcha()) + .onSuccess(({ data }) => { + captchaRequired.value = Boolean(data.required) + captchaImage.value = data.image || '' + loginInfo.value.captcha_code = '' + }) + .onError(() => { + captchaRequired.value = false + captchaImage.value = '' + }) +} + +// 初始加载验证码 +onMounted(() => { + refreshCaptcha() +}) + async function handleLogin() { - const { username, password, pass_code, safe_login } = loginInfo.value + const { username, password, pass_code, safe_login, captcha_code } = loginInfo.value if (!username || !password) { window.$message.warning($gettext('Please enter username and password')) return } + const trimmedCaptcha = captcha_code?.trim() || '' + if (captchaRequired.value && !trimmedCaptcha) { + window.$message.warning($gettext('Please enter captcha code')) + return + } if (!key) { window.$message.warning( $gettext('Failed to get encryption public key, please refresh the page and try again') @@ -60,29 +90,35 @@ async function handleLogin() { rsaEncrypt(username, String(unref(key))), rsaEncrypt(password, String(unref(key))), pass_code, - safe_login + safe_login, + trimmedCaptcha ) - ).onSuccess(async () => { - logining.value = true - window.$notification?.success({ title: $gettext('Login successful!'), duration: 2500 }) - if (isRemember.value) { - setLocal('loginInfo', { username, password }) - } else { - removeLocal('loginInfo') - } + ) + .onSuccess(async () => { + logining.value = true + window.$notification?.success({ title: $gettext('Login successful!'), duration: 2500 }) + if (isRemember.value) { + setLocal('loginInfo', { username, password }) + } else { + removeLocal('loginInfo') + } - await addDynamicRoutes() - useRequest(user.info()).onSuccess(({ data }) => { - userStore.set(data as any) + await addDynamicRoutes() + useRequest(user.info()).onSuccess(({ data }) => { + userStore.set(data as any) + }) + if (query.redirect) { + const path = query.redirect as string + Reflect.deleteProperty(query, 'redirect') + await router.push({ path, query }) + } else { + await router.push('/') + } + }) + .onError(() => { + // 登录失败后刷新验证码状态 + refreshCaptcha() }) - if (query.redirect) { - const path = query.redirect as string - Reflect.deleteProperty(query, 'redirect') - await router.push({ path, query }) - } else { - await router.push('/') - } - }) logining.value = false } @@ -155,6 +191,26 @@ watch(isLogin, async () => { @keydown.enter="handleLogin" /> +
+ + + + +
diff --git a/web/src/views/setting/IndexView.vue b/web/src/views/setting/IndexView.vue index eecea77d..98c29fe2 100644 --- a/web/src/views/setting/IndexView.vue +++ b/web/src/views/setting/IndexView.vue @@ -26,6 +26,8 @@ const { data: model } = useRequest(setting.list, { locale: 'en', port: 8888, entrance: '', + entrance_error: '418', + login_captcha: false, offline_mode: false, two_fa: false, lifetime: 0, diff --git a/web/src/views/setting/SettingSafe.vue b/web/src/views/setting/SettingSafe.vue index e07929f2..f9022a5d 100644 --- a/web/src/views/setting/SettingSafe.vue +++ b/web/src/views/setting/SettingSafe.vue @@ -29,12 +29,6 @@ const httpsMode = computed({ } } }) - -const httpsModeOptions = computed(() => [ - { label: $gettext('Disabled'), value: 'off' }, - { label: $gettext('ACME (Auto)'), value: 'acme' }, - { label: $gettext('Custom Certificate'), value: 'custom' } -]) + + + + + + + +