veza/veza-backend-api/internal/handlers/profile_handler.go

249 lines
7.2 KiB
Go

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
}