Files
Novault-backend/internal/handler/image_handler.go
2026-01-25 21:59:00 +08:00

200 lines
5.0 KiB
Go

package handler
import (
"errors"
"strconv"
"accounting-app/pkg/api"
"accounting-app/internal/service"
"github.com/gin-gonic/gin"
)
// ImageHandler handles HTTP requests for transaction image operations
// Feature: accounting-feature-upgrade
// Validates: Requirements 4.1-4.13
type ImageHandler struct {
imageService *service.ImageService
}
// NewImageHandler creates a new ImageHandler instance
func NewImageHandler(imageService *service.ImageService) *ImageHandler {
return &ImageHandler{
imageService: imageService,
}
}
// UploadImage handles POST /api/v1/transactions/:id/images
// Uploads an image attachment for a transaction
// Validates: Requirements 4.3, 4.4, 4.9-4.13
func (h *ImageHandler) UploadImage(c *gin.Context) {
userId, exists := c.Get("user_id")
if !exists {
api.Unauthorized(c, "User not authenticated")
return
}
// Parse transaction ID
transactionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
api.BadRequest(c, "Invalid transaction ID")
return
}
// Get compression level from query parameter (default: medium)
compressionStr := c.DefaultQuery("compression", "medium")
compression := service.CompressionLevel(compressionStr)
// Validate compression level
if compression != service.CompressionLow &&
compression != service.CompressionMedium &&
compression != service.CompressionHigh {
api.BadRequest(c, "Invalid compression level. Must be 'low', 'medium', or 'high'")
return
}
// Get uploaded file
file, err := c.FormFile("image")
if err != nil {
api.BadRequest(c, "No image file provided")
return
}
// Upload image
input := service.UploadImageInput{
UserID: userId.(uint),
TransactionID: uint(transactionID),
File: file,
Compression: compression,
}
image, err := h.imageService.UploadImage(input)
if err != nil {
handleImageError(c, err)
return
}
api.Created(c, image)
}
// GetImage handles GET /api/v1/images/:id
// Retrieves an image file by ID
// Validates: Requirements 4.8
func (h *ImageHandler) GetImage(c *gin.Context) {
userId, exists := c.Get("user_id")
if !exists {
api.Unauthorized(c, "User not authenticated")
return
}
// Parse image ID
imageID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
api.BadRequest(c, "Invalid image ID")
return
}
// Get image record
image, err := h.imageService.GetImage(userId.(uint), uint(imageID))
if err != nil {
handleImageError(c, err)
return
}
// Serve the file
c.File(image.FilePath)
}
// GetTransactionImages handles GET /api/v1/transactions/:id/images
// Retrieves all images for a transaction
// Validates: Requirements 4.8
func (h *ImageHandler) GetTransactionImages(c *gin.Context) {
userId, exists := c.Get("user_id")
if !exists {
api.Unauthorized(c, "User not authenticated")
return
}
// Parse transaction ID
transactionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
api.BadRequest(c, "Invalid transaction ID")
return
}
// Get images
images, err := h.imageService.GetImagesByTransaction(userId.(uint), uint(transactionID))
if err != nil {
handleImageError(c, err)
return
}
api.Success(c, images)
}
// DeleteImage handles DELETE /api/v1/transactions/:id/images/:imageId
// Deletes an image attachment
// Validates: Requirements 4.7
func (h *ImageHandler) DeleteImage(c *gin.Context) {
userId, exists := c.Get("user_id")
if !exists {
api.Unauthorized(c, "User not authenticated")
return
}
// Parse transaction ID
transactionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
api.BadRequest(c, "Invalid transaction ID")
return
}
// Parse image ID
imageID, err := strconv.ParseUint(c.Param("imageId"), 10, 32)
if err != nil {
api.BadRequest(c, "Invalid image ID")
return
}
// Delete image
err = h.imageService.DeleteImage(userId.(uint), uint(imageID), uint(transactionID))
if err != nil {
handleImageError(c, err)
return
}
api.NoContent(c)
}
// RegisterRoutes registers all image routes to the given router group
func (h *ImageHandler) RegisterRoutes(rg *gin.RouterGroup) {
// Transaction image routes
transactions := rg.Group("/transactions")
{
transactions.POST("/:id/images", h.UploadImage)
transactions.GET("/:id/images", h.GetTransactionImages)
transactions.DELETE("/:id/images/:imageId", h.DeleteImage)
}
// Direct image access route
rg.GET("/images/:id", h.GetImage)
}
// handleImageError handles common image service errors
func handleImageError(c *gin.Context, err error) {
switch {
case errors.Is(err, service.ErrInvalidImageFormat):
api.BadRequest(c, "Invalid image format. Only JPEG, PNG, and HEIC are supported")
case errors.Is(err, service.ErrImageTooLarge):
api.RequestEntityTooLarge(c, "Image size exceeds 10MB limit")
case errors.Is(err, service.ErrMaxImagesExceeded):
api.BadRequest(c, "Maximum 9 images per transaction")
case errors.Is(err, service.ErrImageTransactionNotFound):
api.NotFound(c, "Transaction not found")
case errors.Is(err, service.ErrImageNotFound):
api.NotFound(c, "Image not found")
default:
api.InternalError(c, "Failed to process image: "+err.Error())
}
}