diff --git a/web/assets/js/util/utils.js b/web/assets/js/util/utils.js
index 30f1f6a2..941a676e 100644
--- a/web/assets/js/util/utils.js
+++ b/web/assets/js/util/utils.js
@@ -57,6 +57,20 @@ class HttpUtil {
}
}
+ static async delete(url, params, options = {}) {
+ try {
+ const resp = await axios.delete(url, { params, ...options });
+ const msg = this._respToMsg(resp);
+ this._handleMsg(msg);
+ return msg;
+ } catch (error) {
+ console.error('DELETE request failed:', error);
+ const errorMsg = new Msg(false, error.response?.data?.message || error.message || 'Request failed');
+ this._handleMsg(errorMsg);
+ return errorMsg;
+ }
+ }
+
static async postWithModal(url, data, modal) {
if (modal) {
modal.loading(true);
diff --git a/web/controller/api.go b/web/controller/api.go
index 3e599688..dd548c65 100644
--- a/web/controller/api.go
+++ b/web/controller/api.go
@@ -64,7 +64,7 @@ func (a *APIController) createBackup(c *gin.Context) {
func (controller *APIController) initApiV2Router(router *gin.RouterGroup) {
apiV2 := router.Group("/api/v2")
- apiV2.Use(controller.checkLogin)
+ apiV2.Use(controller.apiTokenGuard)
serverApiGroup := apiV2.Group("/server")
inboundsApiGroup := apiV2.Group("/inbounds")
diff --git a/web/controller/base.go b/web/controller/base.go
index 492fc2dc..264a8f2d 100644
--- a/web/controller/base.go
+++ b/web/controller/base.go
@@ -1,16 +1,21 @@
package controller
import (
+ "fmt"
"net/http"
+ "strings"
"x-ui/logger"
"x-ui/web/locale"
"x-ui/web/session"
"github.com/gin-gonic/gin"
+ "x-ui/web/service"
)
-type BaseController struct{}
+type BaseController struct{
+ settingService service.SettingService
+}
func (a *BaseController) checkLogin(c *gin.Context) {
if !session.IsLogin(c) {
@@ -35,3 +40,39 @@ func I18nWeb(c *gin.Context, name string, params ...string) string {
msg := i18nFunc(locale.Web, name, params...)
return msg
}
+
+func (a *BaseController) apiTokenGuard(c *gin.Context) {
+ bearerToken := c.Request.Header.Get("Authorization")
+ tokenParts := strings.Split(bearerToken, " ")
+ if len(tokenParts) != 2 {
+ pureJsonMsg(c, http.StatusUnauthorized, false, "Invalid token format")
+ c.Abort()
+ return
+ }
+ reqToken := tokenParts[1]
+ token, err := a.settingService.GetApiToken()
+
+ if err != nil {
+ pureJsonMsg(c, http.StatusUnauthorized, false, err.Error())
+ c.Abort()
+ return
+ }
+
+ if reqToken != token {
+ pureJsonMsg(c, http.StatusUnauthorized, false, "Auth failed")
+ c.Abort()
+ return
+ }
+
+ userService := service.UserService{}
+ user, err := userService.GetFirstUser()
+ if err != nil {
+ fmt.Println("get current user info failed, error info:", err)
+ }
+
+ session.SetSessionUser(c, user)
+
+ c.Next()
+
+ session.ClearSession(c)
+}
\ No newline at end of file
diff --git a/web/controller/inbound.go b/web/controller/inbound.go
index 0c90174b..a923f782 100644
--- a/web/controller/inbound.go
+++ b/web/controller/inbound.go
@@ -45,7 +45,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
}
func (a *InboundController) getInbounds(c *gin.Context) {
- user := session.GetLoginUser(c)
+ user := session.GetSessionUser(c)
inbounds, err := a.inboundService.GetInbounds(user.Id)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
@@ -54,10 +54,19 @@ func (a *InboundController) getInbounds(c *gin.Context) {
jsonObj(c, inbounds, nil)
}
+func (a *InboundController) getAllInbounds(c *gin.Context) {
+ inbounds, err := a.inboundService.GetAllInbounds()
+ if err != nil {
+ jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
+ return
+ }
+ jsonObj(c, inbounds, nil)
+}
+
func (a *InboundController) getInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
- jsonMsg(c, I18nWeb(c, "get"), err)
+ jsonMsg(c, I18nWeb(c, "get"), errors.New("Invalid inbound id"))
return
}
inbound, err := a.inboundService.GetInbound(id)
@@ -95,7 +104,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.create"), err)
return
}
- user := session.GetLoginUser(c)
+ user := session.GetSessionUser(c)
inbound.UserId = user.Id
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
@@ -343,7 +352,7 @@ func (a *InboundController) importInbound(c *gin.Context) {
jsonMsg(c, "Something went wrong!", err)
return
}
- user := session.GetLoginUser(c)
+ user := session.GetSessionUser(c)
inbound.Id = 0
inbound.UserId = user.Id
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
diff --git a/web/controller/index.go b/web/controller/index.go
index 9af4ed7f..e3017505 100644
--- a/web/controller/index.go
+++ b/web/controller/index.go
@@ -86,7 +86,7 @@ func (a *IndexController) login(c *gin.Context) {
}
session.SetMaxAge(c, sessionMaxAge*60)
- session.SetLoginUser(c, user)
+ session.SetSessionUser(c, user)
if err := sessions.Default(c).Save(); err != nil {
logger.Warning("Unable to save session: ", err)
return
@@ -97,7 +97,7 @@ func (a *IndexController) login(c *gin.Context) {
}
func (a *IndexController) logout(c *gin.Context) {
- user := session.GetLoginUser(c)
+ user := session.GetSessionUser(c)
if user != nil {
logger.Infof("%s logged out successfully", user.Username)
}
diff --git a/web/controller/setting.go b/web/controller/setting.go
index d04969dc..8a896534 100644
--- a/web/controller/setting.go
+++ b/web/controller/setting.go
@@ -3,6 +3,9 @@ package controller
import (
"errors"
"time"
+ "crypto/rand"
+ "crypto/sha512"
+ "encoding/hex"
"x-ui/web/entity"
"x-ui/web/service"
@@ -28,6 +31,10 @@ type SettingController struct {
panelService service.PanelService
}
+type ApiTokenResponse struct {
+ Token string `json:"token"`
+}
+
func NewSettingController(g *gin.RouterGroup) *SettingController {
a := &SettingController{}
a.initRouter(g)
@@ -45,6 +52,10 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
g.POST("/updateUserSecret", a.updateSecret)
g.POST("/getUserSecret", a.getUserSecret)
+
+ g.GET("/apiToken", a.getApiToken)
+ g.POST("/apiToken", a.generateApiToken)
+ g.DELETE("/apiToken", a.removeApiToken)
}
func (a *SettingController) getAllSetting(c *gin.Context) {
@@ -83,7 +94,7 @@ func (a *SettingController) updateUser(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
return
}
- user := session.GetLoginUser(c)
+ user := session.GetSessionUser(c)
if user.Username != form.OldUsername || user.Password != form.OldPassword {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
return
@@ -96,7 +107,7 @@ func (a *SettingController) updateUser(c *gin.Context) {
if err == nil {
user.Username = form.NewUsername
user.Password = form.NewPassword
- session.SetLoginUser(c, user)
+ session.SetSessionUser(c, user)
}
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
}
@@ -112,17 +123,17 @@ func (a *SettingController) updateSecret(c *gin.Context) {
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
}
- user := session.GetLoginUser(c)
+ user := session.GetSessionUser(c)
err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret)
if err == nil {
user.LoginSecret = form.LoginSecret
- session.SetLoginUser(c, user)
+ session.SetSessionUser(c, user)
}
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
}
func (a *SettingController) getUserSecret(c *gin.Context) {
- loginUser := session.GetLoginUser(c)
+ loginUser := session.GetSessionUser(c)
user := a.userService.GetUserSecret(loginUser.Id)
if user != nil {
jsonObj(c, user, nil)
@@ -137,3 +148,50 @@ func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
}
jsonObj(c, defaultJsonConfig, nil)
}
+
+func (a *SettingController) getApiToken(c *gin.Context) {
+ response := &ApiTokenResponse{}
+ token, err := a.settingService.GetApiToken()
+ if err != nil {
+ jsonObj(c, response , err)
+ return
+ }
+
+ response.Token = token
+
+ jsonObj(c, response , nil)
+}
+
+func (a *SettingController) generateApiToken(c *gin.Context) {
+ response := &ApiTokenResponse{}
+ randomBytes := make([]byte, 32)
+
+ _, err := rand.Read(randomBytes)
+ if err != nil {
+ jsonObj(c, nil, err)
+ return
+ }
+
+ hash := sha512.Sum512(randomBytes)
+ response.Token = hex.EncodeToString(hash[:])
+
+ saveErr := a.settingService.SaveApiToken(response.Token)
+
+ if saveErr != nil {
+ jsonObj(c, nil, saveErr)
+ return
+ }
+
+ jsonMsgObj(c, I18nWeb(c, "pages.settings.security.apiTokenGeneratedSuccessful"), response, nil)
+}
+
+func (a *SettingController) removeApiToken(c *gin.Context) {
+ err := a.settingService.RemoveApiToken()
+
+ if err != nil {
+ jsonObj(c, nil, err)
+ return
+ }
+
+ jsonMsg(c, "Removed", nil)
+}
diff --git a/web/entity/entity.go b/web/entity/entity.go
index 12206340..b875bdc3 100644
--- a/web/entity/entity.go
+++ b/web/entity/entity.go
@@ -57,6 +57,7 @@ type AllSetting struct {
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
Datepicker string `json:"datepicker" form:"datepicker"`
+ ApiToken string `json:"apiToken" form:"apiToken"`
}
func (s *AllSetting) CheckValid() error {
diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html
index 0c70ca1c..4c6c46cf 100644
--- a/web/html/xui/settings.html
+++ b/web/html/xui/settings.html
@@ -235,6 +235,28 @@
{{ i18n "confirm" }}
+
+ {{ i18n "pages.settings.security.apiTitle"}}
+
+
+
+
+
+
+
+ [[ apiToken ]]
+
+
+
+
+
{{ i18n "pages.settings.security.apiGenerateToken" }}
+
+
+
+
+
+
+
@@ -401,6 +423,7 @@
+
{{template "js" .}}
{{template "component/themeSwitcher" .}}
@@ -522,132 +545,166 @@
sample = []
this.remarkModel.forEach(r => sample.push(this.remarkModels[r]));
this.remarkSample = sample.length == 0 ? '' : sample.join(this.remarkSeparator);
- }
+ },
+ apiToken: null,
},
- methods: {
- loading(spinning = true) {
- this.spinning = spinning;
- },
- async getAllSetting() {
- this.loading(true);
- const msg = await HttpUtil.post("/panel/setting/all");
- this.loading(false);
- if (msg.success) {
- this.oldAllSetting = new AllSetting(msg.obj);
- this.allSetting = new AllSetting(msg.obj);
- app.changeRemarkSample();
- this.saveBtnDisable = true;
- }
- await this.fetchUserSecret();
- },
- async updateAllSetting() {
- this.loading(true);
- const msg = await HttpUtil.post("/panel/setting/update", this.allSetting);
- this.loading(false);
- if (msg.success) {
- await this.getAllSetting();
- }
- },
- async updateUser() {
- this.loading(true);
- const msg = await HttpUtil.post("/panel/setting/updateUser", this.user);
- this.loading(false);
- if (msg.success) {
- this.user = {};
- window.location.replace(basePath + "logout");
- }
- },
- async restartPanel() {
- await new Promise(resolve => {
- this.$confirm({
- title: '{{ i18n "pages.settings.restartPanel" }}',
- content: '{{ i18n "pages.settings.restartPanelDesc" }}',
- class: themeSwitcher.currentTheme,
- okText: '{{ i18n "sure" }}',
- cancelText: '{{ i18n "cancel" }}',
- onOk: () => resolve(),
- });
- });
- this.loading(true);
- const msg = await HttpUtil.post("/panel/setting/restartPanel");
- this.loading(false);
- if (msg.success) {
- this.loading(true);
- await PromiseUtil.sleep(5000);
- var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
- if (host == this.oldAllSetting.webDomain) host = null;
- if (port == this.oldAllSetting.webPort) port = null;
- const isTLS = webCertFile !== "" || webKeyFile !== "";
- const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
- 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();
- },
- generateRandomString(length) {
- var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
- let randomString = "";
- for (let i = 0; i < length; i++) {
- randomString += chars[Math.floor(Math.random() * chars.length)];
- }
- return randomString;
- },
- async getNewSecret() {
- if (!this.changeSecret) {
- this.changeSecret = true;
- this.user.loginSecret = '';
- const newSecret = this.generateRandomString(64);
- await PromiseUtil.sleep(1000);
- this.user.loginSecret = newSecret;
- this.changeSecret = false;
- }
- },
- async toggleToken(value) {
- if (value) {
- await this.getNewSecret();
- } else {
- this.user.loginSecret = "";
- }
- },
- addNoise() {
- const newNoise = { type: "rand", packet: "10-20", delay: "10-16" };
- this.noisesArray = [...this.noisesArray, newNoise];
- },
- removeNoise(index) {
- const newNoises = [...this.noisesArray];
- newNoises.splice(index, 1);
- this.noisesArray = newNoises;
- },
- updateNoiseType(index, value) {
- const updatedNoises = [...this.noisesArray];
- updatedNoises[index] = { ...updatedNoises[index], type: value };
- this.noisesArray = updatedNoises;
- },
- updateNoisePacket(index, value) {
- const updatedNoises = [...this.noisesArray];
- updatedNoises[index] = { ...updatedNoises[index], packet: value };
- this.noisesArray = updatedNoises;
- },
- updateNoiseDelay(index, value) {
- const updatedNoises = [...this.noisesArray];
- updatedNoises[index] = { ...updatedNoises[index], delay: value };
- this.noisesArray = updatedNoises;
- },
+ methods: {
+ loading(spinning = true) {
+ this.spinning = spinning;
+ },
+ async getAllSetting() {
+ this.loading(true);
+ const msg = await HttpUtil.post("/panel/setting/all");
+ this.loading(false);
+ if (msg.success) {
+ this.oldAllSetting = new AllSetting(msg.obj);
+ this.allSetting = new AllSetting(msg.obj);
+ this.apiToken = msg.obj.apiToken;
+ app.changeRemarkSample();
+ this.saveBtnDisable = true;
+ }
+ await this.fetchUserSecret();
+ },
+ async updateAllSetting() {
+ this.loading(true);
+ const msg = await HttpUtil.post("/panel/setting/update", this.allSetting);
+ this.loading(false);
+ if (msg.success) {
+ await this.getAllSetting();
+ }
+ },
+ async updateUser() {
+ this.loading(true);
+ const msg = await HttpUtil.post("/panel/setting/updateUser", this.user);
+ this.loading(false);
+ if (msg.success) {
+ this.user = {};
+ window.location.replace(basePath + "logout");
+ }
+ },
+ async restartPanel() {
+ await new Promise(resolve => {
+ this.$confirm({
+ title: '{{ i18n "pages.settings.restartPanel" }}',
+ content: '{{ i18n "pages.settings.restartPanelDesc" }}',
+ class: themeSwitcher.currentTheme,
+ okText: '{{ i18n "sure" }}',
+ cancelText: '{{ i18n "cancel" }}',
+ onOk: () => resolve(),
+ });
+ });
+ this.loading(true);
+ const msg = await HttpUtil.post("/panel/setting/restartPanel");
+ this.loading(false);
+ if (msg.success) {
+ this.loading(true);
+ await PromiseUtil.sleep(5000);
+ var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
+ if (host == this.oldAllSetting.webDomain) host = null;
+ if (port == this.oldAllSetting.webPort) port = null;
+ const isTLS = webCertFile !== "" || webKeyFile !== "";
+ const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
+ 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();
+ },
+ generateRandomString(length) {
+ var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
+ let randomString = "";
+ for (let i = 0; i < length; i++) {
+ randomString += chars[Math.floor(Math.random() * chars.length)];
+ }
+ return randomString;
+ },
+ async getNewSecret() {
+ if (!this.changeSecret) {
+ this.changeSecret = true;
+ this.user.loginSecret = '';
+ const newSecret = this.generateRandomString(64);
+ await PromiseUtil.sleep(1000);
+ this.user.loginSecret = newSecret;
+ this.changeSecret = false;
+ }
+ },
+ async toggleToken(value) {
+ if (value) {
+ await this.getNewSecret();
+ } else {
+ this.user.loginSecret = "";
+ }
+ },
+ addNoise() {
+ const newNoise = { type: "rand", packet: "10-20", delay: "10-16" };
+ this.noisesArray = [...this.noisesArray, newNoise];
+ },
+ removeNoise(index) {
+ const newNoises = [...this.noisesArray];
+ newNoises.splice(index, 1);
+ this.noisesArray = newNoises;
+ },
+ updateNoiseType(index, value) {
+ const updatedNoises = [...this.noisesArray];
+ updatedNoises[index] = { ...updatedNoises[index], type: value };
+ this.noisesArray = updatedNoises;
+ },
+ updateNoisePacket(index, value) {
+ const updatedNoises = [...this.noisesArray];
+ updatedNoises[index] = { ...updatedNoises[index], packet: value };
+ this.noisesArray = updatedNoises;
+ },
+ updateNoiseDelay(index, value) {
+ const updatedNoises = [...this.noisesArray];
+ updatedNoises[index] = { ...updatedNoises[index], delay: value };
+ this.noisesArray = updatedNoises;
+ },
+ async generateApiToken() {
+ this.loading(true);
+ const msg = await HttpUtil.post("/panel/setting/apiToken");
+ if (msg && msg.obj) {
+ this.apiToken = msg.obj.token;
+ }
+ this.loading(false);
+ },
+ copyApiToken() {
+ ClipboardJS.copy(this.apiToken);
+ app.$message.success('{{ i18n "copied" }}')
+ },
+ async removeApiToken() {
+ await new Promise(() => {
+ this.$confirm({
+ title: '{{ i18n "pages.settings.security.apiConfirmRemoveTokenTitle" }}',
+ content: '{{ i18n "pages.settings.security.apiConfirmRemoveTokenText" }}',
+ class: themeSwitcher.currentTheme,
+ okText: '{{ i18n "delete"}}',
+ cancelText: '{{ i18n "cancel" }}',
+ onOk: async () => {
+ this.loading(true);
+ const msg = await HttpUtil.delete("/panel/setting/apiToken");
+ if (msg && msg.success) {
+ app.$message.success('{{ i18n "deleted" }}')
+ this.apiToken = null;
+ }
+ this.loading(false);
+ },
+ });
+ });
+ },
},
computed: {
fragment: {
diff --git a/web/service/setting.go b/web/service/setting.go
index e3ea3ece..7531d389 100644
--- a/web/service/setting.go
+++ b/web/service/setting.go
@@ -596,3 +596,46 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
return result, nil
}
+
+
+func (s *SettingService) GetApiToken() (token string, err error) {
+ db := database.GetDB()
+ setting := &model.Setting{}
+ err = db.Model(model.Setting{}).Where("key = 'apiToken'").Find(setting).Error
+ if err != nil {
+ return "", err
+ }
+ return setting.Value, nil
+}
+
+func (s *SettingService) SaveApiToken(token string) error {
+ db := database.GetDB()
+ setting := &model.Setting{}
+ err := db.Model(model.Setting{}).Where("key = 'apiToken'").Find(setting).Error
+
+ if err != nil {
+ return err
+ }
+
+ if setting.Value == "" {
+ newSetting := model.Setting{
+ Key: "apiToken",
+ Value: token,
+ }
+ fmt.Println("New setting created")
+ return db.Model(model.Setting{}).Create(&newSetting).Error
+ }
+ return db.Model(model.Setting{}).
+ Where("key = 'apiToken'").
+ Update("value", token).Error
+}
+
+func (s *SettingService) RemoveApiToken() error {
+ db := database.GetDB()
+ setting := &model.Setting{}
+ err := db.Model(model.Setting{}).Where("key = 'apiToken'").Find(setting).Error
+ if err != nil {
+ return err
+ }
+ return db.Model(model.Setting{}).Delete(setting, setting.Id).Error
+}
\ No newline at end of file
diff --git a/web/session/session.go b/web/session/session.go
index 13aedad8..f52382bf 100644
--- a/web/session/session.go
+++ b/web/session/session.go
@@ -18,7 +18,7 @@ func init() {
gob.Register(model.User{})
}
-func SetLoginUser(c *gin.Context, user *model.User) {
+func SetSessionUser(c *gin.Context, user *model.User) {
if user == nil {
return
}
@@ -35,7 +35,7 @@ func SetMaxAge(c *gin.Context, maxAge int) {
})
}
-func GetLoginUser(c *gin.Context) *model.User {
+func GetSessionUser(c *gin.Context) *model.User {
s := sessions.Default(c)
obj := s.Get(loginUserKey)
if obj == nil {
@@ -51,7 +51,7 @@ func GetLoginUser(c *gin.Context) *model.User {
}
func IsLogin(c *gin.Context) bool {
- return GetLoginUser(c) != nil
+ return GetSessionUser(c) != nil
}
func ClearSession(c *gin.Context) {