package handlers import ( "net/http" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" "veza-backend-api/internal/services" "veza-backend-api/internal/types" ) // ProfileHandler handles profile-related operations type ProfileHandler struct { userService *services.UserService commonHandler *CommonHandler } // NewProfileHandler creates a new ProfileHandler instance func NewProfileHandler(userService *services.UserService, logger *zap.Logger) *ProfileHandler { return &ProfileHandler{ userService: userService, commonHandler: NewCommonHandler(logger), } } // GetProfile retrieves a public user profile by ID func (h *ProfileHandler) GetProfile(c *gin.Context) { userIDStr := c.Param("id") userID, err := uuid.Parse(userIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"}) return } // Get the requesting user ID if authenticated (optional) var requesterID *uuid.UUID if reqID, exists := c.Get("user_id"); exists { if reqUUID, ok := reqID.(uuid.UUID); ok { requesterID = &reqUUID } } // Get user profile with privacy check profile, err := h.userService.GetProfile(userID, requesterID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } c.JSON(http.StatusOK, gin.H{"profile": profile}) } // GetProfileByUsername retrieves a public profile by username func (h *ProfileHandler) GetProfileByUsername(c *gin.Context) { username := c.Param("username") if username == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "username required"}) return } // Get the requesting user ID if authenticated (optional) var requesterID *uuid.UUID if reqID, exists := c.Get("user_id"); exists { if reqUUID, ok := reqID.(uuid.UUID); ok { requesterID = &reqUUID } } // Get profile with privacy check profile, err := h.userService.GetProfileByUsername(username, requesterID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } c.JSON(http.StatusOK, gin.H{"profile": profile}) } // GetProfileCompletion retrieves the profile completion status // T0220: Returns percentage and missing fields func (h *ProfileHandler) GetProfileCompletion(c *gin.Context) { userIDStr := c.Param("id") userID, err := uuid.Parse(userIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"}) return } // Get authenticated user ID var authenticatedUserID uuid.UUID if reqID, exists := c.Get("user_id"); exists { if reqUUID, ok := reqID.(uuid.UUID); ok { authenticatedUserID = reqUUID } else { c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"}) return } } else { c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"}) return } // Verify that user_id corresponds to authenticated user if userID != authenticatedUserID { c.JSON(http.StatusForbidden, gin.H{"error": "cannot access other user's profile completion"}) return } // Calculate profile completion completion, err := h.userService.CalculateProfileCompletion(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to calculate profile completion"}) return } c.JSON(http.StatusOK, completion) } // UpdateProfileRequest represents the request body for updating a user profile type UpdateProfileRequest struct { FirstName string `json:"first_name" binding:"omitempty,max=100"` LastName string `json:"last_name" binding:"omitempty,max=100"` Username string `json:"username" binding:"omitempty,min=3,max=30"` Bio string `json:"bio" binding:"omitempty,max=500"` Location string `json:"location" binding:"omitempty,max=100"` Birthdate string `json:"birthdate" binding:"omitempty,datetime=2006-01-02"` Gender string `json:"gender" binding:"omitempty,oneof=Male Female Other 'Prefer not to say'"` } // UpdateProfile updates a user profile func (h *ProfileHandler) UpdateProfile(c *gin.Context) { userIDStr := c.Param("id") userID, err := uuid.Parse(userIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"}) return } // Get authenticated user ID var authenticatedUserID uuid.UUID if reqID, exists := c.Get("user_id"); exists { if reqUUID, ok := reqID.(uuid.UUID); ok { authenticatedUserID = reqUUID } else { c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"}) return } } else { c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"}) return } // Verify that user_id corresponds to authenticated user if userID != authenticatedUserID { c.JSON(http.StatusForbidden, gin.H{"error": "cannot update other user's profile"}) return } var req UpdateProfileRequest if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } // Validate username if provided if req.Username != "" { // Validate username format (alphanumeric + underscore, 3-30 chars) if !isValidUsername(req.Username) { c.JSON(http.StatusBadRequest, gin.H{"error": "username must be 3-30 characters, alphanumeric and underscore only"}) return } // Validate username uniqueness if modified if err := h.userService.ValidateUsername(userID, req.Username); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Check if username can be modified (once per month) canChange, err := h.userService.CanChangeUsername(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check username change eligibility"}) return } if !canChange { c.JSON(http.StatusBadRequest, gin.H{"error": "username can only be changed once per month"}) return } } // Validate birthdate if provided if req.Birthdate != "" { birthdate, err := time.Parse("2006-01-02", req.Birthdate) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid birthdate format, expected YYYY-MM-DD"}) return } // Check if user is at least 13 years old age := time.Since(birthdate) minAge := 13 * 365 * 24 * time.Hour // 13 years if age < minAge { c.JSON(http.StatusBadRequest, gin.H{"error": "user must be at least 13 years old"}) return } } // Convert UpdateProfileRequest to types.UpdateProfileRequest serviceReq := types.UpdateProfileRequest{ FirstName: &req.FirstName, LastName: &req.LastName, Username: &req.Username, Bio: &req.Bio, Location: &req.Location, Gender: &req.Gender, } if req.Birthdate != "" { birthdate, _ := time.Parse("2006-01-02", req.Birthdate) birthdateStr := birthdate.Format("2006-01-02") serviceReq.BirthDate = &birthdateStr } // Update profile using the new UpdateProfile method profile, err := h.userService.UpdateProfile(userID, serviceReq) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update profile"}) return } c.JSON(http.StatusOK, gin.H{"profile": profile}) } // isValidUsername validates username format (alphanumeric + underscore, 3-30 chars) func isValidUsername(username string) bool { if len(username) < 3 || len(username) > 30 { return false } for _, char := range username { if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_') { return false } } return true }