Added token based auth

This commit is contained in:
Аlexander Kiselev 2025-02-11 11:10:21 +03:00
parent fb9839cb3d
commit 8e62acc9e5
10 changed files with 364 additions and 141 deletions

View file

@ -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) { static async postWithModal(url, data, modal) {
if (modal) { if (modal) {
modal.loading(true); modal.loading(true);

View file

@ -64,7 +64,7 @@ func (a *APIController) createBackup(c *gin.Context) {
func (controller *APIController) initApiV2Router(router *gin.RouterGroup) { func (controller *APIController) initApiV2Router(router *gin.RouterGroup) {
apiV2 := router.Group("/api/v2") apiV2 := router.Group("/api/v2")
apiV2.Use(controller.checkLogin) apiV2.Use(controller.apiTokenGuard)
serverApiGroup := apiV2.Group("/server") serverApiGroup := apiV2.Group("/server")
inboundsApiGroup := apiV2.Group("/inbounds") inboundsApiGroup := apiV2.Group("/inbounds")

View file

@ -1,16 +1,21 @@
package controller package controller
import ( import (
"fmt"
"net/http" "net/http"
"strings"
"x-ui/logger" "x-ui/logger"
"x-ui/web/locale" "x-ui/web/locale"
"x-ui/web/session" "x-ui/web/session"
"github.com/gin-gonic/gin" "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) { func (a *BaseController) checkLogin(c *gin.Context) {
if !session.IsLogin(c) { if !session.IsLogin(c) {
@ -35,3 +40,39 @@ func I18nWeb(c *gin.Context, name string, params ...string) string {
msg := i18nFunc(locale.Web, name, params...) msg := i18nFunc(locale.Web, name, params...)
return msg 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)
}

View file

@ -45,7 +45,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
} }
func (a *InboundController) getInbounds(c *gin.Context) { func (a *InboundController) getInbounds(c *gin.Context) {
user := session.GetLoginUser(c) user := session.GetSessionUser(c)
inbounds, err := a.inboundService.GetInbounds(user.Id) inbounds, err := a.inboundService.GetInbounds(user.Id)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
@ -54,10 +54,19 @@ func (a *InboundController) getInbounds(c *gin.Context) {
jsonObj(c, inbounds, nil) 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) { func (a *InboundController) getInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "get"), err) jsonMsg(c, I18nWeb(c, "get"), errors.New("Invalid inbound id"))
return return
} }
inbound, err := a.inboundService.GetInbound(id) 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) jsonMsg(c, I18nWeb(c, "pages.inbounds.create"), err)
return return
} }
user := session.GetLoginUser(c) user := session.GetSessionUser(c)
inbound.UserId = user.Id inbound.UserId = user.Id
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) 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) jsonMsg(c, "Something went wrong!", err)
return return
} }
user := session.GetLoginUser(c) user := session.GetSessionUser(c)
inbound.Id = 0 inbound.Id = 0
inbound.UserId = user.Id inbound.UserId = user.Id
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {

View file

@ -86,7 +86,7 @@ func (a *IndexController) login(c *gin.Context) {
} }
session.SetMaxAge(c, sessionMaxAge*60) session.SetMaxAge(c, sessionMaxAge*60)
session.SetLoginUser(c, user) session.SetSessionUser(c, user)
if err := sessions.Default(c).Save(); err != nil { if err := sessions.Default(c).Save(); err != nil {
logger.Warning("Unable to save session: ", err) logger.Warning("Unable to save session: ", err)
return return
@ -97,7 +97,7 @@ func (a *IndexController) login(c *gin.Context) {
} }
func (a *IndexController) logout(c *gin.Context) { func (a *IndexController) logout(c *gin.Context) {
user := session.GetLoginUser(c) user := session.GetSessionUser(c)
if user != nil { if user != nil {
logger.Infof("%s logged out successfully", user.Username) logger.Infof("%s logged out successfully", user.Username)
} }

View file

@ -3,6 +3,9 @@ package controller
import ( import (
"errors" "errors"
"time" "time"
"crypto/rand"
"crypto/sha512"
"encoding/hex"
"x-ui/web/entity" "x-ui/web/entity"
"x-ui/web/service" "x-ui/web/service"
@ -28,6 +31,10 @@ type SettingController struct {
panelService service.PanelService panelService service.PanelService
} }
type ApiTokenResponse struct {
Token string `json:"token"`
}
func NewSettingController(g *gin.RouterGroup) *SettingController { func NewSettingController(g *gin.RouterGroup) *SettingController {
a := &SettingController{} a := &SettingController{}
a.initRouter(g) a.initRouter(g)
@ -45,6 +52,10 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
g.POST("/updateUserSecret", a.updateSecret) g.POST("/updateUserSecret", a.updateSecret)
g.POST("/getUserSecret", a.getUserSecret) 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) { 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) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
return return
} }
user := session.GetLoginUser(c) user := session.GetSessionUser(c)
if user.Username != form.OldUsername || user.Password != form.OldPassword { 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"))) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
return return
@ -96,7 +107,7 @@ func (a *SettingController) updateUser(c *gin.Context) {
if err == nil { if err == nil {
user.Username = form.NewUsername user.Username = form.NewUsername
user.Password = form.NewPassword user.Password = form.NewPassword
session.SetLoginUser(c, user) session.SetSessionUser(c, user)
} }
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
} }
@ -112,17 +123,17 @@ func (a *SettingController) updateSecret(c *gin.Context) {
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) 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) err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret)
if err == nil { if err == nil {
user.LoginSecret = form.LoginSecret user.LoginSecret = form.LoginSecret
session.SetLoginUser(c, user) session.SetSessionUser(c, user)
} }
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
} }
func (a *SettingController) getUserSecret(c *gin.Context) { func (a *SettingController) getUserSecret(c *gin.Context) {
loginUser := session.GetLoginUser(c) loginUser := session.GetSessionUser(c)
user := a.userService.GetUserSecret(loginUser.Id) user := a.userService.GetUserSecret(loginUser.Id)
if user != nil { if user != nil {
jsonObj(c, user, nil) jsonObj(c, user, nil)
@ -137,3 +148,50 @@ func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
} }
jsonObj(c, defaultJsonConfig, nil) 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)
}

View file

@ -57,6 +57,7 @@ type AllSetting struct {
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"` SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
Datepicker string `json:"datepicker" form:"datepicker"` Datepicker string `json:"datepicker" form:"datepicker"`
ApiToken string `json:"apiToken" form:"apiToken"`
} }
func (s *AllSetting) CheckValid() error { func (s *AllSetting) CheckValid() error {

View file

@ -235,6 +235,28 @@
</a-list-item> </a-list-item>
<a-button type="primary" :loading="this.changeSecret" @click="updateSecret">{{ i18n "confirm" }}</a-button> <a-button type="primary" :loading="this.changeSecret" @click="updateSecret">{{ i18n "confirm" }}</a-button>
</a-form> </a-form>
<a-divider>{{ i18n "pages.settings.security.apiTitle"}}</a-divider>
<a-list-item>
<a-row>
<a-col :lg="24" :xl="12" style="white-space: pre-line;">
<a-list-item-meta description='{{ i18n "pages.settings.security.apiDescription" }}'>
</a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
<a-tag class="tr-info-tag" color="green" v-if="apiToken">[[ apiToken ]]</a-tag>
<div style="display: flex; flex-direction: row; align-items: center; gap: 0.5rem; margin-top: 0.5rem">
<a-tooltip title='{{ i18n "copy" }}' v-if="apiToken">
<a-button style="min-width: 24px;" size="small" icon="snippets" :id="'copy-api-token'" @click="copyApiToken"></a-button>
</a-tooltip>
<a-button @click="this.generateApiToken">{{ i18n "pages.settings.security.apiGenerateToken" }}</a-button>
<a-tooltip title='{{ i18n "delete" }}' v-if="apiToken">
<a-button type="danger" style="min-width: 24px;" size="small" icon="delete" :id="'copy-api-token'" @click="removeApiToken"></a-button>
</a-tooltip>
</div>
</a-col>
</a-row>
</a-list-item>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'> <a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
<a-list item-layout="horizontal"> <a-list item-layout="horizontal">
@ -401,6 +423,7 @@
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
</a-layout> </a-layout>
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js?{{ .cur_ver }}"></script>
{{template "js" .}} {{template "js" .}}
<script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script>
{{template "component/themeSwitcher" .}} {{template "component/themeSwitcher" .}}
@ -522,7 +545,8 @@
sample = [] sample = []
this.remarkModel.forEach(r => sample.push(this.remarkModels[r])); this.remarkModel.forEach(r => sample.push(this.remarkModels[r]));
this.remarkSample = sample.length == 0 ? '' : sample.join(this.remarkSeparator); this.remarkSample = sample.length == 0 ? '' : sample.join(this.remarkSeparator);
} },
apiToken: null,
}, },
methods: { methods: {
loading(spinning = true) { loading(spinning = true) {
@ -535,6 +559,7 @@
if (msg.success) { if (msg.success) {
this.oldAllSetting = new AllSetting(msg.obj); this.oldAllSetting = new AllSetting(msg.obj);
this.allSetting = new AllSetting(msg.obj); this.allSetting = new AllSetting(msg.obj);
this.apiToken = msg.obj.apiToken;
app.changeRemarkSample(); app.changeRemarkSample();
this.saveBtnDisable = true; this.saveBtnDisable = true;
} }
@ -648,6 +673,38 @@
updatedNoises[index] = { ...updatedNoises[index], delay: value }; updatedNoises[index] = { ...updatedNoises[index], delay: value };
this.noisesArray = updatedNoises; 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: { computed: {
fragment: { fragment: {

View file

@ -596,3 +596,46 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
return result, nil 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
}

View file

@ -18,7 +18,7 @@ func init() {
gob.Register(model.User{}) gob.Register(model.User{})
} }
func SetLoginUser(c *gin.Context, user *model.User) { func SetSessionUser(c *gin.Context, user *model.User) {
if user == nil { if user == nil {
return 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) s := sessions.Default(c)
obj := s.Get(loginUserKey) obj := s.Get(loginUserKey)
if obj == nil { if obj == nil {
@ -51,7 +51,7 @@ func GetLoginUser(c *gin.Context) *model.User {
} }
func IsLogin(c *gin.Context) bool { func IsLogin(c *gin.Context) bool {
return GetLoginUser(c) != nil return GetSessionUser(c) != nil
} }
func ClearSession(c *gin.Context) { func ClearSession(c *gin.Context) {