index.go 5.4 KB

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