diff --git a/frontend/src/pages/login/LoginPage.vue b/frontend/src/pages/login/LoginPage.vue
index bbccbcef..fab7ba9d 100644
--- a/frontend/src/pages/login/LoginPage.vue
+++ b/frontend/src/pages/login/LoginPage.vue
@@ -52,9 +52,7 @@ async function login() {
submitting.value = true;
try {
const msg = await HttpUtil.post('/login', user);
- if (msg.success) {
- window.location.href = basePath + (msg.obj?.mustChangeCredentials ? 'panel/settings' : 'panel/');
- }
+ if (msg.success) window.location.href = basePath + 'panel/';
} finally {
submitting.value = false;
}
diff --git a/frontend/src/pages/settings/SecurityTab.vue b/frontend/src/pages/settings/SecurityTab.vue
index 3edb1f3b..d841c787 100644
--- a/frontend/src/pages/settings/SecurityTab.vue
+++ b/frontend/src/pages/settings/SecurityTab.vue
@@ -21,10 +21,9 @@ const tfa = reactive({
description: '',
token: '',
type: 'set',
- // resolveConfirm is called by the modal's @confirm with the success bool
- // and, for redacted-token confirm flows, the code entered by the user.
+ // resolveConfirm is called by the modal's @confirm with the success bool;
// it then routes the value back to whichever flow opened the modal.
- resolveConfirm: (_success, _code) => { },
+ resolveConfirm: (_success) => { },
});
function openTfa({ title, description = '', token = '', type, onConfirm }) {
@@ -36,8 +35,8 @@ function openTfa({ title, description = '', token = '', type, onConfirm }) {
tfa.open = true;
}
-function onTfaConfirm(success, code = '') {
- tfa.resolveConfirm(success, code);
+function onTfaConfirm(success) {
+ tfa.resolveConfirm(success);
}
const user = reactive({
@@ -53,23 +52,16 @@ async function sendUpdateUser() {
try {
const msg = await HttpUtil.post('/panel/setting/updateUser', user);
if (msg?.success) {
- await logoutAndReturn();
+ // Force re-login at the standard logout path; basePath is handled
+ // by the Go router so a relative redirect is correct here.
+ const basePath = window.X_UI_BASE_PATH || '';
+ window.location.replace(`${basePath}logout`);
}
} finally {
updating.value = false;
}
}
-async function logoutAndReturn() {
- await HttpUtil.post('/logout');
- window.location.replace(window.X_UI_BASE_PATH || '/');
-}
-
-async function verifyTwoFactor(code) {
- const msg = await HttpUtil.post('/panel/setting/verifyTwoFactor', { code });
- return !!(msg?.success && msg.obj === true);
-}
-
function updateUser() {
if (props.allSetting.twoFactorEnable) {
openTfa({
@@ -77,11 +69,7 @@ function updateUser() {
description: t('pages.settings.security.twoFactorModalChangeCredentialsStep'),
token: props.allSetting.twoFactorToken,
type: 'confirm',
- onConfirm: async (ok, code) => {
- if (!ok) return;
- const verified = props.allSetting.twoFactorToken ? ok : await verifyTwoFactor(code);
- if (verified) sendUpdateUser();
- },
+ onConfirm: (ok) => { if (ok) sendUpdateUser(); },
});
} else {
sendUpdateUser();
@@ -100,10 +88,7 @@ async function loadApiToken() {
apiTokenLoading.value = true;
try {
const msg = await HttpUtil.get('/panel/setting/getApiToken');
- if (msg?.success) {
- apiToken.value = msg.obj || '';
- props.allSetting.hasApiToken = !!apiToken.value;
- }
+ if (msg?.success) apiToken.value = msg.obj || '';
} finally {
apiTokenLoading.value = false;
}
@@ -139,7 +124,6 @@ function regenerateApiToken() {
const msg = await HttpUtil.post('/panel/setting/regenerateApiToken');
if (msg?.success) {
apiToken.value = msg.obj || '';
- props.allSetting.hasApiToken = !!apiToken.value;
message.success(t('success'));
}
} finally {
@@ -163,7 +147,6 @@ function toggleTwoFactor() {
if (ok) {
message.success(t('pages.settings.security.twoFactorModalSetSuccess'));
props.allSetting.twoFactorToken = newToken;
- props.allSetting.hasTwoFactorToken = true;
}
props.allSetting.twoFactorEnable = ok;
},
@@ -174,14 +157,11 @@ function toggleTwoFactor() {
description: t('pages.settings.security.twoFactorModalRemoveStep'),
token: props.allSetting.twoFactorToken,
type: 'confirm',
- onConfirm: async (ok, code) => {
+ onConfirm: (ok) => {
if (!ok) return;
- const verified = props.allSetting.twoFactorToken ? ok : await verifyTwoFactor(code);
- if (!verified) return;
message.success(t('pages.settings.security.twoFactorModalDeleteSuccess'));
props.allSetting.twoFactorEnable = false;
props.allSetting.twoFactorToken = '';
- props.allSetting.hasTwoFactorToken = false;
},
});
}
diff --git a/frontend/src/pages/settings/SettingsPage.vue b/frontend/src/pages/settings/SettingsPage.vue
index 2166a259..5f2ddf39 100644
--- a/frontend/src/pages/settings/SettingsPage.vue
+++ b/frontend/src/pages/settings/SettingsPage.vue
@@ -26,9 +26,6 @@ const { t } = useI18n();
const { fetched, spinning, saveDisabled, allSetting, saveAll } = useAllSetting();
const { isMobile } = useMediaQuery();
-const mustChangeCredentials = window.X_UI_MUST_CHANGE_CREDENTIALS === true
-const activeTab = ref(mustChangeCredentials ? '2' : '1')
-
const basePath = window.X_UI_BASE_PATH || '';
const requestUri = window.location.pathname;
@@ -120,68 +117,39 @@ function restartPanel() {
});
}
-const securityChecklist = computed(() => {
- const segs = window.location.pathname.split('/').length < 4;
- const out = []
- if (mustChangeCredentials) {
- out.push({
- label: 'Default credentials',
- ok: false,
- action: 'Change the default admin/admin credentials in Authentication settings.',
- })
+// Conf alerts mirror the legacy banner — pure derivation off allSetting.
+const confAlerts = computed(() => {
+ const out = [];
+ if (window.location.protocol !== 'https:') {
+ out.push('Panel is served over plain HTTP — set up TLS for production.');
+ }
+ if (allSetting.webPort === 2053) {
+ out.push('Default port 2053 is well-known — change it to a random port.');
+ }
+ const segs = window.location.pathname.split('/').length < 4;
+ if (segs && allSetting.webBasePath === '/') {
+ out.push('Default base path "/" is well-known — change it to a random path.');
}
- out.push(
- {
- label: 'TLS',
- ok: window.location.protocol === 'https:',
- action: 'Set certificate and key paths, then restart.',
- },
- {
- label: 'Base path',
- ok: !(segs && allSetting.webBasePath === '/'),
- action: 'Change the panel URL path from "/".',
- },
- {
- label: 'Panel port',
- ok: allSetting.webPort !== 2053,
- action: 'Use a non-default listening port.',
- },
- {
- label: 'Two-factor authentication',
- ok: allSetting.twoFactorEnable && allSetting.hasTwoFactorToken,
- action: 'Enable 2FA in Security.',
- },
- {
- label: 'API token',
- ok: allSetting.hasApiToken,
- action: 'Generate or rotate the API token in Security.',
- },
- )
if (allSetting.subEnable) {
let subPath = allSetting.subPath;
if (allSetting.subURI) {
try { subPath = new URL(allSetting.subURI).pathname; } catch (_e) { }
}
- out.push({
- label: 'Subscription path',
- ok: subPath !== '/sub/',
- action: 'Change the default subscription path.',
- });
+ if (subPath === '/sub/') {
+ out.push('Default subscription path "/sub/" is well-known — change it.');
+ }
}
if (allSetting.subJsonEnable) {
let p = allSetting.subJsonPath;
if (allSetting.subJsonURI) {
try { p = new URL(allSetting.subJsonURI).pathname; } catch (_e) { }
}
- out.push({
- label: 'JSON subscription path',
- ok: p !== '/json/',
- action: 'Change the default JSON subscription path.',
- });
+ if (p === '/json/') {
+ out.push('Default JSON subscription path "/json/" is well-known — change it.');
+ }
}
return out;
});
-const hasSecurityGaps = computed(() => securityChecklist.value.some((item) => !item.ok));
const alertVisible = ref(true);
@@ -197,31 +165,14 @@ const alertVisible = ref(true);
-
-
-
- Security posture checklist
+ Security warnings
-
-
-
-
- {{ item.ok ? 'OK' : 'Action' }}
- {{ item.label }}
- {{ item.ok ? 'Configured' : item.action }}
-
-
-
-
+ Your panel may be exposed:
+
@@ -248,7 +199,7 @@ const alertVisible = ref(true);
-
+
@@ -335,11 +286,6 @@ const alertVisible = ref(true);
margin-bottom: 10px;
}
-.checklist-item {
- padding-left: 0 !important;
- padding-right: 0 !important;
-}
-
.header-row {
display: flex;
flex-wrap: wrap;
diff --git a/web/controller/base.go b/web/controller/base.go
index 9964e853..17946892 100644
--- a/web/controller/base.go
+++ b/web/controller/base.go
@@ -4,10 +4,8 @@ package controller
import (
"net/http"
- "strings"
"github.com/mhsanaei/3x-ui/v3/logger"
- "github.com/mhsanaei/3x-ui/v3/util/crypto"
"github.com/mhsanaei/3x-ui/v3/web/locale"
"github.com/mhsanaei/3x-ui/v3/web/session"
@@ -19,8 +17,7 @@ type BaseController struct{}
// checkLogin is a middleware that verifies user authentication and handles unauthorized access.
func (a *BaseController) checkLogin(c *gin.Context) {
- user := session.GetLoginUser(c)
- if user == nil {
+ if !session.IsLogin(c) {
if isAjax(c) {
pureJsonMsg(c, http.StatusUnauthorized, false, I18nWeb(c, "pages.login.loginAgain"))
} else {
@@ -28,41 +25,11 @@ func (a *BaseController) checkLogin(c *gin.Context) {
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
}
c.Abort()
- return
- }
- if isDefaultAdminCredential(user.Username, user.Password) && !credentialChangeRouteAllowed(c) {
- if isAjax(c) {
- pureJsonMsg(c, http.StatusForbidden, false, "Change the default admin credentials before continuing.")
- } else {
- c.Header("Cache-Control", "no-store")
- c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")+"panel/settings")
- }
- c.Abort()
} else {
c.Next()
}
}
-func isDefaultAdminCredential(username string, hashedPassword string) bool {
- return username == "admin" && crypto.CheckPasswordHash(hashedPassword, "admin")
-}
-
-func credentialChangeRouteAllowed(c *gin.Context) bool {
- basePath := c.GetString("base_path")
- path := c.Request.URL.Path
- allowedPrefixes := []string{
- basePath + "panel/settings",
- basePath + "panel/setting/",
- basePath + "panel/csrf-token",
- }
- for _, prefix := range allowedPrefixes {
- if strings.HasPrefix(path, prefix) {
- return true
- }
- }
- return false
-}
-
// I18nWeb retrieves an internationalized message for the web interface based on the current locale.
func I18nWeb(c *gin.Context, name string, params ...string) string {
anyfunc, funcExists := c.Get("I18n")
diff --git a/web/controller/dist.go b/web/controller/dist.go
index 74dd50af..51bd3574 100644
--- a/web/controller/dist.go
+++ b/web/controller/dist.go
@@ -57,18 +57,11 @@ func serveDistPage(c *gin.Context, name string) {
}
csrfMeta := []byte(``)
- nonceAttr := ""
- if nonce := c.GetString("csp_nonce"); nonce != "" {
- nonceAttr = ` nonce="` + htmlpkg.EscapeString(nonce) + `"`
- }
- script := ``
inject := []byte(script)
inject = append(inject, csrfMeta...)
diff --git a/web/controller/index.go b/web/controller/index.go
index dc3b6935..1e77ab99 100644
--- a/web/controller/index.go
+++ b/web/controller/index.go
@@ -39,7 +39,7 @@ func NewIndexController(g *gin.RouterGroup) *IndexController {
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index)
- g.GET("/logout", a.logoutGet)
+ g.GET("/logout", a.logout)
// Public CSRF endpoint — the SPA login page (served by Vite in
// dev or by serveDistPage in prod) needs a token to POST /login,
// but the panel-side /panel/csrf-token sits behind checkLogin.
@@ -48,7 +48,6 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
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)
}
@@ -131,9 +130,7 @@ func (a *IndexController) login(c *gin.Context) {
}
logger.Infof("%s logged in successfully", safeUser)
- jsonMsgObj(c, I18nWeb(c, "pages.login.toasts.successLogin"), gin.H{
- "mustChangeCredentials": user.Username == "admin" && form.Password == "admin",
- }, nil)
+ jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
}
func loginFailureReason(err error) string {
@@ -153,18 +150,9 @@ func (a *IndexController) logout(c *gin.Context) {
logger.Warning("Unable to clear session on logout:", err)
}
c.Header("Cache-Control", "no-store")
- if isAjax(c) {
- jsonMsg(c, "", nil)
- return
- }
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
}
-func (a *IndexController) logoutGet(c *gin.Context) {
- c.Header("Allow", http.MethodPost)
- c.AbortWithStatus(http.StatusMethodNotAllowed)
-}
-
// csrfToken returns the session CSRF token. Public — the login page
// needs a token before authenticating.
func (a *IndexController) csrfToken(c *gin.Context) {
diff --git a/web/controller/setting.go b/web/controller/setting.go
index a421e234..7c4ec7b1 100644
--- a/web/controller/setting.go
+++ b/web/controller/setting.go
@@ -2,7 +2,6 @@ package controller
import (
"errors"
- "strings"
"time"
"github.com/mhsanaei/3x-ui/v3/util/crypto"
@@ -21,15 +20,6 @@ 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
@@ -51,9 +41,7 @@ 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)
@@ -62,7 +50,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
// getAllSetting retrieves all current settings.
func (a *SettingController) getAllSetting(c *gin.Context) {
- allSetting, err := a.settingService.GetAllSettingView()
+ allSetting, err := a.settingService.GetAllSetting()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
@@ -92,16 +80,6 @@ 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{}
@@ -115,18 +93,10 @@ 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 strings.TrimSpace(form.NewUsername) == "" || form.NewPassword == "" {
+ if 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
@@ -138,19 +108,6 @@ 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)
diff --git a/web/service/node.go b/web/service/node.go
index a2b6d818..8330316a 100644
--- a/web/service/node.go
+++ b/web/service/node.go
@@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
- "net"
"net/http"
"strconv"
"strings"
diff --git a/web/service/user.go b/web/service/user.go
index 0be9710c..e5544d5a 100644
--- a/web/service/user.go
+++ b/web/service/user.go
@@ -102,21 +102,6 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
return user, nil
}
-func (s *UserService) VerifyTwoFactorCode(code string) (bool, error) {
- twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
- if err != nil {
- return false, err
- }
- if !twoFactorEnable {
- return true, nil
- }
- twoFactorToken, err := s.settingService.GetTwoFactorToken()
- if err != nil {
- return false, err
- }
- return gotp.NewDefaultTOTP(twoFactorToken).Now() == code, nil
-}
-
func (s *UserService) UpdateUser(id int, username string, password string) error {
db := database.GetDB()
hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
diff --git a/web/service/xray_metrics.go b/web/service/xray_metrics.go
index 9eb08039..766aada3 100644
--- a/web/service/xray_metrics.go
+++ b/web/service/xray_metrics.go
@@ -32,10 +32,10 @@ type ObsTagSnapshot struct {
type XrayMetricsService struct {
settingService SettingService
- mu sync.RWMutex
- state xrayMetricsState
- client *http.Client
- obsByTag map[string]ObsTagSnapshot
+ mu sync.RWMutex
+ state xrayMetricsState
+ client *http.Client
+ obsByTag map[string]ObsTagSnapshot
}
var validObsTag = regexp.MustCompile(`^[a-zA-Z0-9._\-]+$`)