mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
ca4f32e3da
Instead of requiring a manual SOCKS5/HTTP URL, the panel now lets the admin pick an Xray outbound from a dropdown (same UX as Geodata Auto-Update). At runtime, injectPanelEgress appends a loopback SOCKS inbound (tag: panel-egress) and prepends a routing rule so the panel's own HTTP traffic — version checks, Telegram, normal geo-file updates — is routed through the chosen outbound. Xray-native Geodata Auto-Update is unaffected (it uses its own geodata.outbound inside Xray). Blackhole outbounds are excluded from both picker dropdowns since routing any download through one just drops it. Translations updated for all 13 locales.
201 lines
6.7 KiB
Go
201 lines
6.7 KiB
Go
package controller
|
|
|
|
import (
|
|
"errors"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/session"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// updateUserForm represents the form for updating user credentials.
|
|
type updateUserForm struct {
|
|
OldUsername string `json:"oldUsername" form:"oldUsername"`
|
|
OldPassword string `json:"oldPassword" form:"oldPassword"`
|
|
NewUsername string `json:"newUsername" form:"newUsername"`
|
|
NewPassword string `json:"newPassword" form:"newPassword"`
|
|
}
|
|
|
|
// SettingController handles settings and user management operations.
|
|
type SettingController struct {
|
|
settingService service.SettingService
|
|
userService panel.UserService
|
|
panelService panel.PanelService
|
|
apiTokenService panel.ApiTokenService
|
|
xrayService service.XrayService
|
|
}
|
|
|
|
// NewSettingController creates a new SettingController and initializes its routes.
|
|
func NewSettingController(g *gin.RouterGroup) *SettingController {
|
|
a := &SettingController{}
|
|
a.initRouter(g)
|
|
return a
|
|
}
|
|
|
|
// initRouter sets up the routes for settings management.
|
|
func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
|
g = g.Group("/setting")
|
|
|
|
g.POST("/all", a.getAllSetting)
|
|
g.POST("/defaultSettings", a.getDefaultSettings)
|
|
g.POST("/update", a.updateSetting)
|
|
g.POST("/updateUser", a.updateUser)
|
|
g.POST("/restartPanel", a.restartPanel)
|
|
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
|
g.GET("/apiTokens", a.listApiTokens)
|
|
g.POST("/apiTokens/create", a.createApiToken)
|
|
g.POST("/apiTokens/delete/:id", a.deleteApiToken)
|
|
g.POST("/apiTokens/setEnabled/:id", a.setApiTokenEnabled)
|
|
}
|
|
|
|
// getAllSetting retrieves all current settings.
|
|
func (a *SettingController) getAllSetting(c *gin.Context) {
|
|
allSetting, err := a.settingService.GetAllSetting()
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
return
|
|
}
|
|
jsonObj(c, allSetting, nil)
|
|
}
|
|
|
|
// getDefaultSettings retrieves the default settings based on the host.
|
|
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
|
result, err := a.settingService.GetDefaultSettings(c.Request.Host)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
return
|
|
}
|
|
jsonObj(c, result, nil)
|
|
}
|
|
|
|
// updateSetting updates all settings with the provided data.
|
|
func (a *SettingController) updateSetting(c *gin.Context) {
|
|
allSetting, ok := middleware.BindAndValidate[entity.AllSetting](c)
|
|
if !ok {
|
|
return
|
|
}
|
|
oldTwoFactor, twoFactorErr := a.settingService.GetTwoFactorEnable()
|
|
oldPanelOutbound, _ := a.settingService.GetPanelOutbound()
|
|
err := a.settingService.UpdateAllSetting(allSetting)
|
|
if err == nil && twoFactorErr == nil && !oldTwoFactor && allSetting.TwoFactorEnable {
|
|
if bumpErr := a.userService.BumpLoginEpoch(); bumpErr != nil {
|
|
err = bumpErr
|
|
}
|
|
}
|
|
if err == nil && allSetting.PanelOutbound != oldPanelOutbound {
|
|
// The egress bridge lives in the generated config; reconcile the
|
|
// running core. One SOCKS inbound plus one routing rule — both
|
|
// hot-appliable, so this normally does not restart Xray.
|
|
if applyErr := a.xrayService.RestartXray(false); applyErr != nil {
|
|
logger.Warning("apply panel outbound change failed:", applyErr)
|
|
}
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
|
}
|
|
|
|
// updateUser updates the current user's username and password.
|
|
func (a *SettingController) updateUser(c *gin.Context) {
|
|
form := &updateUserForm{}
|
|
err := c.ShouldBind(form)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
|
return
|
|
}
|
|
user := session.GetLoginUser(c)
|
|
if user.Username != form.OldUsername || !crypto.CheckPasswordHash(user.Password, form.OldPassword) {
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
|
|
return
|
|
}
|
|
if form.NewUsername == "" || form.NewPassword == "" {
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
|
|
return
|
|
}
|
|
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
|
|
if err == nil {
|
|
user.Username = form.NewUsername
|
|
user.Password, _ = crypto.HashPasswordAsBcrypt(form.NewPassword)
|
|
if saveErr := session.SetLoginUser(c, user); saveErr != nil {
|
|
err = saveErr
|
|
}
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
|
|
}
|
|
|
|
// restartPanel restarts the panel service after a delay.
|
|
func (a *SettingController) restartPanel(c *gin.Context) {
|
|
err := a.panelService.RestartPanel(time.Second * 3)
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanelSuccess"), err)
|
|
}
|
|
|
|
// getDefaultXrayConfig retrieves the default Xray configuration.
|
|
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
|
|
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
return
|
|
}
|
|
jsonObj(c, defaultJsonConfig, nil)
|
|
}
|
|
|
|
type apiTokenCreateForm struct {
|
|
Name string `json:"name" form:"name"`
|
|
}
|
|
|
|
type apiTokenEnabledForm struct {
|
|
Enabled bool `json:"enabled" form:"enabled"`
|
|
}
|
|
|
|
func (a *SettingController) listApiTokens(c *gin.Context) {
|
|
rows, err := a.apiTokenService.List()
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
return
|
|
}
|
|
jsonObj(c, rows, nil)
|
|
}
|
|
|
|
func (a *SettingController) createApiToken(c *gin.Context) {
|
|
form := &apiTokenCreateForm{}
|
|
if err := c.ShouldBind(form); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
|
return
|
|
}
|
|
row, err := a.apiTokenService.Create(form.Name)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
|
return
|
|
}
|
|
jsonObj(c, row, nil)
|
|
}
|
|
|
|
func (a *SettingController) deleteApiToken(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), a.apiTokenService.Delete(id))
|
|
}
|
|
|
|
func (a *SettingController) setApiTokenEnabled(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
|
return
|
|
}
|
|
form := &apiTokenEnabledForm{}
|
|
if bindErr := c.ShouldBind(form); bindErr != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), bindErr)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), a.apiTokenService.SetEnabled(id, form.Enabled))
|
|
}
|