index.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. package controller
  2. import (
  3. "net/http"
  4. "text/template"
  5. "time"
  6. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  7. "github.com/mhsanaei/3x-ui/v3/internal/web/entity"
  8. "github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
  9. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  10. "github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
  11. "github.com/mhsanaei/3x-ui/v3/internal/web/service/tgbot"
  12. "github.com/mhsanaei/3x-ui/v3/internal/web/session"
  13. "github.com/gin-gonic/gin"
  14. )
  15. // LoginForm represents the login request structure.
  16. type LoginForm struct {
  17. Username string `json:"username" form:"username"`
  18. Password string `json:"password" form:"password"`
  19. TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
  20. }
  21. // IndexController handles the main index and login-related routes.
  22. type IndexController struct {
  23. BaseController
  24. settingService service.SettingService
  25. userService panel.UserService
  26. panelService panel.PanelService
  27. tgbot tgbot.Tgbot
  28. }
  29. // NewIndexController creates a new IndexController and initializes its routes.
  30. func NewIndexController(g *gin.RouterGroup) *IndexController {
  31. a := &IndexController{}
  32. a.initRouter(g)
  33. return a
  34. }
  35. // initRouter sets up the routes for index, login, logout, and two-factor authentication.
  36. func (a *IndexController) initRouter(g *gin.RouterGroup) {
  37. g.GET("/", a.index)
  38. g.GET("/csrf-token", a.csrfToken)
  39. g.GET("/sponsors", a.sponsors)
  40. g.GET("/sponsors/logo/:name", a.sponsorLogo)
  41. g.POST("/login", middleware.CSRFMiddleware(), a.login)
  42. g.POST("/logout", middleware.CSRFMiddleware(), a.logout)
  43. g.POST("/getTwoFactorEnable", middleware.CSRFMiddleware(), a.getTwoFactorEnable)
  44. }
  45. // sponsors is public so the login page can render its slot; failures stay silent.
  46. func (a *IndexController) sponsors(c *gin.Context) {
  47. list, err := a.panelService.GetSponsors()
  48. if err != nil {
  49. logger.Debug("sponsors fetch failed:", err)
  50. c.JSON(http.StatusOK, entity.Msg{Success: false})
  51. return
  52. }
  53. jsonObj(c, list, nil)
  54. }
  55. func (a *IndexController) sponsorLogo(c *gin.Context) {
  56. data, contentType, err := a.panelService.GetSponsorLogo(c.Param("name"))
  57. if err != nil {
  58. logger.Debug("sponsor logo failed:", err)
  59. c.Status(http.StatusNotFound)
  60. return
  61. }
  62. c.Header("Cache-Control", "public, max-age=3600")
  63. c.Data(http.StatusOK, contentType, data)
  64. }
  65. // index handles the root route, redirecting logged-in users to the panel or showing the login page.
  66. func (a *IndexController) index(c *gin.Context) {
  67. if session.IsLogin(c) {
  68. c.Header("Cache-Control", "no-store")
  69. c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")+"panel/")
  70. return
  71. }
  72. serveDistPage(c, "login.html")
  73. }
  74. // login handles user authentication and session creation.
  75. func (a *IndexController) login(c *gin.Context) {
  76. var form LoginForm
  77. if err := c.ShouldBind(&form); err != nil {
  78. pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.invalidFormData"))
  79. return
  80. }
  81. if form.Username == "" {
  82. pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.emptyUsername"))
  83. return
  84. }
  85. if form.Password == "" {
  86. pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.emptyPassword"))
  87. return
  88. }
  89. remoteIP := getRemoteIp(c)
  90. safeUser := template.HTMLEscapeString(form.Username)
  91. timeStr := time.Now().Format("2006-01-02 15:04:05")
  92. if blockedUntil, ok := defaultLoginLimiter.allow(remoteIP, form.Username); !ok {
  93. reason := "too many failed attempts"
  94. logger.Warningf("failed login: username=%q, IP=%q, reason=%q, blocked_until=%s", form.Username, remoteIP, reason, blockedUntil.Format(time.RFC3339))
  95. a.tgbot.UserLoginNotify(tgbot.LoginAttempt{
  96. Username: safeUser,
  97. IP: remoteIP,
  98. Time: timeStr,
  99. Status: tgbot.LoginFail,
  100. Reason: reason,
  101. })
  102. pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
  103. return
  104. }
  105. user, checkErr := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
  106. if user == nil {
  107. reason := loginFailureReason(checkErr)
  108. if blockedUntil, blocked := defaultLoginLimiter.registerFailure(remoteIP, form.Username); blocked {
  109. logger.Warningf("failed login: username=%q, IP=%q, reason=%q, blocked_until=%s", form.Username, remoteIP, reason, blockedUntil.Format(time.RFC3339))
  110. } else {
  111. logger.Warningf("failed login: username=%q, IP=%q, reason=%q", form.Username, remoteIP, reason)
  112. }
  113. a.tgbot.UserLoginNotify(tgbot.LoginAttempt{
  114. Username: safeUser,
  115. IP: remoteIP,
  116. Time: timeStr,
  117. Status: tgbot.LoginFail,
  118. Reason: reason,
  119. })
  120. pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
  121. return
  122. }
  123. defaultLoginLimiter.registerSuccess(remoteIP, form.Username)
  124. logger.Infof("logged in successfully: username=%q, IP=%q", form.Username, remoteIP)
  125. a.tgbot.UserLoginNotify(tgbot.LoginAttempt{
  126. Username: safeUser,
  127. IP: remoteIP,
  128. Time: timeStr,
  129. Status: tgbot.LoginSuccess,
  130. })
  131. if err := session.SetLoginUser(c, user); err != nil {
  132. logger.Warning("Unable to save session:", err)
  133. return
  134. }
  135. jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
  136. }
  137. func loginFailureReason(err error) string {
  138. if err != nil && err.Error() == "invalid 2fa code" {
  139. return "invalid 2FA code"
  140. }
  141. return "invalid credentials"
  142. }
  143. func (a *IndexController) logout(c *gin.Context) {
  144. user := session.GetLoginUser(c)
  145. if user != nil {
  146. logger.Infof("logged out successfully: username=%q", user.Username)
  147. }
  148. if err := session.ClearSession(c); err != nil {
  149. logger.Warning("Unable to clear session on logout:", err)
  150. }
  151. c.Header("Cache-Control", "no-store")
  152. c.JSON(http.StatusOK, gin.H{"success": true})
  153. }
  154. // csrfToken returns the session CSRF token. Public — the login page
  155. // needs a token before authenticating.
  156. func (a *IndexController) csrfToken(c *gin.Context) {
  157. token, err := session.EnsureCSRFToken(c)
  158. if err != nil {
  159. c.JSON(http.StatusInternalServerError, gin.H{"success": false, "msg": err.Error()})
  160. return
  161. }
  162. c.JSON(http.StatusOK, gin.H{"success": true, "obj": token})
  163. }
  164. // getTwoFactorEnable retrieves the current status of two-factor authentication.
  165. func (a *IndexController) getTwoFactorEnable(c *gin.Context) {
  166. status, err := a.settingService.GetTwoFactorEnable()
  167. jsonObj(c, status, err)
  168. }