package handlers import ( "net/http" "github.com/labstack/echo/v4" "github.com/rs/zerolog/log" "github.com/treytartt/honeydue-api/internal/apperrors" "github.com/treytartt/honeydue-api/internal/dto/responses" "github.com/treytartt/honeydue-api/internal/i18n" "github.com/treytartt/honeydue-api/internal/middleware" "github.com/treytartt/honeydue-api/internal/services" ) // FileOwnershipChecker verifies whether a user owns a file referenced by URL. // Implementations should check associated records (e.g., task completion images, // document files, document images) to determine ownership. type FileOwnershipChecker interface { IsFileOwnedByUser(fileURL string, userID uint) (bool, error) } // UploadHandler handles file upload endpoints type UploadHandler struct { storageService *services.StorageService fileOwnershipChecker FileOwnershipChecker } // NewUploadHandler creates a new upload handler func NewUploadHandler(storageService *services.StorageService, fileOwnershipChecker FileOwnershipChecker) *UploadHandler { return &UploadHandler{ storageService: storageService, fileOwnershipChecker: fileOwnershipChecker, } } // UploadImage handles POST /api/uploads/image // Accepts multipart/form-data with "file" field func (h *UploadHandler) UploadImage(c echo.Context) error { file, err := c.FormFile("file") if err != nil { return apperrors.BadRequest("error.no_file_provided") } // Get category from query param (default: images) category := c.QueryParam("category") if category == "" { category = "images" } result, err := h.storageService.Upload(file, category) if err != nil { return err } return c.JSON(http.StatusOK, result) } // UploadDocument handles POST /api/uploads/document // Accepts multipart/form-data with "file" field func (h *UploadHandler) UploadDocument(c echo.Context) error { file, err := c.FormFile("file") if err != nil { return apperrors.BadRequest("error.no_file_provided") } result, err := h.storageService.Upload(file, "documents") if err != nil { return err } return c.JSON(http.StatusOK, result) } // UploadCompletion handles POST /api/uploads/completion // For task completion photos func (h *UploadHandler) UploadCompletion(c echo.Context) error { file, err := c.FormFile("file") if err != nil { return apperrors.BadRequest("error.no_file_provided") } result, err := h.storageService.Upload(file, "completions") if err != nil { return err } return c.JSON(http.StatusOK, result) } // DeleteFileRequest is the request body for deleting a file. type DeleteFileRequest struct { URL string `json:"url" validate:"required"` } // DeleteFile handles DELETE /api/uploads // Expects JSON body with "url" field. // Verifies that the requesting user owns the file by checking associated records // (task completion images, document files/images) before allowing deletion. func (h *UploadHandler) DeleteFile(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } var req DeleteFileRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return apperrors.BadRequest("error.url_required") } // Verify ownership: the user must own a record that references this file URL if h.fileOwnershipChecker != nil { owned, err := h.fileOwnershipChecker.IsFileOwnedByUser(req.URL, user.ID) if err != nil { log.Error().Err(err).Uint("user_id", user.ID).Str("file_url", req.URL).Msg("Failed to check file ownership") return apperrors.Internal(err) } if !owned { log.Warn().Uint("user_id", user.ID).Str("file_url", req.URL).Msg("Unauthorized file deletion attempt") return apperrors.Forbidden("error.file_access_denied") } } // Log the deletion with user ID for audit trail log.Info(). Uint("user_id", user.ID). Str("file_url", req.URL). Msg("File deletion requested") if err := h.storageService.Delete(req.URL); err != nil { return err } return c.JSON(http.StatusOK, responses.MessageResponse{Message: i18n.LocalizedMessage(c, "message.file_deleted")}) }