From 981ba8abe17948a98460ea12a0058bf8a83b2129 Mon Sep 17 00:00:00 2001 From: Muse Date: Tue, 20 Oct 2026 10:05:29 +0000 Subject: [PATCH] =?UTF-8?q?=E7=B2=BE=E7=AE=80:=20=E5=8E=BB=E6=8E=89?= =?UTF-8?q?=E5=8F=8C=E9=87=8D=E9=AA=8C=E8=AF=81(2FA):=20=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C/=E8=AE=BE=E7=BD=AE=E5=AE=88=E5=8D=AB/VerifyT?= =?UTF-8?q?woFactorCode/CLI=20resetTwoFactor=20=E6=A0=87=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/web/controller/index.go | 16 +++------ internal/web/controller/setting.go | 45 ++++++------------------- internal/web/entity/entity.go | 15 ++++----- internal/web/service/panel/user.go | 31 +---------------- internal/web/service/setting.go | 53 ++---------------------------- main.go | 21 +++--------- 6 files changed, 28 insertions(+), 153 deletions(-) diff --git a/internal/web/controller/index.go b/internal/web/controller/index.go index 5cef02e..f6c1cf7 100644 --- a/internal/web/controller/index.go +++ b/internal/web/controller/index.go @@ -17,9 +17,8 @@ import ( // LoginForm represents the login request structure. type LoginForm struct { - Username string `json:"username" form:"username"` - Password string `json:"password" form:"password"` - TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"` + Username string `json:"username" form:"username"` + Password string `json:"password" form:"password"` } // IndexController handles the main index and login-related routes. @@ -38,14 +37,13 @@ func NewIndexController(g *gin.RouterGroup) *IndexController { return a } -// initRouter sets up the routes for index, login, logout, and two-factor authentication. +// initRouter sets up the routes for index, login, and logout. func (a *IndexController) initRouter(g *gin.RouterGroup) { g.GET("/", a.index) g.GET("/csrf-token", a.csrfToken) g.POST("/login", middleware.CSRFMiddleware(), a.login) g.POST("/logout", middleware.CSRFMiddleware(), a.logout) - g.POST("/getTwoFactorEnable", middleware.CSRFMiddleware(), a.getTwoFactorEnable) } // index handles the root route, redirecting logged-in users to the panel or showing the login page. @@ -92,7 +90,7 @@ func (a *IndexController) login(c *gin.Context) { return } - user, checkErr := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode) + user, checkErr := a.userService.CheckUser(form.Username, form.Password) if user == nil { reason := loginFailureReason(checkErr) @@ -159,9 +157,3 @@ func (a *IndexController) csrfToken(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"success": true, "obj": token}) } - -// getTwoFactorEnable retrieves the current status of two-factor authentication. -func (a *IndexController) getTwoFactorEnable(c *gin.Context) { - status, err := a.settingService.GetTwoFactorEnable() - jsonObj(c, status, err) -} diff --git a/internal/web/controller/setting.go b/internal/web/controller/setting.go index 4414d4a..17bd2ac 100644 --- a/internal/web/controller/setting.go +++ b/internal/web/controller/setting.go @@ -4,7 +4,6 @@ import ( "errors" "net/http" "strconv" - "strings" "time" "github.com/mhsanaei/3x-ui/v3/internal/logger" @@ -21,22 +20,20 @@ import ( // updateUserForm represents the form for updating user credentials. type updateUserForm struct { - OldUsername string `json:"oldUsername" form:"oldUsername"` - OldPassword string `json:"oldPassword" form:"oldPassword"` - NewUsername string `json:"newUsername" form:"newUsername"` - NewPassword string `json:"newPassword" form:"newPassword"` - TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"` + OldUsername string `json:"oldUsername" form:"oldUsername"` + OldPassword string `json:"oldPassword" form:"oldPassword"` + NewUsername string `json:"newUsername" form:"newUsername"` + NewPassword string `json:"newPassword" form:"newPassword"` } -// updateSettingForm carries the persisted settings plus request-scoped fields -// that must never land in the settings table: the 2FA confirmation code and -// the explicit clear flags for redacted secrets (a blank secret alone means -// "unchanged", so clearing needs its own signal — see #5724). +// updateSettingForm carries the persisted settings plus the explicit clear +// flags for redacted secrets, which must never land in the settings table (a +// blank secret alone means "unchanged", so clearing needs its own signal — +// see #5724). type updateSettingForm struct { entity.AllSetting - TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"` - ClearTgBotToken bool `json:"clearTgBotToken" form:"clearTgBotToken"` - ClearSmtpPassword bool `json:"clearSmtpPassword" form:"clearSmtpPassword"` + ClearTgBotToken bool `json:"clearTgBotToken" form:"clearTgBotToken"` + ClearSmtpPassword bool `json:"clearSmtpPassword" form:"clearSmtpPassword"` } type validateRegexForm struct { @@ -124,33 +121,15 @@ func (a *SettingController) updateSetting(c *gin.Context) { return } allSetting := &form.AllSetting - oldTwoFactor, twoFactorErr := a.settingService.GetTwoFactorEnable() oldPanelOutbound, _ := a.settingService.GetPanelOutbound() oldTgEnable, _ := a.settingService.GetTgbotEnabled() oldTgToken, _ := a.settingService.GetTgBotToken() oldTgChatId, _ := a.settingService.GetTgBotChatId() oldTgAPIServer, _ := a.settingService.GetTgBotAPIServer() - if twoFactorErr == nil && oldTwoFactor { - // Rebinding the authenticator is the same class of change as turning 2FA - // off, so both need a current code. Blank still means "unchanged". - submittedToken := strings.TrimSpace(allSetting.TwoFactorToken) - storedToken, _ := a.settingService.GetTwoFactorToken() - if !allSetting.TwoFactorEnable || (submittedToken != "" && submittedToken != storedToken) { - if err := a.settingService.VerifyTwoFactorCode(form.TwoFactorCode); err != nil { - jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) - return - } - } - } err := a.settingService.UpdateAllSetting(allSetting, service.SecretClears{ TgBotToken: form.ClearTgBotToken, SmtpPassword: form.ClearSmtpPassword, }) - if err == nil && twoFactorErr == nil && !oldTwoFactor && allSetting.TwoFactorEnable { - if bumpErr := a.userService.BumpLoginEpoch(); bumpErr != nil { - err = bumpErr - } - } if err == nil && form.PanelOutbound != oldPanelOutbound { // The egress bridge lives in the generated config; reconcile the // running core. One SOCKS inbound plus one routing rule — both @@ -189,10 +168,6 @@ func (a *SettingController) updateUser(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty"))) return } - if err := a.settingService.VerifyTwoFactorCode(form.TwoFactorCode); err != nil { - jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), err) - return - } err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword) if err == nil { user.Username = form.NewUsername diff --git a/internal/web/entity/entity.go b/internal/web/entity/entity.go index a4a003a..489b2a3 100644 --- a/internal/web/entity/entity.go +++ b/internal/web/entity/entity.go @@ -64,9 +64,7 @@ type AllSetting struct { OutboundDownThreshold int `json:"outboundDownThreshold" form:"outboundDownThreshold" validate:"gte=1,lte=100"` - TimeLocation string `json:"timeLocation" form:"timeLocation"` - TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` - TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` + TimeLocation string `json:"timeLocation" form:"timeLocation"` SubEnable bool `json:"subEnable" form:"subEnable"` SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"` @@ -113,12 +111,11 @@ type AllSetting struct { type AllSettingView struct { AllSetting - HasTgBotToken bool `json:"hasTgBotToken"` - HasTwoFactorToken bool `json:"hasTwoFactorToken"` - HasApiToken bool `json:"hasApiToken"` - HasWarpSecret bool `json:"hasWarpSecret"` - HasNordSecret bool `json:"hasNordSecret"` - HasSmtpPassword bool `json:"hasSmtpPassword"` + HasTgBotToken bool `json:"hasTgBotToken"` + HasApiToken bool `json:"hasApiToken"` + HasWarpSecret bool `json:"hasWarpSecret"` + HasNordSecret bool `json:"hasNordSecret"` + HasSmtpPassword bool `json:"hasSmtpPassword"` } func pathHasForbiddenChar(s string) bool { diff --git a/internal/web/service/panel/user.go b/internal/web/service/panel/user.go index 3971f67..b5f9d63 100644 --- a/internal/web/service/panel/user.go +++ b/internal/web/service/panel/user.go @@ -3,7 +3,6 @@ package panel import ( "errors" - "github.com/xlzd/gotp" "gorm.io/gorm" "github.com/mhsanaei/3x-ui/v3/internal/database" @@ -34,7 +33,7 @@ func (s *UserService) GetFirstUser() (*model.User, error) { return user, nil } -func (s *UserService) CheckUser(username string, password string, twoFactorCode string) (*model.User, error) { +func (s *UserService) CheckUser(username string, password string) (*model.User, error) { db := database.GetDB() user := &model.User{} @@ -54,24 +53,6 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode return nil, errors.New("invalid credentials") } - twoFactorEnable, err := s.settingService.GetTwoFactorEnable() - if err != nil { - logger.Warning("check two factor err:", err) - return nil, err - } - - if twoFactorEnable { - twoFactorToken, err := s.settingService.GetTwoFactorToken() - if err != nil { - logger.Warning("check two factor token err:", err) - return nil, err - } - - if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode { - return nil, errors.New("invalid 2fa code") - } - } - return user, nil } @@ -90,16 +71,6 @@ func (s *UserService) UpdateUser(id int, username string, password string) error return err } - twoFactorEnable, err := s.settingService.GetTwoFactorEnable() - if err != nil { - return err - } - - if twoFactorEnable { - _ = s.settingService.SetTwoFactorEnable(false) - _ = s.settingService.SetTwoFactorToken("") - } - return db.Model(model.User{}). Where("id = ?", id). Updates(map[string]any{ diff --git a/internal/web/service/setting.go b/internal/web/service/setting.go index 82bb4b0..47730cb 100644 --- a/internal/web/service/setting.go +++ b/internal/web/service/setting.go @@ -15,7 +15,6 @@ import ( "time" "github.com/google/uuid" - "github.com/xlzd/gotp" "gorm.io/gorm" "github.com/mhsanaei/3x-ui/v3/internal/config" @@ -81,8 +80,6 @@ var defaultValueMap = map[string]string{ "tgCpu": "80", "tgMemory": "80", "tgLang": "en-US", - "twoFactorEnable": "false", - "twoFactorToken": "", "subEnable": "true", "subJsonEnable": "false", "subJsonAutoDetect": "false", @@ -246,7 +243,6 @@ func (s *SettingService) GetAllSettingView() (*entity.AllSettingView, error) { } view := &entity.AllSettingView{AllSetting: *allSetting} view.HasTgBotToken = secretConfigured(allSetting.TgBotToken) - view.HasTwoFactorToken = secretConfigured(allSetting.TwoFactorToken) view.HasWarpSecret = secretConfigured(mustString(s.GetWarp())) view.HasNordSecret = secretConfigured(mustString(s.GetNord())) view.HasSmtpPassword = secretConfigured(allSetting.SmtpPassword) @@ -255,7 +251,6 @@ func (s *SettingService) GetAllSettingView() (*entity.AllSettingView, error) { view.HasApiToken = apiTokenCount > 0 } view.TgBotToken = "" - view.TwoFactorToken = "" view.SmtpPassword = "" return view, nil } @@ -556,40 +551,6 @@ func (s *SettingService) GetTgLang() (string, error) { return s.getString("tgLang") } -func (s *SettingService) GetTwoFactorEnable() (bool, error) { - return s.getBool("twoFactorEnable") -} - -func (s *SettingService) SetTwoFactorEnable(value bool) error { - return s.setBool("twoFactorEnable", value) -} - -func (s *SettingService) GetTwoFactorToken() (string, error) { - return s.getString("twoFactorToken") -} - -func (s *SettingService) SetTwoFactorToken(value string) error { - return s.setString("twoFactorToken", value) -} - -func (s *SettingService) VerifyTwoFactorCode(code string) error { - enabled, err := s.GetTwoFactorEnable() - if err != nil { - return err - } - if !enabled { - return nil - } - token, err := s.GetTwoFactorToken() - if err != nil { - return err - } - if strings.TrimSpace(token) == "" || !gotp.NewDefaultTOTP(token).Verify(strings.TrimSpace(code), time.Now().Unix()) { - return common.NewError("invalid two factor code") - } - return nil -} - func (s *SettingService) GetPort() (int, error) { return s.getInt("webPort") } @@ -1167,13 +1128,6 @@ func (s *SettingService) preserveRedactedSecrets(allSetting *entity.AllSetting, } allSetting.TgBotToken = value } - if allSetting.TwoFactorEnable && strings.TrimSpace(allSetting.TwoFactorToken) == "" { - value, err := s.GetTwoFactorToken() - if err != nil { - return err - } - allSetting.TwoFactorToken = value - } if !clears.SmtpPassword && strings.TrimSpace(allSetting.SmtpPassword) == "" { value, err := s.GetSmtpPassword() if err != nil { @@ -1223,7 +1177,7 @@ func validateRemoteRoutingURLSetting(name string, value *string) error { func (s *SettingService) UpdateSecret(key string, value string) error { switch key { - case "tgBotToken", "twoFactorToken": + case "tgBotToken": return s.saveSetting(key, strings.TrimSpace(value)) default: return common.NewError("secret key is not replaceable:", key) @@ -1356,9 +1310,8 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) { } var factoryDefaultSecretKeys = map[string]bool{ - "tgBotToken": true, - "twoFactorToken": true, - "smtpPassword": true, + "tgBotToken": true, + "smtpPassword": true, } /* diff --git a/main.go b/main.go index 816b9c3..f45c361 100644 --- a/main.go +++ b/main.go @@ -14,11 +14,11 @@ import ( "syscall" _ "unsafe" + "github.com/mhsanaei/3x-ui/v3/internal/agent" "github.com/mhsanaei/3x-ui/v3/internal/config" "github.com/mhsanaei/3x-ui/v3/internal/crypto/nodetoken" "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/logger" - "github.com/mhsanaei/3x-ui/v3/internal/agent" "github.com/mhsanaei/3x-ui/v3/internal/sub" "github.com/mhsanaei/3x-ui/v3/internal/tunnelmonitor" "github.com/mhsanaei/3x-ui/v3/internal/util/crypto" @@ -396,8 +396,8 @@ func encryptNodeTokens() { fmt.Printf("node-token migration complete: %d re-encrypted, %d already current/skipped\n", changed, skipped) } -// updateSetting updates various panel settings including port, credentials, base path, listen IP, and two-factor authentication. -func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) error { +// updateSetting updates various panel settings including port, credentials, base path, and listen IP. +func updateSetting(port int, username string, password string, webBasePath string, listenIP string) error { err := database.InitDB(config.GetDBPath()) if err != nil { fmt.Println("Database initialization failed:", err) @@ -434,17 +434,6 @@ func updateSetting(port int, username string, password string, webBasePath strin } } - if resetTwoFactor { - err := settingService.SetTwoFactorEnable(false) - - if err != nil { - fmt.Println("Failed to reset two-factor authentication:", err) - } else { - _ = settingService.SetTwoFactorToken("") - fmt.Println("Two-factor authentication reset successfully") - } - } - if listenIP != "" { err := settingService.SetListen(listenIP) if err != nil { @@ -644,7 +633,6 @@ func main() { var show bool var getCert bool var getApiToken bool - var resetTwoFactor bool settingCmd.BoolVar(&reset, "reset", false, "Reset all settings") settingCmd.BoolVar(&show, "show", false, "Display current settings") settingCmd.IntVar(&port, "port", 0, "Set panel port number") @@ -652,7 +640,6 @@ func main() { settingCmd.StringVar(&password, "password", "", "Set login password") settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel") settingCmd.StringVar(&listenIP, "listenIP", "", "set panel listenIP IP") - settingCmd.BoolVar(&resetTwoFactor, "resetTwoFactor", false, "Reset two-factor authentication settings") settingCmd.BoolVar(&getListen, "getListen", false, "Display current panel listenIP IP") settingCmd.BoolVar(&getCert, "getCert", false, "Display current certificate settings") settingCmd.BoolVar(&getApiToken, "getApiToken", false, "Display current API token") @@ -734,7 +721,7 @@ func main() { return } } else { - if err = updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor); err != nil { + if err = updateSetting(port, username, password, webBasePath, listenIP); err != nil { return } }