package handlers import ( "fmt" "net/http" "os" "strings" "gauth-central/internal/database" "gauth-central/internal/models" "gauth-central/pkg/utils" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" ) type ProfileHandler struct{} func NewProfileHandler() *ProfileHandler { return &ProfileHandler{} } // isOAuthUser checks if user is an OAuth user (has social accounts) func isOAuthUser(user *models.User) bool { // OAuth user if they have social accounts OR if they don't have a password return len(user.SocialAccounts) > 0 || user.Password == "" } // GetProfile godoc // @Summary Get current user profile // @Tags Profile // @Security ApiKeyAuth // @Produce json // @Success 200 {object} models.User // @Router /profile [get] func (h *ProfileHandler) GetProfile(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } var user models.User if err := database.DB. Preload("Roles"). Preload("Roles.Permissions"). Preload("SocialAccounts"). Where("id = ?", userID). First(&user).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } // Add is_oauth_user flag to response type ProfileResponse struct { models.User IsOAuthUser bool `json:"is_oauth_user"` } response := ProfileResponse{ User: user, IsOAuthUser: isOAuthUser(&user), } c.JSON(http.StatusOK, response) } // UpdateProfile godoc // @Summary Update current user profile // @Tags Profile // @Security ApiKeyAuth // @Accept multipart/form-data // @Produce json // @Param user_name formData string false "Username" // @Param avatar formData file false "Avatar image" // @Success 200 {object} map[string]interface{} // @Router /profile [put] func (h *ProfileHandler) UpdateProfile(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } // Try to parse as multipart form first contentType := c.GetHeader("Content-Type") isMultipart := strings.Contains(contentType, "multipart/form-data") updates := make(map[string]interface{}) if isMultipart { // Parse multipart form if err := c.Request.ParseMultipartForm(32 << 20); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse form data"}) return } // Get form values if userName := c.PostForm("user_name"); userName != "" { updates["user_name"] = userName } } else { // Parse as JSON var input struct { UserName *string `json:"user_name"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if input.UserName != nil { updates["user_name"] = *input.UserName } } // Update basic user fields if len(updates) > 0 { if err := database.DB.Model(&models.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"}) return } } // Handle avatar upload if multipart and file provided if isMultipart { avatarFile, err := c.FormFile("avatar") if err == nil && avatarFile != nil { // Validate file size (max 5MB) if avatarFile.Size > 5*1024*1024 { c.JSON(http.StatusBadRequest, gin.H{"error": "Avatar file size exceeds 5MB limit"}) return } // Get user to check for old avatar var user models.User if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } // Delete old avatar if exists and is local if user.Avatar != "" && strings.HasPrefix(user.Avatar, "/uploads/") { oldPath := "." + user.Avatar os.Remove(oldPath) } // Use utils.SaveOptimizedImage avatarURL, err := utils.SaveOptimizedImage(avatarFile, "./uploads/avatars", userID, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar: " + err.Error()}) return } // Update user avatar if err := database.DB.Model(&user).Update("avatar", avatarURL).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update avatar in database"}) return } } } // Get updated user to return var user models.User if err := database.DB. Preload("Roles"). Preload("SocialAccounts"). Where("id = ?", userID). First(&user).Error; err != nil { c.JSON(http.StatusOK, gin.H{"message": "Profile updated successfully"}) return } c.JSON(http.StatusOK, gin.H{ "message": "Profile updated successfully", "user": user, }) } // ChangePassword godoc // @Summary Change password // @Tags Profile // @Security ApiKeyAuth // @Accept json // @Produce json // @Param request body object true "Password change request" // @Success 200 {object} map[string]interface{} // @Router /profile/password [put] func (h *ProfileHandler) ChangePassword(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } var input struct { CurrentPassword string `json:"current_password" binding:"required"` NewPassword string `json:"new_password" binding:"required,min=6"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Get user var user models.User if err := database.DB.Preload("SocialAccounts").Where("id = ?", userID).First(&user).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } // Check if user is OAuth user if isOAuthUser(&user) { c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot change password for OAuth users (Google/GitHub login)"}) return } // Verify current password if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.CurrentPassword)); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Current password is incorrect"}) return } // Hash new password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.NewPassword), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"}) return } // Update password if err := database.DB.Model(&user).Update("password", string(hashedPassword)).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update password"}) return } c.JSON(http.StatusOK, gin.H{"message": "Password changed successfully"}) } // ChangeEmail godoc // @Summary Change email address // @Tags Profile // @Security ApiKeyAuth // @Accept json // @Produce json // @Param request body object true "Email change request" // @Success 200 {object} map[string]interface{} // @Router /profile/email [put] func (h *ProfileHandler) ChangeEmail(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } var input struct { NewEmail string `json:"new_email" binding:"required,email"` Password string `json:"password"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Get user var user models.User if err := database.DB.Preload("SocialAccounts").Where("id = ?", userID).First(&user).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } // Check if user is OAuth user if isOAuthUser(&user) { c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot change email for OAuth users (Google/GitHub login)"}) return } // Verify password if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Password is incorrect"}) return } // Check if new email already exists var existingUser models.User if err := database.DB.Where("email = ? AND id != ?", input.NewEmail, userID).First(&existingUser).Error; err == nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Email already in use"}) return } // Generate verification token verifyToken, err := utils.GenerateSecureToken(32) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate verification token"}) return } // Update email and set as unverified falseBool := false updates := map[string]interface{}{ "email": input.NewEmail, "email_verified": &falseBool, "email_verify_token": verifyToken, } if err := database.DB.Model(&user).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update email"}) return } // Send verification email go func() { if err := utils.SendVerificationEmail(input.NewEmail, verifyToken); err != nil { fmt.Printf("Failed to send verification email to %s: %v\n", input.NewEmail, err) } }() c.JSON(http.StatusOK, gin.H{ "message": "Email updated. Please verify your new email address.", "new_email": input.NewEmail, "verification_token": verifyToken, }) }