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) {