package handlers import ( "path/filepath" "strconv" "strings" "github.com/labstack/echo/v4" "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/repositories" "github.com/treytartt/casera-api/internal/services" ) // MediaHandler handles authenticated media serving type MediaHandler struct { documentRepo *repositories.DocumentRepository taskRepo *repositories.TaskRepository residenceRepo *repositories.ResidenceRepository storageSvc *services.StorageService } // NewMediaHandler creates a new media handler func NewMediaHandler( documentRepo *repositories.DocumentRepository, taskRepo *repositories.TaskRepository, residenceRepo *repositories.ResidenceRepository, storageSvc *services.StorageService, ) *MediaHandler { return &MediaHandler{ documentRepo: documentRepo, taskRepo: taskRepo, residenceRepo: residenceRepo, storageSvc: storageSvc, } } // ServeDocument serves a document file with access control // GET /api/media/document/:id func (h *MediaHandler) ServeDocument(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_id") } // Get document doc, err := h.documentRepo.FindByID(uint(id)) if err != nil { return apperrors.NotFound("error.document_not_found") } // Check access to residence hasAccess, err := h.residenceRepo.HasAccess(doc.ResidenceID, user.ID) if err != nil || !hasAccess { return apperrors.Forbidden("error.access_denied") } // Serve the file filePath := h.resolveFilePath(doc.FileURL) if filePath == "" { return apperrors.NotFound("error.file_not_found") } // Set caching headers (private, 1 hour) c.Response().Header().Set("Cache-Control", "private, max-age=3600") return c.File(filePath) } // ServeDocumentImage serves a document image with access control // GET /api/media/document-image/:id func (h *MediaHandler) ServeDocumentImage(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_id") } // Get document image img, err := h.documentRepo.FindImageByID(uint(id)) if err != nil { return apperrors.NotFound("error.image_not_found") } // Get parent document to check residence access doc, err := h.documentRepo.FindByID(img.DocumentID) if err != nil { return apperrors.NotFound("error.document_not_found") } // Check access to residence hasAccess, err := h.residenceRepo.HasAccess(doc.ResidenceID, user.ID) if err != nil || !hasAccess { return apperrors.Forbidden("error.access_denied") } // Serve the file filePath := h.resolveFilePath(img.ImageURL) if filePath == "" { return apperrors.NotFound("error.file_not_found") } c.Response().Header().Set("Cache-Control", "private, max-age=3600") return c.File(filePath) } // ServeCompletionImage serves a task completion image with access control // GET /api/media/completion-image/:id func (h *MediaHandler) ServeCompletionImage(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_id") } // Get completion image img, err := h.taskRepo.FindCompletionImageByID(uint(id)) if err != nil { return apperrors.NotFound("error.image_not_found") } // Get the completion to get the task completion, err := h.taskRepo.FindCompletionByID(img.CompletionID) if err != nil { return apperrors.NotFound("error.completion_not_found") } // Get task to check residence access task, err := h.taskRepo.FindByID(completion.TaskID) if err != nil { return apperrors.NotFound("error.task_not_found") } // Check access to residence hasAccess, err := h.residenceRepo.HasAccess(task.ResidenceID, user.ID) if err != nil || !hasAccess { return apperrors.Forbidden("error.access_denied") } // Serve the file filePath := h.resolveFilePath(img.ImageURL) if filePath == "" { return apperrors.NotFound("error.file_not_found") } c.Response().Header().Set("Cache-Control", "private, max-age=3600") return c.File(filePath) } // resolveFilePath converts a stored URL to an actual file path func (h *MediaHandler) resolveFilePath(storedURL string) string { if storedURL == "" { return "" } uploadDir := h.storageSvc.GetUploadDir() // Handle legacy /uploads/... URLs if strings.HasPrefix(storedURL, "/uploads/") { relativePath := strings.TrimPrefix(storedURL, "/uploads/") return filepath.Join(uploadDir, relativePath) } // Handle relative paths (new format) return filepath.Join(uploadDir, storedURL) }