feat(auth): block panel with default admin/admin credentials and guide credential change

checkLogin middleware now detects default admin/admin credentials and
redirects every panel route to /panel/settings until they are changed.
The settings page auto-opens the Authentication tab, shows a
non-dismissible error banner, and lists 'Default credentials' first in
the security checklist. Login response includes mustChangeCredentials
so the login page can redirect directly. Logout is now POST-only.
Password must be at least 10 characters and cannot be admin/admin.
This commit is contained in:
farhadh
2026-05-11 21:09:48 +02:00
parent ce88b0b432
commit 56ce6073ce
8 changed files with 228 additions and 42 deletions
+45 -2
View File
@@ -2,6 +2,7 @@ package controller
import (
"errors"
"strings"
"time"
"github.com/mhsanaei/3x-ui/v3/util/crypto"
@@ -20,6 +21,15 @@ type updateUserForm struct {
NewPassword string `json:"newPassword" form:"newPassword"`
}
type verifyTwoFactorForm struct {
Code string `json:"code" form:"code"`
}
type updateSecretForm struct {
Key string `json:"key" form:"key"`
Value string `json:"value" form:"value"`
}
// SettingController handles settings and user management operations.
type SettingController struct {
settingService service.SettingService
@@ -41,7 +51,9 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
g.POST("/all", a.getAllSetting)
g.POST("/defaultSettings", a.getDefaultSettings)
g.POST("/update", a.updateSetting)
g.POST("/secret", a.updateSecret)
g.POST("/updateUser", a.updateUser)
g.POST("/verifyTwoFactor", a.verifyTwoFactor)
g.POST("/restartPanel", a.restartPanel)
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
g.GET("/getApiToken", a.getApiToken)
@@ -50,7 +62,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
// getAllSetting retrieves all current settings.
func (a *SettingController) getAllSetting(c *gin.Context) {
allSetting, err := a.settingService.GetAllSetting()
allSetting, err := a.settingService.GetAllSettingView()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
@@ -80,6 +92,16 @@ func (a *SettingController) updateSetting(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
}
func (a *SettingController) updateSecret(c *gin.Context) {
form := &updateSecretForm{}
if err := c.ShouldBind(form); err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
return
}
err := a.settingService.UpdateSecret(form.Key, form.Value)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
}
// updateUser updates the current user's username and password.
func (a *SettingController) updateUser(c *gin.Context) {
form := &updateUserForm{}
@@ -93,10 +115,18 @@ func (a *SettingController) updateUser(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
return
}
if form.NewUsername == "" || form.NewPassword == "" {
if strings.TrimSpace(form.NewUsername) == "" || form.NewPassword == "" {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
return
}
if len(form.NewPassword) < 10 {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New("new password must be at least 10 characters"))
return
}
if strings.TrimSpace(form.NewUsername) == "admin" && form.NewPassword == "admin" {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New("default admin/admin credentials are not allowed"))
return
}
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
if err == nil {
user.Username = form.NewUsername
@@ -108,6 +138,19 @@ func (a *SettingController) updateUser(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
}
func (a *SettingController) verifyTwoFactor(c *gin.Context) {
form := &verifyTwoFactorForm{}
if err := c.ShouldBind(form); err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
ok, err := a.userService.VerifyTwoFactorCode(form.Code)
if err == nil && !ok {
err = errors.New("invalid 2fa code")
}
jsonObj(c, ok, err)
}
// restartPanel restarts the panel service after a delay.
func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3)