Files
3x-ui/web/controller/index.go
lolka1333 8177f6dc66 ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds

- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize

* Remove hub_test.go file

* fix: ws hub, inbound service, and frontend correctness

- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count

* fix: chunk large IN ? queries and fix IPv6 same-origin check

* fix: chunk large IN ? queries and fix IPv6 same-origin check

* fix: unify clientStats cache, throttle clarity, hub constants

* fix(ui): align traffic/expiry cell columns across all rows

* style(ui): redesign outbounds table for visual consistency

* style(ui): redesign routing table for visual consistency

* fix:

* fix:

* fix:

* fix:

* fix:

* fix: font

* refactor: simplify outbound tone functions for consistency and maintainability

---------

Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 17:27:49 +02:00

125 lines
3.8 KiB
Go

package controller
import (
"fmt"
"net/http"
"text/template"
"time"
"github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/mhsanaei/3x-ui/v2/web/session"
"github.com/gin-gonic/gin"
)
// LoginForm represents the login request structure.
type LoginForm struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
}
// IndexController handles the main index and login-related routes.
type IndexController struct {
BaseController
settingService service.SettingService
userService service.UserService
tgbot service.Tgbot
}
// NewIndexController creates a new IndexController and initializes its routes.
func NewIndexController(g *gin.RouterGroup) *IndexController {
a := &IndexController{}
a.initRouter(g)
return a
}
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index)
g.GET("/logout", a.logout)
g.POST("/login", a.login)
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
}
// index handles the root route, redirecting logged-in users to the panel or showing the login page.
func (a *IndexController) index(c *gin.Context) {
if session.IsLogin(c) {
c.Redirect(http.StatusTemporaryRedirect, "panel/")
return
}
html(c, "login.html", "pages.login.title", nil)
}
// login handles user authentication and session creation.
func (a *IndexController) login(c *gin.Context) {
var form LoginForm
if err := c.ShouldBind(&form); err != nil {
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.invalidFormData"))
return
}
if form.Username == "" {
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.emptyUsername"))
return
}
if form.Password == "" {
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.emptyPassword"))
return
}
user, checkErr := 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)
if user == nil {
logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c))
notifyPass := safePass
if checkErr != nil && checkErr.Error() == "invalid 2fa code" {
translatedError := a.tgbot.I18nBot("tgbot.messages.2faFailed")
notifyPass = fmt.Sprintf("*** (%s)", translatedError)
}
a.tgbot.UserLoginNotify(safeUser, notifyPass, getRemoteIp(c), timeStr, 0)
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
return
}
logger.Infof("%s logged in successfully, Ip Address: %s\n", safeUser, getRemoteIp(c))
a.tgbot.UserLoginNotify(safeUser, ``, getRemoteIp(c), timeStr, 1)
if err := session.SetLoginUser(c, user); err != nil {
logger.Warning("Unable to save session:", err)
return
}
logger.Infof("%s logged in successfully", safeUser)
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
}
// logout handles user logout by clearing the session and redirecting to the login page.
func (a *IndexController) logout(c *gin.Context) {
user := session.GetLoginUser(c)
if user != nil {
logger.Infof("%s logged out successfully", user.Username)
}
if err := session.ClearSession(c); err != nil {
logger.Warning("Unable to clear session on logout:", err)
}
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
}
// getTwoFactorEnable retrieves the current status of two-factor authentication.
func (a *IndexController) getTwoFactorEnable(c *gin.Context) {
status, err := a.settingService.GetTwoFactorEnable()
if err == nil {
jsonObj(c, status, nil)
}
}