浏览代码

chore: implement 2fa auth (#2968)

* chore: implement 2fa auth

from #2786

* chore: format code

* chore: replace two factor token input with qr-code

* chore: requesting confirmation of setting/removing two-factor authentication

otpauth library was taken from cdnjs

* chore: revert changes in `ClipboardManager`

don't need it.

* chore: removing twoFactor prop in settings page

* chore: remove `twoFactorQr` object in `mounted` function
Shishkevich D. 9 小时之前
父节点
当前提交
fe3b1c9b52

+ 2 - 4
database/db.go

@@ -24,7 +24,6 @@ var db *gorm.DB
 const (
 	defaultUsername = "admin"
 	defaultPassword = "admin"
-	defaultSecret   = ""
 )
 
 func initModels() error {
@@ -61,9 +60,8 @@ func initUser() error {
 		}
 
 		user := &model.User{
-			Username:    defaultUsername,
-			Password:    hashedPassword,
-			LoginSecret: defaultSecret,
+			Username: defaultUsername,
+			Password: hashedPassword,
 		}
 		return db.Create(user).Error
 	}

+ 3 - 4
database/model/model.go

@@ -21,10 +21,9 @@ const (
 )
 
 type User struct {
-	Id          int    `json:"id" gorm:"primaryKey;autoIncrement"`
-	Username    string `json:"username"`
-	Password    string `json:"password"`
-	LoginSecret string `json:"loginSecret"`
+	Id       int    `json:"id" gorm:"primaryKey;autoIncrement"`
+	Username string `json:"username"`
+	Password string `json:"password"`
 }
 
 type Inbound struct {

+ 2 - 1
go.mod

@@ -15,8 +15,10 @@ require (
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/shirou/gopsutil/v4 v4.25.3
 	github.com/valyala/fasthttp v1.61.0
+	github.com/xlzd/gotp v0.1.0
 	github.com/xtls/xray-core v1.250306.1-0.20250430044058-87ab8e512882
 	go.uber.org/atomic v1.11.0
+	golang.org/x/crypto v0.37.0
 	golang.org/x/text v0.24.0
 	google.golang.org/grpc v1.72.0
 	gorm.io/driver/sqlite v1.5.7
@@ -84,7 +86,6 @@ require (
 	go.uber.org/mock v0.5.2 // indirect
 	go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
 	golang.org/x/arch v0.16.0 // indirect
-	golang.org/x/crypto v0.37.0 // indirect
 	golang.org/x/mod v0.24.0 // indirect
 	golang.org/x/net v0.39.0 // indirect
 	golang.org/x/sync v0.13.0 // indirect

+ 2 - 0
go.sum

@@ -187,6 +187,8 @@ github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn
 github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
 github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
 github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
+github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
+github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
 github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 h1:g1Cj7d+my6k/HHxLAyxPwyX8i7FGRr6ulBDMkBzg2BM=
 github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463/go.mod h1:BjIOLmkEEtAgloAiVUcYj0Mt+YU00JARZw8AEU0IwAg=
 github.com/xtls/xray-core v1.250306.1-0.20250430044058-87ab8e512882 h1:O/aN4TCrJ+fmaDOBoQhtTRev2hVHIENy2EJ70jQcyEY=

+ 0 - 35
main.go

@@ -346,36 +346,6 @@ func migrateDb() {
 	fmt.Println("Migration done!")
 }
 
-func removeSecret() {
-	userService := service.UserService{}
-
-	secretExists, err := userService.CheckSecretExistence()
-	if err != nil {
-		fmt.Println("Error checking secret existence:", err)
-		return
-	}
-
-	if !secretExists {
-		fmt.Println("No secret exists to remove.")
-		return
-	}
-
-	err = userService.RemoveUserSecret()
-	if err != nil {
-		fmt.Println("Error removing secret:", err)
-		return
-	}
-
-	settingService := service.SettingService{}
-	err = settingService.SetSecretStatus(false)
-	if err != nil {
-		fmt.Println("Error updating secret status:", err)
-		return
-	}
-
-	fmt.Println("Secret removed successfully.")
-}
-
 func main() {
 	if len(os.Args) < 2 {
 		runWebServer()
@@ -403,10 +373,8 @@ func main() {
 	var reset bool
 	var show bool
 	var getCert bool
-	var remove_secret bool
 	settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
 	settingCmd.BoolVar(&show, "show", false, "Display current settings")
-	settingCmd.BoolVar(&remove_secret, "remove_secret", false, "Remove secret key")
 	settingCmd.IntVar(&port, "port", 0, "Set panel port number")
 	settingCmd.StringVar(&username, "username", "", "Set login username")
 	settingCmd.StringVar(&password, "password", "", "Set login password")
@@ -470,9 +438,6 @@ func main() {
 		if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
 			updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
 		}
-		if remove_secret {
-			removeSecret()
-		}
 		if enabletgbot {
 			updateTgbotEnableSts(enabletgbot)
 		}

+ 2 - 1
web/assets/js/model/setting.js

@@ -23,8 +23,9 @@ class AllSetting {
         this.tgBotLoginNotify = true;
         this.tgCpu = 80;
         this.tgLang = "en-US";
+        this.twoFactorEnable = false;
+        this.twoFactorToken = "";
         this.xrayTemplateConfig = "";
-        this.secretEnable = false;
         this.subEnable = false;
         this.subTitle = "";
         this.subListen = "";

+ 27 - 0
web/assets/js/util/index.js

@@ -145,6 +145,33 @@ class RandomUtil {
 
         return Base64.alternativeEncode(String.fromCharCode(...array));
     }
+
+    static randomBase32String(length = 16) {
+        const array = new Uint8Array(length);
+        
+        window.crypto.getRandomValues(array);
+        
+        const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
+        let result = '';
+        let bits = 0;
+        let buffer = 0;
+        
+        for (let i = 0; i < array.length; i++) {
+            buffer = (buffer << 8) | array[i];
+            bits += 8;
+            
+            while (bits >= 5) {
+                bits -= 5;
+                result += base32Chars[(buffer >>> bits) & 0x1F];
+            }
+        }
+        
+        if (bits > 0) {
+            result += base32Chars[(buffer << (5 - bits)) & 0x1F];
+        }
+        
+        return result;
+    }
 }
 
 class ObjectUtil {

文件差异内容过多而无法显示
+ 4 - 0
web/assets/otpauth/otpauth.umd.min.js


+ 8 - 9
web/controller/index.go

@@ -14,9 +14,9 @@ import (
 )
 
 type LoginForm struct {
-	Username    string `json:"username" form:"username"`
-	Password    string `json:"password" form:"password"`
-	LoginSecret string `json:"loginSecret" form:"loginSecret"`
+	Username    	string `json:"username" form:"username"`
+	Password    	string `json:"password" form:"password"`
+	TwoFactorCode	string `json:"twoFactorCode" form:"twoFactorCode"`
 }
 
 type IndexController struct {
@@ -37,7 +37,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
 	g.GET("/", a.index)
 	g.POST("/login", a.login)
 	g.GET("/logout", a.logout)
-	g.POST("/getSecretStatus", a.getSecretStatus)
+	g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
 }
 
 func (a *IndexController) index(c *gin.Context) {
@@ -64,14 +64,13 @@ func (a *IndexController) login(c *gin.Context) {
 		return
 	}
 
-	user := a.userService.CheckUser(form.Username, form.Password, form.LoginSecret)
+	user := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
 	timeStr := time.Now().Format("2006-01-02 15:04:05")
 	safeUser := template.HTMLEscapeString(form.Username)
 	safePass := template.HTMLEscapeString(form.Password)
-	safeSecret := template.HTMLEscapeString(form.LoginSecret)
 
 	if user == nil {
-		logger.Warningf("wrong username: \"%s\", password: \"%s\", secret: \"%s\", IP: \"%s\"", safeUser, safePass, safeSecret, getRemoteIp(c))
+		logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c))
 		a.tgbot.UserLoginNotify(safeUser, safePass, getRemoteIp(c), timeStr, 0)
 		pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
 		return
@@ -108,8 +107,8 @@ func (a *IndexController) logout(c *gin.Context) {
 	c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
 }
 
-func (a *IndexController) getSecretStatus(c *gin.Context) {
-	status, err := a.settingService.GetSecretStatus()
+func (a *IndexController) getTwoFactorEnable(c *gin.Context) {
+	status, err := a.settingService.GetTwoFactorEnable()
 	if err == nil {
 		jsonObj(c, status, nil)
 	}

+ 0 - 29
web/controller/setting.go

@@ -19,10 +19,6 @@ type updateUserForm struct {
 	NewPassword string `json:"newPassword" form:"newPassword"`
 }
 
-type updateSecretForm struct {
-	LoginSecret string `json:"loginSecret" form:"loginSecret"`
-}
-
 type SettingController struct {
 	settingService service.SettingService
 	userService    service.UserService
@@ -44,8 +40,6 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
 	g.POST("/updateUser", a.updateUser)
 	g.POST("/restartPanel", a.restartPanel)
 	g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
-	g.POST("/updateUserSecret", a.updateSecret)
-	g.POST("/getUserSecret", a.getUserSecret)
 }
 
 func (a *SettingController) getAllSetting(c *gin.Context) {
@@ -107,29 +101,6 @@ func (a *SettingController) restartPanel(c *gin.Context) {
 	jsonMsg(c, I18nWeb(c, "pages.settings.restartPanel"), err)
 }
 
-func (a *SettingController) updateSecret(c *gin.Context) {
-	form := &updateSecretForm{}
-	err := c.ShouldBind(form)
-	if err != nil {
-		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
-	}
-	user := session.GetLoginUser(c)
-	err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret)
-	if err == nil {
-		user.LoginSecret = form.LoginSecret
-		session.SetLoginUser(c, user)
-	}
-	jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
-}
-
-func (a *SettingController) getUserSecret(c *gin.Context) {
-	loginUser := session.GetLoginUser(c)
-	user := a.userService.GetUserSecret(loginUser.Id)
-	if user != nil {
-		jsonObj(c, user, nil)
-	}
-}
-
 func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
 	defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
 	if err != nil {

+ 2 - 1
web/entity/entity.go

@@ -38,7 +38,8 @@ type AllSetting struct {
 	TgCpu                       int    `json:"tgCpu" form:"tgCpu"`
 	TgLang                      string `json:"tgLang" form:"tgLang"`
 	TimeLocation                string `json:"timeLocation" form:"timeLocation"`
-	SecretEnable                bool   `json:"secretEnable" form:"secretEnable"`
+	TwoFactorEnable				bool   `json:"twoFactorEnable" form:"twoFactorEnable"`
+	TwoFactorToken				string `json:"twoFactorToken" form:"twoFactorToken"`
 	SubEnable                   bool   `json:"subEnable" form:"subEnable"`
 	SubTitle                    string `json:"subTitle" form:"subTitle"`
 	SubListen                   string `json:"subListen" form:"subListen"`

+ 10 - 10
web/html/login.html

@@ -512,11 +512,11 @@
                         <a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon>
                       </a-input-password>
                     </a-form-item>
-                    <a-form-item v-if="secretEnable">
-                      <a-input-password autocomplete="secret" name="secret" v-model.trim="user.loginSecret"
-                        placeholder='{{ i18n "secretToken" }}' @keydown.enter.native="login">
+                    <a-form-item v-if="twoFactorEnable">
+                      <a-input autocomplete="totp" name="twoFactorCode" v-model.trim="user.twoFactorCode"
+                        placeholder='{{ i18n "twoFactorCode" }}' @keydown.enter.native="login">
                         <a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon>
-                      </a-input-password>
+                      </a-input>
                     </a-form-item>
                     <a-form-item>
                       <a-row justify="center" class="centered">
@@ -549,14 +549,14 @@
         user: {
           username: "",
           password: "",
-          loginSecret: ""
+          twoFactorCode: ""
         },
-        secretEnable: false,
+        twoFactorEnable: false,
         lang: ""
       },
       async mounted() {
         this.lang = LanguageManager.getLanguage();
-        this.secretEnable = await this.getSecretStatus();
+        this.twoFactorEnable = await this.getTwoFactorEnable();
       },
       methods: {
         async login() {
@@ -567,12 +567,12 @@
             location.href = basePath + 'panel/';
           }
         },
-        async getSecretStatus() {
+        async getTwoFactorEnable() {
           this.loading = true;
-          const msg = await HttpUtil.post('/getSecretStatus');
+          const msg = await HttpUtil.post('/getTwoFactorEnable');
           this.loading = false;
           if (msg.success) {
-            this.secretEnable = msg.obj;
+            this.twoFactorEnable = msg.obj;
             return msg.obj;
           }
         },

+ 118 - 0
web/html/modals/two_factor_modal.html

@@ -0,0 +1,118 @@
+{{define "modals/twoFactorModal"}}
+<a-modal id="two-factor-modal" v-model="twoFactorModal.visible" :title="twoFactorModal.title" :closable="true"
+    :class="themeSwitcher.currentTheme">
+    <template v-if="twoFactorModal.type === 'set'">
+        <p>{{ i18n "pages.settings.security.twoFactorModalSteps" }}</p>
+        <a-divider></a-divider>
+        <p>{{ i18n "pages.settings.security.twoFactorModalFirstStep" }}</p>
+        <div :style="{ display: 'flex', alignItems: 'center', flexDirection: 'column', gap: '12px' }">
+            <div
+                :style="{ border: '1px solid', borderRadius: '1rem', borderColor: themeSwitcher.isDarkTheme ? 'var(--dark-color-surface-300)' : '#d9d9d9', padding: 0 }">
+                <img :src="twoFactorModal.qrImage"
+                    :style="{ filter: themeSwitcher.isDarkTheme ? 'invert(1)' : 'none'}"
+                    :alt="twoFactorModal.token">
+            </div>
+            <span :style="{ fontSize: '12px', fontFamily: 'monospace' }">[[ twoFactorModal.token ]]</span>
+        </div>
+        <a-divider></a-divider>
+        <p>{{ i18n "pages.settings.security.twoFactorModalSecondStep" }}</p>
+        <a-input v-model.trim="twoFactorModal.enteredCode" :style="{ width: '100%' }"></a-input>
+    </template>
+    <template v-if="twoFactorModal.type === 'remove'">
+        <p>{{ i18n "pages.settings.security.twoFactorModalRemoveStep" }}</p>
+        <a-input v-model.trim="twoFactorModal.enteredCode" :style="{ width: '100%' }"></a-input>
+    </template>
+    <template slot="footer">
+        <a-button @click="twoFactorModal.cancel">
+            <span>{{ i18n "cancel" }}</span>
+        </a-button>
+        <a-button type="primary" :disabled="twoFactorModal.enteredCode.length < 6" @click="twoFactorModal.ok">
+            <span>{{ i18n "confirm" }}</span>
+        </a-button>
+    </template>
+</a-modal>
+
+<script>
+    const twoFactorModal = {
+        title: '',
+        fileName: '',
+        token: '',
+        enteredCode: '',
+        visible: false,
+        type: 'set',
+        confirm: null,
+        totpObject: null,
+        qrImage: "",
+        ok() {
+            if (twoFactorModal.totpObject.generate() === twoFactorModal.enteredCode) {
+                ObjectUtil.execute(twoFactorModal.confirm, true)
+
+                twoFactorModal.close()
+
+                switch (twoFactorModal.type) {
+                    case 'set':
+                        Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalSetSuccess" }}')
+                        break;
+                    case 'remove':
+                        Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalDeleteSuccess" }}')
+                        break;
+                    default:
+                        break;
+                }
+            } else {
+                Vue.prototype.$message['error']('{{ i18n "pages.settings.security.twoFactorModalError" }}')
+            }
+        },
+        cancel() {
+            ObjectUtil.execute(twoFactorModal.confirm, false)
+
+            twoFactorModal.close()
+        },
+        show: function ({
+            title = '',
+            token = '',
+            type = 'set',
+            confirm = (success) => { }
+        }) {
+            this.title = title;
+            this.token = token;
+            this.visible = true;
+            this.confirm = confirm;
+            this.type = type;
+
+            this.totpObject = new OTPAuth.TOTP({
+                issuer: "3x-ui",
+                label: "Administrator",
+                algorithm: "SHA1",
+                digits: 6,
+                period: 30,
+                secret: twoFactorModal.token,
+            });
+
+            if (type === 'set') {
+                this.qrImage = new QRious({
+                    size: 150,
+                    value: twoFactorModal.totpObject.toString(),
+                    background: 'white',
+                    backgroundAlpha: 0,
+                    foreground: 'black',
+                    padding: 12,
+                    level: 'L'
+                }).toDataURL()
+            }
+        },
+        close: function () {
+            twoFactorModal.enteredCode = "";
+            twoFactorModal.visible = false;
+        },
+    };
+
+    const twoFactorModalApp = new Vue({
+        delimiters: ['[[', ']]'],
+        el: '#two-factor-modal',
+        data: {
+            twoFactorModal: twoFactorModal,
+        },
+    });
+</script>
+{{end}}

+ 31 - 33
web/html/settings.html

@@ -122,10 +122,13 @@
     </a-layout>
   </a-layout>
 {{template "js" .}}
+<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
+<script src="{{ .base_path }}assets/otpauth/otpauth.umd.min.js?{{ .cur_ver }}"></script>
 <script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script>
 {{template "component/aSidebar" .}}
 {{template "component/aThemeSwitch" .}}
 {{template "component/aSettingListItem" .}}
+{{template "modals/twoFactorModal"}}
 <script>
   const app = new Vue({
     delimiters: ['[[', ']]'],
@@ -133,7 +136,6 @@
     data: {
       themeSwitcher,
       spinning: false,
-      changeSecret: false,
       oldAllSetting: new AllSetting(),
       allSetting: new AllSetting(),
       saveBtnDisable: true,
@@ -258,7 +260,6 @@
           app.changeRemarkSample();
           this.saveBtnDisable = true;
         }
-        await this.fetchUserSecret();
       },
       async updateAllSetting() {
         this.loading(true);
@@ -302,38 +303,34 @@
           window.location.replace(url);
         }
       },
-      async fetchUserSecret() {
-        this.loading(true);
-        const userMessage = await HttpUtil.post("/panel/setting/getUserSecret", this.user);
-        if (userMessage.success) {
-          this.user = userMessage.obj;
-        }
-        this.loading(false);
-      },
-      async updateSecret() {
-        this.loading(true);
-        const msg = await HttpUtil.post("/panel/setting/updateUserSecret", this.user);
-        if (msg && msg.obj) {
-          this.user = msg.obj;
-        }
-        this.loading(false);
-        await this.updateAllSetting();
-      },
-      async getNewSecret() {
-        if (!this.changeSecret) {
-          this.changeSecret = true;
-          this.user.loginSecret = '';
-          const newSecret = RandomUtil.randomSeq(64);
-          await PromiseUtil.sleep(1000);
-          this.user.loginSecret = newSecret;
-          this.changeSecret = false;
-        }
-      },
-      async toggleToken(value) {
-        if (value) {
-          await this.getNewSecret();
+      toggleTwoFactor(newValue) {
+        if (newValue) {
+          const newTwoFactorToken = RandomUtil.randomBase32String()
+
+          twoFactorModal.show({
+            title: '{{ i18n "pages.settings.security.twoFactorModalSetTitle" }}',
+            token: newTwoFactorToken,
+            type: 'set',
+            confirm: (success) => {
+              if (success) {
+                this.allSetting.twoFactorToken = newTwoFactorToken
+              }
+
+              this.allSetting.twoFactorEnable = success
+            }
+          })
         } else {
-          this.user.loginSecret = "";
+          twoFactorModal.show({
+            title: '{{ i18n "pages.settings.security.twoFactorModalDeleteTitle" }}',
+            token: this.allSetting.twoFactorToken,
+            type: 'remove',
+            confirm: (success) => {
+              if (success) {
+                this.allSetting.twoFactorEnable = false
+                this.allSetting.twoFactorToken = ""
+              }
+            }
+          })
         }
       },
       addNoise() {
@@ -526,6 +523,7 @@
     },
     async mounted() {
       await this.getAllSetting();
+
       while (true) {
         await PromiseUtil.sleep(1000);
         this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);

+ 4 - 20
web/html/settings/panel/security.html

@@ -31,30 +31,14 @@
             </a-space>
         </a-list-item>
     </a-collapse-panel>
-    <a-collapse-panel key="2" header='{{ i18n "pages.settings.security.secret"}}'>
+    <a-collapse-panel key="2" header='{{ i18n "pages.settings.security.twoFactor" }}'>
         <a-setting-list-item paddings="small">
-            <template #title>{{ i18n "pages.settings.security.loginSecurity" }}</template>
-            <template #description>{{ i18n "pages.settings.security.loginSecurityDesc" }}</template>
+            <template #title>{{ i18n "pages.settings.security.twoFactorEnable" }}</template>
+            <template #description>{{ i18n "pages.settings.security.twoFactorEnableDesc" }}</template>
             <template #control>
-                <a-switch @change="toggleToken(allSetting.secretEnable)" v-model="allSetting.secretEnable"></a-switch>
-                <a-icon :style="{ marginLeft: '1rem' }" v-if="allSetting.secretEnable" :spin="this.changeSecret" type="sync"
-                    @click="getNewSecret"></a-icon>
+                <a-switch @click="toggleTwoFactor" :checked="allSetting.twoFactorEnable"></a-switch>
             </template>
         </a-setting-list-item>
-        <a-setting-list-item paddings="small">
-            <template #title>{{ i18n "pages.settings.security.secretToken" }}</template>
-            <template #description>{{ i18n "pages.settings.security.secretTokenDesc" }}</template>
-            <template #control>
-                <a-textarea type="text" :disabled="!allSetting.secretEnable" v-model="user.loginSecret"></a-textarea>
-            </template>
-        </a-setting-list-item>
-        <a-list-item>
-            <a-space direction="horizontal" :style="{ padding: '0 20px' }">
-                <a-button type="primary" :loading="this.changeSecret" @click="updateSecret">
-                    <span>{{ i18n "confirm"}}</span>
-                </a-button>
-            </a-space>
-        </a-list-item>
     </a-collapse-panel>
 </a-collapse>
 {{end}}

+ 11 - 11
web/service/setting.go

@@ -48,7 +48,8 @@ var defaultValueMap = map[string]string{
 	"tgBotLoginNotify":            "true",
 	"tgCpu":                       "80",
 	"tgLang":                      "en-US",
-	"secretEnable":                "false",
+	"twoFactorEnable":             "false",
+	"twoFactorToken":              "",
 	"subEnable":                   "false",
 	"subTitle":                    "",
 	"subListen":                   "",
@@ -166,8 +167,7 @@ func (s *SettingService) ResetSettings() error {
 		return err
 	}
 	return db.Model(model.User{}).
-		Where("1 = 1").
-		Update("login_secret", "").Error
+		Where("1 = 1").Error
 }
 
 func (s *SettingService) getSetting(key string) (*model.Setting, error) {
@@ -318,6 +318,14 @@ func (s *SettingService) GetTgLang() (string, error) {
 	return s.getString("tgLang")
 }
 
+func (s *SettingService) GetTwoFactorEnable() (bool, error) {
+	return s.getBool("twoFactorEnable")
+}
+
+func (s *SettingService) GetTwoFactorToken() (string, error) {
+	return s.getString("twoFactorToken")
+}
+
 func (s *SettingService) GetPort() (int, error) {
 	return s.getInt("webPort")
 }
@@ -358,14 +366,6 @@ func (s *SettingService) GetRemarkModel() (string, error) {
 	return s.getString("remarkModel")
 }
 
-func (s *SettingService) GetSecretStatus() (bool, error) {
-	return s.getBool("secretEnable")
-}
-
-func (s *SettingService) SetSecretStatus(value bool) error {
-	return s.setBool("secretEnable", value)
-}
-
 func (s *SettingService) GetSecret() ([]byte, error) {
 	secret, err := s.getString("secret")
 	if secret == defaultValueMap["secret"] {

+ 28 - 50
web/service/user.go

@@ -8,10 +8,13 @@ import (
 	"x-ui/logger"
 	"x-ui/util/crypto"
 
+	"github.com/xlzd/gotp"
 	"gorm.io/gorm"
 )
 
-type UserService struct{}
+type UserService struct {
+	settingService SettingService
+}
 
 func (s *UserService) GetFirstUser() (*model.User, error) {
 	db := database.GetDB()
@@ -26,13 +29,13 @@ func (s *UserService) GetFirstUser() (*model.User, error) {
 	return user, nil
 }
 
-func (s *UserService) CheckUser(username string, password string, secret string) *model.User {
+func (s *UserService) CheckUser(username string, password string, twoFactorCode string) *model.User {
 	db := database.GetDB()
 
 	user := &model.User{}
 
 	err := db.Model(model.User{}).
-		Where("username = ? and login_secret = ?", username, secret).
+		Where("username = ?", username).
 		First(user).
 		Error
 	if err == gorm.ErrRecordNotFound {
@@ -42,69 +45,44 @@ func (s *UserService) CheckUser(username string, password string, secret string)
 		return nil
 	}
 
-	if crypto.CheckPasswordHash(user.Password, password) {
-		return user
+	if !crypto.CheckPasswordHash(user.Password, password) {
+		return nil
 	}
 
-	return nil
-}
-
-func (s *UserService) UpdateUser(id int, username string, password string) error {
-	db := database.GetDB()
-	hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
-
+	twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
 	if err != nil {
-		return err
+		logger.Warning("check two factor err:", err)
+		return nil
 	}
 
-	return db.Model(model.User{}).
-		Where("id = ?", id).
-		Updates(map[string]any{"username": username, "password": hashedPassword}).
-		Error
-}
+	if twoFactorEnable {
+		twoFactorToken, err := s.settingService.GetTwoFactorToken()
 
-func (s *UserService) UpdateUserSecret(id int, secret string) error {
-	db := database.GetDB()
-	return db.Model(model.User{}).
-		Where("id = ?", id).
-		Update("login_secret", secret).
-		Error
-}
+		if err != nil {
+			logger.Warning("check two factor token err:", err)
+			return nil
+		}
 
-func (s *UserService) RemoveUserSecret() error {
-	db := database.GetDB()
-	return db.Model(model.User{}).
-		Where("1 = 1").
-		Update("login_secret", "").
-		Error
-}
-
-func (s *UserService) GetUserSecret(id int) *model.User {
-	db := database.GetDB()
-	user := &model.User{}
-	err := db.Model(model.User{}).
-		Where("id = ?", id).
-		First(user).
-		Error
-	if err == gorm.ErrRecordNotFound {
-		return nil
+		if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode {
+			return nil
+		}
 	}
+
 	return user
 }
 
-func (s *UserService) CheckSecretExistence() (bool, error) {
+func (s *UserService) UpdateUser(id int, username string, password string) error {
 	db := database.GetDB()
+	hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
 
-	var count int64
-	err := db.Model(model.User{}).
-		Where("login_secret IS NOT NULL").
-		Count(&count).
-		Error
 	if err != nil {
-		return false, err
+		return err
 	}
 
-	return count > 0, nil
+	return db.Model(model.User{}).
+		Where("id = ?", id).
+		Updates(map[string]any{"username": username, "password": hashedPassword}).
+		Error
 }
 
 func (s *UserService) UpdateFirstUser(username string, password string) error {

+ 14 - 7
web/translation/translate.ar_EG.toml

@@ -51,7 +51,7 @@
 "install" = "تثبيت"
 "clients" = "عملاء"
 "usage" = "استخدام"
-"secretToken" = "توكن سري"
+"twoFactorCode" = "الكود"
 "remained" = "المتبقي"
 "security" = "أمان"
 "secAlertTitle" = "تنبيه أمني"
@@ -87,7 +87,7 @@
 "invalidFormData" = "تنسيق البيانات المدخلة مش صحيح."
 "emptyUsername" = "اسم المستخدم مطلوب"
 "emptyPassword" = "الباسورد مطلوب"
-"wrongUsernameOrPassword" = "اسم المستخدم أو الباسورد أو السر مش صحيح."
+"wrongUsernameOrPassword" = "اسم المستخدم أو كلمة المرور أو كود المصادقة الثنائية غير صحيح."  
 "successLogin" = "تسجيل دخول ناجح"
 
 [pages.index]
@@ -501,11 +501,18 @@
 
 [pages.settings.security]
 "admin" = "بيانات الأدمن"
-"secret" = "توكن سري"
-"loginSecurity" = "أمان تسجيل الدخول"
-"loginSecurityDesc" = "بيضيف طبقة مصادقة إضافية لزيادة الأمان."
-"secretToken" = "توكن سري"
-"secretTokenDesc" = "احتفظ بالتوكن ده في مكان آمن. التوكن ده مطلوب لتسجيل الدخول ومش ممكن تسترجعه لو ضاع."
+"twoFactor" = "المصادقة الثنائية"  
+"twoFactorEnable" = "تفعيل المصادقة الثنائية"  
+"twoFactorEnableDesc" = "يضيف طبقة إضافية من المصادقة لتعزيز الأمان."  
+"twoFactorModalSetTitle" = "تفعيل المصادقة الثنائية"
+"twoFactorModalDeleteTitle" = "تعطيل المصادقة الثنائية"
+"twoFactorModalSteps" = "لإعداد المصادقة الثنائية، قم ببعض الخطوات:"
+"twoFactorModalFirstStep" = "1. امسح رمز QR هذا في تطبيق المصادقة أو انسخ الرمز الموجود بجانب رمز QR والصقه في التطبيق"
+"twoFactorModalSecondStep" = "2. أدخل الرمز من التطبيق"
+"twoFactorModalRemoveStep" = "أدخل الرمز من التطبيق لإزالة المصادقة الثنائية."
+"twoFactorModalSetSuccess" = "تم إنشاء المصادقة الثنائية بنجاح"
+"twoFactorModalDeleteSuccess" = "تم حذف المصادقة الثنائية بنجاح"
+"twoFactorModalError" = "رمز خاطئ"
 
 [pages.settings.toasts]
 "modifySettings" = "تعديل الإعدادات"

+ 14 - 7
web/translation/translate.en_US.toml

@@ -51,7 +51,7 @@
 "install" = "Install"
 "clients" = "Clients"
 "usage" = "Usage"
-"secretToken" = "Secret Token"
+"twoFactorCode" = "Code"
 "remained" = "Remained"
 "security" = "Security"
 "secAlertTitle" = "Security Alert"
@@ -87,7 +87,7 @@
 "invalidFormData" = "The Input data format is invalid."
 "emptyUsername" = "Username is required"
 "emptyPassword" = "Password is required"
-"wrongUsernameOrPassword" = "Invalid username or password or secret."
+"wrongUsernameOrPassword" = "Invalid username or password or two-factor code."
 "successLogin" = "Login"
 
 [pages.index]
@@ -501,11 +501,18 @@
 
 [pages.settings.security]
 "admin" = "Admin credentials"
-"secret" = "Secret Token"
-"loginSecurity" = "Secure Login"
-"loginSecurityDesc" = "Adds an additional layer of authentication to provide more security."
-"secretToken" = "Secret Token"
-"secretTokenDesc" = "Please securely store this token in a safe place. This token is required for login and cannot be recovered."
+"twoFactor" = "Two-factor authentication"
+"twoFactorEnable" = "Enable 2FA"
+"twoFactorEnableDesc" = "Adds an additional layer of authentication to provide more security."
+"twoFactorModalSetTitle" = "Enable two-factor authentication"
+"twoFactorModalDeleteTitle" = "Disable two-factor authentication"
+"twoFactorModalSteps" = "To set up two-factor authentication, perform a few steps:"
+"twoFactorModalFirstStep" = "1. Scan this QR code in the app for authentication or copy the token near the QR code and paste it into the app"
+"twoFactorModalSecondStep" = "2. Enter the code from the app"
+"twoFactorModalRemoveStep" = "Enter the code from the application to remove two-factor authentication."
+"twoFactorModalSetSuccess" = "Two-factor authentication has been successfully established"
+"twoFactorModalDeleteSuccess" = "Two-factor authentication has been successfully deleted"
+"twoFactorModalError" = "Wrong code"
 
 [pages.settings.toasts]
 "modifySettings" = "Modify Settings"

+ 14 - 7
web/translation/translate.es_ES.toml

@@ -51,7 +51,7 @@
 "install" = "Instalar"
 "clients" = "Clientes"
 "usage" = "Uso"
-"secretToken" = "Token Secreto"
+"twoFactorCode" = "Código"
 "remained" = "Restante"
 "security" = "Seguridad"
 "secAlertTitle" = "Alerta de Seguridad"
@@ -87,7 +87,7 @@
 "invalidFormData" = "El formato de los datos de entrada es inválido."
 "emptyUsername" = "Por favor ingresa el nombre de usuario."
 "emptyPassword" = "Por favor ingresa la contraseña."
-"wrongUsernameOrPassword" = "Nombre de usuario o contraseña inválidos."
+"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."  
 "successLogin" = "Inicio de Sesión Exitoso"
 
 [pages.index]
@@ -503,11 +503,18 @@
 
 [pages.settings.security]
 "admin" = "Credenciales de administrador"
-"secret" = "Token Secreto"
-"loginSecurity" = "Seguridad de Inicio de Sesión"
-"loginSecurityDesc" = "Habilitar un paso adicional de seguridad para el inicio de sesión de usuarios."
-"secretToken" = "Token Secreto"
-"secretTokenDesc" = "Por favor, copia y guarda este token de forma segura en un lugar seguro. Este token es necesario para iniciar sesión y no se puede recuperar con la herramienta de comando x-ui."
+"twoFactor" = "Autenticación de dos factores"  
+"twoFactorEnable" = "Habilitar 2FA"  
+"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."  
+"twoFactorModalSetTitle" = "Activar autenticación de dos factores"
+"twoFactorModalDeleteTitle" = "Desactivar autenticación de dos factores"
+"twoFactorModalSteps" = "Para configurar la autenticación de dos factores, sigue estos pasos:"
+"twoFactorModalFirstStep" = "1. Escanea este código QR en la aplicación de autenticación o copia el token cerca del código QR y pégalo en la aplicación"
+"twoFactorModalSecondStep" = "2. Ingresa el código de la aplicación"
+"twoFactorModalRemoveStep" = "Ingresa el código de la aplicación para eliminar la autenticación de dos factores."
+"twoFactorModalSetSuccess" = "La autenticación de dos factores se ha establecido con éxito"
+"twoFactorModalDeleteSuccess" = "La autenticación de dos factores se ha eliminado con éxito"
+"twoFactorModalError" = "Código incorrecto"
 
 [pages.settings.toasts]
 "modifySettings" = "Modificar Configuraciones "

+ 14 - 7
web/translation/translate.fa_IR.toml

@@ -51,7 +51,7 @@
 "install" = "نصب"
 "clients" = "کاربران"
 "usage" = "استفاده"
-"secretToken" = "توکن امنیتی"
+"twoFactorCode" = "کد"
 "remained" = "باقی‌مانده"
 "security" = "امنیت"
 "secAlertTitle" = "هشدار‌امنیتی"
@@ -87,7 +87,7 @@
 "invalidFormData" = "اطلاعات به‌درستی وارد نشده‌است"
 "emptyUsername" = "لطفا یک نام‌کاربری وارد کنید‌"
 "emptyPassword" = "لطفا یک رمزعبور وارد کنید"
-"wrongUsernameOrPassword" = "نام‌کاربری یا رمزعبور‌اشتباه‌است"
+"wrongUsernameOrPassword" = "نام کاربری، رمز عبور یا کد دو مرحله‌ای نامعتبر است."  
 "successLogin" = "ورود"
 
 [pages.index]
@@ -503,11 +503,18 @@
 
 [pages.settings.security]
 "admin" = "اعتبارنامه‌های ادمین"
-"secret" = "توکن مخفی"
-"loginSecurity" = "ورود ایمن"
-"loginSecurityDesc" = "یک لایه اضافی از احراز هویت برای ایجاد امنیت بیشتر اضافه می کند"
-"secretToken" = "توکن مخفی"
-"secretTokenDesc" = "لطفاً این توکن را در مکانی امن ذخیره کنید. این توکن برای ورود به سیستم مورد نیاز است و قابل بازیابی نیست"
+"twoFactor" = "احراز هویت دو مرحله‌ای"  
+"twoFactorEnable" = "فعال‌سازی 2FA"  
+"twoFactorEnableDesc" = "یک لایه اضافی امنیتی برای احراز هویت فراهم می‌کند."  
+"twoFactorModalSetTitle" = "فعال‌سازی احراز هویت دو مرحله‌ای"
+"twoFactorModalDeleteTitle" = "غیرفعال‌سازی احراز هویت دو مرحله‌ای"
+"twoFactorModalSteps" = "برای راه‌اندازی احراز هویت دو مرحله‌ای، مراحل زیر را انجام دهید:"
+"twoFactorModalFirstStep" = "1. این کد QR را در برنامه احراز هویت اسکن کنید یا توکن کنار کد QR را کپی کرده و در برنامه بچسبانید"
+"twoFactorModalSecondStep" = "2. کد را از برنامه وارد کنید"
+"twoFactorModalRemoveStep" = "برای حذف احراز هویت دو مرحله‌ای، کد را از برنامه وارد کنید."
+"twoFactorModalSetSuccess" = "احراز هویت دو مرحله‌ای با موفقیت برقرار شد"
+"twoFactorModalDeleteSuccess" = "احراز هویت دو مرحله‌ای با موفقیت حذف شد"
+"twoFactorModalError" = "کد نادرست"
 
 [pages.settings.toasts]
 "modifySettings" = "ویرایش تنظیمات"

+ 14 - 7
web/translation/translate.id_ID.toml

@@ -51,7 +51,7 @@
 "install" = "Instal"
 "clients" = "Klien"
 "usage" = "Penggunaan"
-"secretToken" = "Token Rahasia"
+"twoFactorCode" = "Kode"
 "remained" = "Tersisa"
 "security" = "Keamanan"
 "secAlertTitle" = "Peringatan keamanan"
@@ -87,7 +87,7 @@
 "invalidFormData" = "Format data input tidak valid."
 "emptyUsername" = "Nama Pengguna diperlukan"
 "emptyPassword" = "Kata Sandi diperlukan"
-"wrongUsernameOrPassword" = "Nama pengguna atau kata sandi tidak valid."
+"wrongUsernameOrPassword" = "Username, kata sandi, atau kode dua faktor tidak valid."  
 "successLogin" = "Login berhasil"
 
 [pages.index]
@@ -503,11 +503,18 @@
 
 [pages.settings.security]
 "admin" = "Kredensial admin"
-"secret" = "Token Rahasia"
-"loginSecurity" = "Login Aman"
-"loginSecurityDesc" = "Menambahkan lapisan otentikasi tambahan untuk memberikan keamanan lebih."
-"secretToken" = "Token Rahasia"
-"secretTokenDesc" = "Simpan token ini dengan aman di tempat yang aman. Token ini diperlukan untuk login dan tidak dapat dipulihkan."
+"twoFactor" = "Autentikasi dua faktor"
+"twoFactorEnable" = "Aktifkan 2FA"
+"twoFactorEnableDesc" = "Menambahkan lapisan autentikasi tambahan untuk keamanan lebih."
+"twoFactorModalSetTitle" = "Aktifkan autentikasi dua faktor"
+"twoFactorModalDeleteTitle" = "Nonaktifkan autentikasi dua faktor"
+"twoFactorModalSteps" = "Untuk menyiapkan autentikasi dua faktor, lakukan beberapa langkah:"
+"twoFactorModalFirstStep" = "1. Pindai kode QR ini di aplikasi autentikasi atau salin token di dekat kode QR dan tempelkan ke aplikasi"
+"twoFactorModalSecondStep" = "2. Masukkan kode dari aplikasi"
+"twoFactorModalRemoveStep" = "Masukkan kode dari aplikasi untuk menghapus autentikasi dua faktor."
+"twoFactorModalSetSuccess" = "Autentikasi dua faktor telah berhasil dibuat"
+"twoFactorModalDeleteSuccess" = "Autentikasi dua faktor telah berhasil dihapus"
+"twoFactorModalError" = "Kode salah"
 
 [pages.settings.toasts]
 "modifySettings" = "Ubah Pengaturan"

+ 14 - 7
web/translation/translate.ja_JP.toml

@@ -51,7 +51,7 @@
 "install" = "インストール"
 "clients" = "クライアント"
 "usage" = "利用状況"
-"secretToken" = "シークレットトークン"
+"twoFactorCode" = "コード"
 "remained" = "残り"
 "security" = "セキュリティ"
 "secAlertTitle" = "セキュリティアラート"
@@ -87,7 +87,7 @@
 "invalidFormData" = "データ形式エラー"
 "emptyUsername" = "ユーザー名を入力してください"
 "emptyPassword" = "パスワードを入力してください"
-"wrongUsernameOrPassword" = "ユーザー名またはパスワードが間違っています"
+"wrongUsernameOrPassword" = "ユーザー名、パスワード、または二段階認証コードが無効です。"  
 "successLogin" = "ログイン成功"
 
 [pages.index]
@@ -503,11 +503,18 @@
 
 [pages.settings.security]
 "admin" = "管理者の資格情報"
-"secret" = "セキュリティトークン"
-"loginSecurity" = "ログインセキュリティ"
-"loginSecurityDesc" = "追加の認証を追加してセキュリティを向上させる"
-"secretToken" = "セキュリティトークン"
-"secretTokenDesc" = "このトークンを安全な場所に保管してください。このトークンはログインに使用され、紛失すると回復できません。"
+"twoFactor" = "二段階認証"  
+"twoFactorEnable" = "2FAを有効化"  
+"twoFactorEnableDesc" = "セキュリティを強化するために追加の認証層を追加します。"  
+"twoFactorModalSetTitle" = "二段階認証を有効にする"
+"twoFactorModalDeleteTitle" = "二段階認証を無効にする"
+"twoFactorModalSteps" = "二段階認証を設定するには、次の手順を実行してください:"
+"twoFactorModalFirstStep" = "1. 認証アプリでこのQRコードをスキャンするか、QRコード近くのトークンをコピーしてアプリに貼り付けます"
+"twoFactorModalSecondStep" = "2. アプリからコードを入力してください"
+"twoFactorModalRemoveStep" = "二段階認証を削除するには、アプリからコードを入力してください。"
+"twoFactorModalSetSuccess" = "二要素認証が正常に設定されました"
+"twoFactorModalDeleteSuccess" = "二要素認証が正常に削除されました"
+"twoFactorModalError" = "コードが間違っています"
 
 [pages.settings.toasts]
 "modifySettings" = "設定を変更"

+ 14 - 7
web/translation/translate.pt_BR.toml

@@ -51,7 +51,7 @@
 "install" = "Instalar"
 "clients" = "Clientes"
 "usage" = "Uso"
-"secretToken" = "Token Secreto"
+"twoFactorCode" = "Código"
 "remained" = "Restante"
 "security" = "Segurança"
 "secAlertTitle" = "Alerta de Segurança"
@@ -87,7 +87,7 @@
 "invalidFormData" = "O formato dos dados de entrada é inválido."
 "emptyUsername" = "Nome de usuário é obrigatório"
 "emptyPassword" = "Senha é obrigatória"
-"wrongUsernameOrPassword" = "Nome de usuário, senha ou segredo inválidos."
+"wrongUsernameOrPassword" = "Nome de usuário, senha ou código de dois fatores inválido."  
 "successLogin" = "Login realizado com sucesso"
 
 [pages.index]
@@ -503,11 +503,18 @@
 
 [pages.settings.security]
 "admin" = "Credenciais de administrador"
-"secret" = "Token Secreto"
-"loginSecurity" = "Login Seguro"
-"loginSecurityDesc" = "Adiciona uma camada extra de autenticação para fornecer mais segurança."
-"secretToken" = "Token Secreto"
-"secretTokenDesc" = "Por favor, armazene este token em um local seguro. Este token é necessário para o login e não pode ser recuperado."
+"twoFactor" = "Autenticação de dois fatores"  
+"twoFactorEnable" = "Ativar 2FA"  
+"twoFactorEnableDesc" = "Adiciona uma camada extra de autenticação para mais segurança."  
+"twoFactorModalSetTitle" = "Ativar autenticação de dois fatores"
+"twoFactorModalDeleteTitle" = "Desativar autenticação de dois fatores"
+"twoFactorModalSteps" = "Para configurar a autenticação de dois fatores, siga alguns passos:"
+"twoFactorModalFirstStep" = "1. Escaneie este QR code no aplicativo de autenticação ou copie o token próximo ao QR code e cole no aplicativo"
+"twoFactorModalSecondStep" = "2. Digite o código do aplicativo"
+"twoFactorModalRemoveStep" = "Digite o código do aplicativo para remover a autenticação de dois fatores."
+"twoFactorModalSetSuccess" = "A autenticação de dois fatores foi estabelecida com sucesso"
+"twoFactorModalDeleteSuccess" = "A autenticação de dois fatores foi excluída com sucesso"
+"twoFactorModalError" = "Código incorreto"
 
 [pages.settings.toasts]
 "modifySettings" = "Modificar Configurações"

+ 14 - 7
web/translation/translate.ru_RU.toml

@@ -51,7 +51,7 @@
 "install" = "Установка"
 "clients" = "Клиенты"
 "usage" = "Использование"
-"secretToken" = "Секретный токен"
+"twoFactorCode" = "Код"
 "remained" = "Остаток"
 "security" = "Безопасность"
 "secAlertTitle" = "Предупреждение системы безопасности"
@@ -87,7 +87,7 @@
 "invalidFormData" = "Недопустимый формат данных"
 "emptyUsername" = "Введите имя пользователя"
 "emptyPassword" = "Введите пароль"
-"wrongUsernameOrPassword" = "Неверное имя пользователя, пароль или секретный токен."
+"wrongUsernameOrPassword" = "Неверное имя пользователя, пароль или код двухфакторной аутентификации."  
 "successLogin" = "Успешный вход"
 
 [pages.index]
@@ -503,11 +503,18 @@
 
 [pages.settings.security]
 "admin" = "Учетные данные администратора"
-"secret" = "Секретный токен"
-"loginSecurity" = "Безопасность входа"
-"loginSecurityDesc" = "Включить дополнительные меры безопасности входа пользователя"
-"secretToken" = "Секретный токен"
-"secretTokenDesc" = "Пожалуйста, скопируйте и сохраните этот токен в безопасном месте. Этот токен необходим для входа в систему и не может быть восстановлен с помощью инструмента x-ui"
+"twoFactor" = "Двухфакторная аутентификация"  
+"twoFactorEnable" = "Включить 2FA"  
+"twoFactorEnableDesc" = "Добавляет дополнительный уровень аутентификации для повышения безопасности."  
+"twoFactorModalSetTitle" = "Включить двухфакторную аутентификацию"
+"twoFactorModalDeleteTitle" = "Отключить двухфакторную аутентификацию"
+"twoFactorModalSteps" = "Для настройки двухфакторной аутентификации выполните несколько шагов:"
+"twoFactorModalFirstStep" = "1. Отсканируйте этот QR-код в приложении для аутентификации или скопируйте токен рядом с QR-кодом и вставьте его в приложение"
+"twoFactorModalSecondStep" = "2. Введите код из приложения"
+"twoFactorModalRemoveStep" = "Введите код из приложения, чтобы отключить двухфакторную аутентификацию."
+"twoFactorModalSetSuccess" = "Двухфакторная аутентификация была успешно установлена"
+"twoFactorModalDeleteSuccess" = "Двухфакторная аутентификация была успешно удалена"
+"twoFactorModalError" = "Неверный код"
 
 [pages.settings.toasts]
 "modifySettings" = "Настройки изменены"

+ 14 - 7
web/translation/translate.tr_TR.toml

@@ -51,7 +51,7 @@
 "install" = "Yükle"
 "clients" = "Müşteriler"
 "usage" = "Kullanım"
-"secretToken" = "Gizli Anahtar"
+"twoFactorCode" = "Kod"
 "remained" = "Kalan"
 "security" = "Güvenlik"
 "secAlertTitle" = "Güvenlik Uyarısı"
@@ -87,7 +87,7 @@
 "invalidFormData" = "Girdi verisi formatı geçersiz."
 "emptyUsername" = "Kullanıcı adı gerekli"
 "emptyPassword" = "Şifre gerekli"
-"wrongUsernameOrPassword" = "Geçersiz kullanıcı adı veya şifre veya gizli anahtar."
+"wrongUsernameOrPassword" = "Geçersiz kullanıcı adı, şifre veya iki adımlı doğrulama kodu."  
 "successLogin" = "Giriş Başarılı"
 
 [pages.index]
@@ -503,11 +503,18 @@
 
 [pages.settings.security]
 "admin" = "Yönetici kimlik bilgileri"
-"secret" = "Gizli Anahtar"
-"loginSecurity" = "Güvenli Giriş"
-"loginSecurityDesc" = "Daha fazla güvenlik sağlamak için ek bir kimlik doğrulama katmanı ekler."
-"secretToken" = "Gizli Anahtar"
-"secretTokenDesc" = "Bu anahtarı güvenli bir yerde saklayın. Bu anahtar giriş için gereklidir ve geri alınamaz."
+"twoFactor" = "İki adımlı doğrulama"  
+"twoFactorEnable" = "2FA'yı Etkinleştir"  
+"twoFactorEnableDesc" = "Daha fazla güvenlik için ek bir doğrulama katmanı ekler."  
+"twoFactorModalSetTitle" = "İki adımlı doğrulamayı etkinleştir"
+"twoFactorModalDeleteTitle" = "İki adımlı doğrulamayı devre dışı bırak"
+"twoFactorModalSteps" = "İki adımlı doğrulamayı ayarlamak için şu adımları izleyin:"
+"twoFactorModalFirstStep" = "1. Bu QR kodunu doğrulama uygulamasında tarayın veya QR kodunun yanındaki token'ı kopyalayıp uygulamaya yapıştırın"
+"twoFactorModalSecondStep" = "2. Uygulamadaki kodu girin"
+"twoFactorModalRemoveStep" = "İki adımlı doğrulamayı kaldırmak için uygulamadaki kodu girin."
+"twoFactorModalSetSuccess" = "İki faktörlü kimlik doğrulama başarıyla kuruldu"
+"twoFactorModalDeleteSuccess" = "İki faktörlü kimlik doğrulama başarıyla silindi"
+"twoFactorModalError" = "Yanlış kod"
 
 [pages.settings.toasts]
 "modifySettings" = "Ayarları Değiştir"

+ 14 - 7
web/translation/translate.uk_UA.toml

@@ -51,7 +51,7 @@
 "install" = "Встановити"
 "clients" = "Клієнти"
 "usage" = "Використання"
-"secretToken" = "Секретний маркер"
+"twoFactorCode" = "Код"
 "remained" = "Залишилося"
 "security" = "Беспека"
 "secAlertTitle" = "Попередження системи безпеки"
@@ -87,7 +87,7 @@
 "invalidFormData" = "Формат вхідних даних недійсний."
 "emptyUsername" = "Потрібне ім'я користувача"
 "emptyPassword" = "Потрібен пароль"
-"wrongUsernameOrPassword" = "Невірне ім'я користувача або пароль."
+"wrongUsernameOrPassword" = "Невірне ім’я користувача, пароль або код двофакторної аутентифікації."  
 "successLogin" = "Вхід"
 
 [pages.index]
@@ -503,11 +503,18 @@
 
 [pages.settings.security]
 "admin" = "Облікові дані адміністратора"
-"secret" = "Секретний маркер"
-"loginSecurity" = "Безпечний вхід"
-"loginSecurityDesc" = "Додає додатковий рівень автентифікації для забезпечення більшої безпеки."
-"secretToken" = "Секретний маркер"
-"secretTokenDesc" = "Будь ласка, надійно зберігайте цей маркер у безпечному місці. Цей маркер потрібен для входу, і його неможливо відновити."
+"twoFactor" = "Двофакторна аутентифікація"  
+"twoFactorEnable" = "Увімкнути 2FA"  
+"twoFactorEnableDesc" = "Додає додатковий рівень аутентифікації для підвищення безпеки."  
+"twoFactorModalSetTitle" = "Увімкнути двофакторну аутентифікацію"
+"twoFactorModalDeleteTitle" = "Вимкнути двофакторну аутентифікацію"
+"twoFactorModalSteps" = "Щоб налаштувати двофакторну аутентифікацію, виконайте кілька кроків:"
+"twoFactorModalFirstStep" = "1. Відскануйте цей QR-код у програмі для аутентифікації або скопіюйте токен біля QR-коду та вставте його в програму"
+"twoFactorModalSecondStep" = "2. Введіть код з програми"
+"twoFactorModalRemoveStep" = "Введіть код з програми, щоб вимкнути двофакторну аутентифікацію."
+"twoFactorModalSetSuccess" = "Двофакторна аутентифікація була успішно встановлена"
+"twoFactorModalDeleteSuccess" = "Двофакторна аутентифікація була успішно видалена"
+"twoFactorModalError" = "Невірний код"
 
 [pages.settings.toasts]
 "modifySettings" = "Змінити налаштування"

+ 14 - 7
web/translation/translate.vi_VN.toml

@@ -51,7 +51,7 @@
 "install" = "Cài đặt"
 "clients" = "Các khách hàng"
 "usage" = "Sử dụng"
-"secretToken" = "Mã bí mật"
+"twoFactorCode" = "Mã"
 "remained" = "Còn lại"
 "security" = "Bảo vệ"
 "secAlertTitle" = "Cảnh báo an ninh-Tiếng Việt by Ohoang7"
@@ -87,7 +87,7 @@
 "invalidFormData" = "Dạng dữ liệu nhập không hợp lệ."
 "emptyUsername" = "Vui lòng nhập tên người dùng."
 "emptyPassword" = "Vui lòng nhập mật khẩu."
-"wrongUsernameOrPassword" = "Tên người dùng hoặc mật khẩu không đúng."
+"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."  
 "successLogin" = "Đăng nhập thành công."
 
 [pages.index]
@@ -503,11 +503,18 @@
 
 [pages.settings.security]
 "admin" = "Thông tin đăng nhập quản trị viên"
-"secret" = "Mã thông báo bí mật"
-"loginSecurity" = "Bảo mật đăng nhập"
-"loginSecurityDesc" = "Bật bước bảo mật đăng nhập bổ sung cho người dùng"
-"secretToken" = "Mã bí mật"
-"secretTokenDesc" = "Vui lòng sao chép và lưu trữ mã này một cách an toàn ở nơi an toàn. Mã này cần thiết để đăng nhập và không thể phục hồi từ công cụ lệnh x-ui."
+"twoFactor" = "Xác thực hai yếu tố"  
+"twoFactorEnable" = "Bật 2FA"  
+"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."  
+"twoFactorModalSetTitle" = "Bật xác thực hai yếu tố"
+"twoFactorModalDeleteTitle" = "Tắt xác thực hai yếu tố"
+"twoFactorModalSteps" = "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:"
+"twoFactorModalFirstStep" = "1. Quét mã QR này trong ứng dụng xác thực hoặc sao chép mã token gần mã QR và dán vào ứng dụng"
+"twoFactorModalSecondStep" = "2. Nhập mã từ ứng dụng"
+"twoFactorModalRemoveStep" = "Nhập mã từ ứng dụng để xóa xác thực hai yếu tố."
+"twoFactorModalSetSuccess" = "Xác thực hai yếu tố đã được thiết lập thành công"
+"twoFactorModalDeleteSuccess" = "Xác thực hai yếu tố đã được xóa thành công"
+"twoFactorModalError" = "Mã sai"
 
 [pages.settings.toasts]
 "modifySettings" = "Chỉnh sửa cài đặt "

+ 14 - 7
web/translation/translate.zh_CN.toml

@@ -51,7 +51,7 @@
 "install" = "安装"
 "clients" = "客户端"
 "usage" = "使用情况"
-"secretToken" = "安全密钥"
+"twoFactorCode" = "代码"
 "remained" = "剩余"
 "security" = "安全"
 "secAlertTitle" = "安全警报"
@@ -87,7 +87,7 @@
 "invalidFormData" = "数据格式错误"
 "emptyUsername" = "请输入用户名"
 "emptyPassword" = "请输入密码"
-"wrongUsernameOrPassword" = "用户名或密码错误"
+"wrongUsernameOrPassword" = "用户名、密码或双重验证码无效。"  
 "successLogin" = "登录"
 
 [pages.index]
@@ -503,11 +503,18 @@
 
 [pages.settings.security]
 "admin" = "管理员凭据"
-"secret" = "安全令牌"
-"loginSecurity" = "登录安全"
-"loginSecurityDesc" = "添加额外的身份验证以提高安全性"
-"secretToken" = "安全令牌"
-"secretTokenDesc" = "请将此令牌存储在安全的地方。此令牌用于登录,丢失无法恢复。"
+"twoFactor" = "双重验证"  
+"twoFactorEnable" = "启用2FA"  
+"twoFactorEnableDesc" = "增加额外的验证层以提高安全性。"  
+"twoFactorModalSetTitle" = "啟用雙重認證"
+"twoFactorModalDeleteTitle" = "停用雙重認證"
+"twoFactorModalSteps" = "要設定雙重認證,請執行以下步驟:"
+"twoFactorModalFirstStep" = "1. 在認證應用程式中掃描此QR碼,或複製QR碼附近的令牌並貼到應用程式中"
+"twoFactorModalSecondStep" = "2. 輸入應用程式中的驗證碼"
+"twoFactorModalRemoveStep" = "輸入應用程式中的驗證碼以移除雙重認證。"
+"twoFactorModalSetSuccess" = "双因素认证已成功建立"
+"twoFactorModalDeleteSuccess" = "双因素认证已成功删除"
+"twoFactorModalError" = "驗證碼錯誤"
 
 [pages.settings.toasts]
 "modifySettings" = "修改设置"

+ 14 - 7
web/translation/translate.zh_TW.toml

@@ -51,7 +51,7 @@
 "install" = "安裝"
 "clients" = "客戶端"
 "usage" = "使用情況"
-"secretToken" = "安全金鑰"
+"twoFactorCode" = "代碼"
 "remained" = "剩餘"
 "security" = "安全"
 "secAlertTitle" = "安全警報"
@@ -87,7 +87,7 @@
 "invalidFormData" = "資料格式錯誤"
 "emptyUsername" = "請輸入使用者名稱"
 "emptyPassword" = "請輸入密碼"
-"wrongUsernameOrPassword" = "使用者名稱或密碼錯誤"
+"wrongUsernameOrPassword" = "用戶名、密碼或雙重驗證碼無效。"  
 "successLogin" = "登入"
 
 [pages.index]
@@ -503,11 +503,18 @@
 
 [pages.settings.security]
 "admin" = "管理員憑證"
-"secret" = "安全令牌"
-"loginSecurity" = "登入安全"
-"loginSecurityDesc" = "新增額外的身份驗證以提高安全性"
-"secretToken" = "安全令牌"
-"secretTokenDesc" = "請將此令牌儲存在安全的地方。此令牌用於登入,丟失無法恢復。"
+"twoFactor" = "雙重驗證"  
+"twoFactorEnable" = "啟用2FA"  
+"twoFactorEnableDesc" = "增加額外的驗證層以提高安全性。"  
+"twoFactorModalSetTitle" = "啟用雙重認證"
+"twoFactorModalDeleteTitle" = "停用雙重認證"
+"twoFactorModalSteps" = "要設定雙重認證,請執行以下步驟:"
+"twoFactorModalFirstStep" = "1. 在認證應用程式中掃描此QR碼,或複製QR碼附近的令牌並貼到應用程式中"
+"twoFactorModalSecondStep" = "2. 輸入應用程式中的驗證碼"
+"twoFactorModalRemoveStep" = "輸入應用程式中的驗證碼以移除雙重認證。"
+"twoFactorModalSetSuccess" = "雙重身份驗證已成功建立"
+"twoFactorModalDeleteSuccess" = "雙重身份驗證已成功刪除"
+"twoFactorModalError" = "驗證碼錯誤"
 
 [pages.settings.toasts]
 "modifySettings" = "修改設定"

+ 1 - 3
x-ui.sh

@@ -184,10 +184,8 @@ reset_user() {
     read -rp "Please set the login password [default is a random password]: " config_password
     [[ -z $config_password ]] && config_password=$(date +%s%N | md5sum | cut -c 1-8)
     /usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password} >/dev/null 2>&1
-    /usr/local/x-ui/x-ui setting -remove_secret >/dev/null 2>&1
     echo -e "Panel login username has been reset to: ${green} ${config_account} ${plain}"
     echo -e "Panel login password has been reset to: ${green} ${config_password} ${plain}"
-    echo -e "${yellow} Panel login secret token disabled ${plain}"
     echo -e "${green} Please use the new login username and password to access the X-UI panel. Also remember them! ${plain}"
     confirm_restart
 }
@@ -1731,7 +1729,7 @@ show_menu() {
 │   ${green}4.${plain} Legacy Version                            │
 │   ${green}5.${plain} Uninstall                                 │
 │────────────────────────────────────────────────│
-│   ${green}6.${plain} Reset Username & Password & Secret Token
+│   ${green}6.${plain} Reset Username & Password               
 │   ${green}7.${plain} Reset Web Base Path                       │
 │   ${green}8.${plain} Reset Settings                            │
 │   ${green}9.${plain} Change Port                               │

部分文件因为文件数量过多而无法显示