package handlers import ( "net/http" "github.com/labstack/echo/v4" "github.com/rs/zerolog/log" "github.com/treytartt/casera-api/internal/apperrors" "github.com/treytartt/casera-api/internal/middleware" "github.com/treytartt/casera-api/internal/models" "github.com/treytartt/casera-api/internal/services" ) // UploadHandler handles file upload endpoints type UploadHandler struct { storageService *services.StorageService } // NewUploadHandler creates a new upload handler func NewUploadHandler(storageService *services.StorageService) *UploadHandler { return &UploadHandler{storageService: storageService} } // 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. // // TODO(SEC-18): Add ownership verification. Currently any authenticated user can delete // any file if they know the URL. The upload system does not track which user uploaded // which file, so a proper fix requires adding an uploads table or file ownership metadata. // For now, deletions are logged with user ID for audit trail, and StorageService.Delete // enforces path containment to prevent deleting files outside the upload directory. func (h *UploadHandler) DeleteFile(c echo.Context) error { 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") } // Log the deletion with user ID for audit trail if user, ok := c.Get(middleware.AuthUserKey).(*models.User); ok { 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, map[string]interface{}{"message": "File deleted successfully"}) }