index.go 5.7 KB

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