feat: complete production readiness improvements
- Fix DIP violations in service layer (device, stats, auth middleware) - Add ReplaceUserRoles interface method for transaction safety - Implement Magic Bytes validation for avatar uploads - Standardize OAuth error handling with ErrOAuthProviderNotSupported - Use crypto/rand for JWT secret generation instead of weak fixed key - Apply code formatting with gofumpt and goimports - Fix staticcheck issues (S1024, S1008, ST1005) - Add comprehensive quality and functional test reports - Achieve 36.3% test coverage (up from 16.3%) - All E2E, integration, and business logic tests passing
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -12,16 +14,21 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/user-management-system/internal/domain"
|
||||
"github.com/user-management-system/internal/repository"
|
||||
)
|
||||
|
||||
// avatarUserRepository interface for dependency inversion (DIP)
|
||||
type avatarUserRepository interface {
|
||||
GetByID(ctx context.Context, id int64) (*domain.User, error)
|
||||
Update(ctx context.Context, user *domain.User) error
|
||||
}
|
||||
|
||||
// AvatarHandler handles avatar upload requests
|
||||
type AvatarHandler struct {
|
||||
userRepo *repository.UserRepository
|
||||
userRepo avatarUserRepository
|
||||
}
|
||||
|
||||
// NewAvatarHandler creates a new AvatarHandler
|
||||
func NewAvatarHandler(userRepo *repository.UserRepository) *AvatarHandler {
|
||||
func NewAvatarHandler(userRepo avatarUserRepository) *AvatarHandler {
|
||||
return &AvatarHandler{userRepo: userRepo}
|
||||
}
|
||||
|
||||
@@ -107,12 +114,37 @@ func (h *AvatarHandler) UploadAvatar(c *gin.Context) {
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// Validate Magic Bytes to detect actual file type (prevents file extension spoofing)
|
||||
buf := make([]byte, 512)
|
||||
n, err := src.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "failed to read file"})
|
||||
return
|
||||
}
|
||||
contentType := http.DetectContentType(buf[:n])
|
||||
allowedMIME := map[string]bool{
|
||||
"image/jpeg": true,
|
||||
"image/png": true,
|
||||
"image/gif": true,
|
||||
"image/webp": true,
|
||||
}
|
||||
if !allowedMIME[contentType] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid file content, allowed: jpeg, png, gif, webp"})
|
||||
return
|
||||
}
|
||||
|
||||
// Seek back to beginning for full file read
|
||||
if _, err := src.Seek(0, io.SeekStart); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to read file"})
|
||||
return
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if err := os.MkdirAll(uploadDir, 0o755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to create upload directory"})
|
||||
return
|
||||
}
|
||||
@@ -124,7 +156,7 @@ func (h *AvatarHandler) UploadAvatar(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to read uploaded file"})
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(dstPath, data, 0644); err != nil {
|
||||
if err := os.WriteFile(dstPath, data, 0o644); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to save avatar file"})
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user