package handler import ( "crypto/md5" "crypto/rand" "encoding/hex" "fmt" "net/http" "strings" "time" "xboard-go/internal/database" "xboard-go/internal/model" "xboard-go/internal/service" "xboard-go/pkg/utils" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type LoginRequest struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` } type RegisterRequest struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=8"` InviteCode *string `json:"invite_code"` } func Login(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { Fail(c, http.StatusBadRequest, "invalid request body") return } var user model.User if err := database.DB.Where("email = ?", req.Email).First(&user).Error; err != nil { Fail(c, http.StatusUnauthorized, "email or password is incorrect") return } if !utils.CheckPassword(req.Password, user.Password, user.PasswordAlgo, user.PasswordSalt) { Fail(c, http.StatusUnauthorized, "email or password is incorrect") return } token, err := utils.GenerateToken(user.ID, user.IsAdmin) if err != nil { Fail(c, http.StatusInternalServerError, "failed to create auth token") return } now := time.Now().Unix() _ = database.DB.Model(&model.User{}).Where("id = ?", user.ID).Update("last_login_at", now).Error service.TrackSession(user.ID, token, c.ClientIP(), c.GetHeader("User-Agent")) Success(c, gin.H{ "token": token, "auth_data": token, "is_admin": user.IsAdmin, }) } func Register(c *gin.Context) { var req RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { Fail(c, http.StatusBadRequest, "invalid request body") return } var count int64 database.DB.Model(&model.User{}).Where("email = ?", req.Email).Count(&count) if count > 0 { Fail(c, http.StatusBadRequest, "email already exists") return } hashedPassword, err := utils.HashPassword(req.Password) if err != nil { Fail(c, http.StatusInternalServerError, "failed to hash password") return } now := time.Now().Unix() tokenRaw := fmt.Sprintf("%x", md5.Sum([]byte(time.Now().String()+req.Email))) user := model.User{ Email: req.Email, Password: hashedPassword, UUID: uuid.New().String(), Token: tokenRaw[:16], CreatedAt: now, UpdatedAt: now, } if err := database.DB.Create(&user).Error; err != nil { Fail(c, http.StatusInternalServerError, "register failed") return } if service.IsPluginEnabled(service.PluginUserAddIPv6) && user.PlanID != nil { service.SyncIPv6ShadowAccount(&user) } token, err := utils.GenerateToken(user.ID, user.IsAdmin) if err != nil { Fail(c, http.StatusInternalServerError, "failed to create auth token") return } service.TrackSession(user.ID, token, c.ClientIP(), c.GetHeader("User-Agent")) Success(c, gin.H{ "token": token, "auth_data": token, "is_admin": user.IsAdmin, }) } func Token2Login(c *gin.Context) { verify := strings.TrimSpace(c.Query("verify")) if verify == "" { Fail(c, http.StatusBadRequest, "verify token is required") return } userID, ok := service.ResolveQuickLoginToken(verify) if !ok { Fail(c, http.StatusUnauthorized, "verify token is invalid or expired") return } var user model.User if err := database.DB.First(&user, userID).Error; err != nil { Fail(c, http.StatusNotFound, "user not found") return } token, err := utils.GenerateToken(user.ID, user.IsAdmin) if err != nil { Fail(c, http.StatusInternalServerError, "failed to create auth token") return } service.TrackSession(user.ID, token, c.ClientIP(), c.GetHeader("User-Agent")) _ = database.DB.Model(&model.User{}).Where("id = ?", user.ID).Update("last_login_at", time.Now().Unix()).Error Success(c, gin.H{ "token": token, "auth_data": token, "is_admin": user.IsAdmin, }) } func SendEmailVerify(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` } if err := c.ShouldBindJSON(&req); err != nil { Fail(c, http.StatusBadRequest, "invalid email") return } code, err := randomDigits(6) if err != nil { Fail(c, http.StatusInternalServerError, "failed to generate verify code") return } service.StoreEmailVerifyCode(strings.ToLower(strings.TrimSpace(req.Email)), code, 10*time.Minute) SuccessMessage(c, "email verify code generated", gin.H{ "email": req.Email, "debug_code": code, "expires_in": 600, }) } func ForgetPassword(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` EmailCode string `json:"email_code" binding:"required"` Password string `json:"password" binding:"required,min=8"` } if err := c.ShouldBindJSON(&req); err != nil { Fail(c, http.StatusBadRequest, "invalid request body") return } email := strings.ToLower(strings.TrimSpace(req.Email)) if !service.CheckEmailVerifyCode(email, strings.TrimSpace(req.EmailCode)) { Fail(c, http.StatusBadRequest, "email verify code is invalid") return } hashed, err := utils.HashPassword(req.Password) if err != nil { Fail(c, http.StatusInternalServerError, "failed to hash password") return } updates := map[string]any{ "password": hashed, "password_algo": nil, "password_salt": nil, "updated_at": time.Now().Unix(), } if err := database.DB.Model(&model.User{}).Where("email = ?", email).Updates(updates).Error; err != nil { Fail(c, http.StatusInternalServerError, "password reset failed") return } SuccessMessage(c, "password reset success", true) } func randomDigits(length int) (string, error) { if length <= 0 { return "", nil } buf := make([]byte, length) if _, err := rand.Read(buf); err != nil { return "", err } encoded := hex.EncodeToString(buf) digits := make([]byte, 0, length) for i := 0; i < len(encoded) && len(digits) < length; i++ { digits = append(digits, '0'+(encoded[i]%10)) } return string(digits), nil }