package handlers import ( "errors" "net/http" "strings" "time" "github.com/celestia-trace/backend/config" "github.com/celestia-trace/backend/models" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) type AuthHandler struct { DB *gorm.DB Cfg *config.Config } func respondError(c *gin.Context, status int, code int, message string) { c.JSON(status, models.ErrorResponse{Code: code, Message: message}) } func respondSuccess(c *gin.Context, data interface{}) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": data}) } func (h *AuthHandler) generateToken(userID string) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "sub": userID, "exp": time.Now().Add(7 * 24 * time.Hour).Unix(), }) return token.SignedString([]byte(h.Cfg.JWTSecret)) } func isEmail(identifier string) bool { return strings.Contains(identifier, "@") } // Register handler func (h *AuthHandler) Register(c *gin.Context) { var req models.RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { respondError(c, http.StatusBadRequest, 400, "invalid input data") return } hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10) if err != nil { respondError(c, http.StatusInternalServerError, 500, "failed to hash password") return } user := models.User{ Username: req.Username, PasswordHash: string(hash), } if isEmail(req.Identifier) { user.Email = &req.Identifier } else { user.PhoneNumber = &req.Identifier } if err := h.DB.Create(&user).Error; err != nil { if strings.Contains(err.Error(), "duplicate key") || strings.Contains(err.Error(), "UNIQUE constraint") { respondError(c, http.StatusConflict, 409, "username or identifier already exists") return } respondError(c, http.StatusInternalServerError, 500, "failed to create user") return } token, err := h.generateToken(user.ID) if err != nil { respondError(c, http.StatusInternalServerError, 500, "failed to generate token") return } respondSuccess(c, models.AuthResponse{User: user, Token: token}) } // Login handler func (h *AuthHandler) Login(c *gin.Context) { var req models.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { respondError(c, http.StatusBadRequest, 400, "invalid input data") return } var user models.User if err := h.DB.Where("username = ? OR email = ? OR phone_number = ?", req.Identifier, req.Identifier, req.Identifier).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { respondError(c, http.StatusUnauthorized, 401, "invalid credentials") return } respondError(c, http.StatusInternalServerError, 500, "database error") return } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { respondError(c, http.StatusUnauthorized, 401, "invalid credentials") return } token, err := h.generateToken(user.ID) if err != nil { respondError(c, http.StatusInternalServerError, 500, "failed to generate token") return } respondSuccess(c, models.AuthResponse{User: user, Token: token}) } // Logout handler func (h *AuthHandler) Logout(c *gin.Context) { // Client side discards the token respondSuccess(c, nil) } // GetProfile handler func (h *AuthHandler) GetProfile(c *gin.Context) { userID := c.GetString("userID") var user models.User if err := h.DB.First(&user, "id = ?", userID).Error; err != nil { respondError(c, http.StatusNotFound, 404, "user not found") return } respondSuccess(c, user) } // UpdateProfile handler func (h *AuthHandler) UpdateProfile(c *gin.Context) { userID := c.GetString("userID") var req models.UpdateProfileRequest if err := c.ShouldBindJSON(&req); err != nil { respondError(c, http.StatusBadRequest, 400, "invalid input data") return } var user models.User if err := h.DB.First(&user, "id = ?", userID).Error; err != nil { respondError(c, http.StatusNotFound, 404, "user not found") return } if req.Username != nil && len(*req.Username) >= 2 { user.Username = *req.Username } if req.Email != nil { user.Email = req.Email } if req.PhoneNumber != nil { user.PhoneNumber = req.PhoneNumber } if req.AvatarURL != nil { user.AvatarURL = req.AvatarURL } if err := h.DB.Save(&user).Error; err != nil { respondError(c, http.StatusInternalServerError, 500, "failed to update profile") return } respondSuccess(c, user) } // ChangePassword handler func (h *AuthHandler) ChangePassword(c *gin.Context) { userID := c.GetString("userID") var req models.ChangePasswordRequest if err := c.ShouldBindJSON(&req); err != nil { respondError(c, http.StatusBadRequest, 400, "invalid input data") return } var user models.User if err := h.DB.First(&user, "id = ?", userID).Error; err != nil { respondError(c, http.StatusNotFound, 404, "user not found") return } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.OldPassword)); err != nil { respondError(c, http.StatusUnauthorized, 401, "invalid old password") return } hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), 10) if err != nil { respondError(c, http.StatusInternalServerError, 500, "failed to hash new password") return } user.PasswordHash = string(hash) if err := h.DB.Save(&user).Error; err != nil { respondError(c, http.StatusInternalServerError, 500, "failed to change password") return } respondSuccess(c, nil) }