package handler import ( "crypto/rand" "encoding/hex" "fmt" "net/http" "os" "path/filepath" "strconv" "github.com/gin-gonic/gin" "github.com/user-management-system/internal/domain" "github.com/user-management-system/internal/repository" ) // AvatarHandler handles avatar upload requests type AvatarHandler struct { userRepo *repository.UserRepository } // NewAvatarHandler creates a new AvatarHandler func NewAvatarHandler(userRepo *repository.UserRepository) *AvatarHandler { return &AvatarHandler{userRepo: userRepo} } // generateSecureToken generates a secure random token func generateSecureToken(length int) string { bytes := make([]byte, length) rand.Read(bytes) return hex.EncodeToString(bytes)[:length] } // UploadAvatar handles avatar file upload func (h *AvatarHandler) UploadAvatar(c *gin.Context) { userIDStr := c.Param("id") userID, err := strconv.ParseInt(userIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"}) return } // Get current user from context (set by auth middleware) currentUserID := c.GetInt64("user_id") if currentUserID == 0 { c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"}) return } // Check permission: user can only update their own avatar, or admin can update any isAdmin := false if roles, ok := c.Get("user_roles"); ok { for _, role := range roles.([]*domain.Role) { if role.Code == "admin" { isAdmin = true break } } } if currentUserID != userID && !isAdmin { c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "permission denied"}) return } // Get file from form file, err := c.FormFile("avatar") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "no avatar file provided"}) return } // Validate file size (max 5MB) if file.Size > 5*1024*1024 { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "file size exceeds 5MB limit"}) return } // Validate file type ext := filepath.Ext(file.Filename) allowedExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true} if !allowedExts[ext] { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid file type, allowed: jpg, jpeg, png, gif, webp"}) return } // Open the uploaded file src, err := file.Open() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to open uploaded file"}) return } defer src.Close() // Generate unique filename avatarFilename := fmt.Sprintf("avatar_%d_%s%s", userID, generateSecureToken(8), ext) uploadDir := "./uploads/avatars" // Create upload directory if not exists if err := os.MkdirAll(uploadDir, 0755); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to create upload directory"}) return } // Save file to disk dstPath := filepath.Join(uploadDir, avatarFilename) data := make([]byte, file.Size) if _, err := src.Read(data); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to read uploaded file"}) return } if err := os.WriteFile(dstPath, data, 0644); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to save avatar file"}) return } // Generate avatar URL (in production, this would be a CDN URL) avatarURL := fmt.Sprintf("/uploads/avatars/%s", avatarFilename) // Update user's avatar in database user, err := h.userRepo.GetByID(c.Request.Context(), userID) if err != nil { // Clean up the uploaded file os.Remove(dstPath) c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "user not found"}) return } user.Avatar = avatarURL if err := h.userRepo.Update(c.Request.Context(), user); err != nil { // Clean up the uploaded file os.Remove(dstPath) c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to update user avatar"}) return } c.JSON(http.StatusOK, gin.H{ "code": 0, "message": "avatar uploaded successfully", "data": gin.H{ "avatar_url": avatarURL, "thumbnail": avatarURL, }, }) }