Add secure media access control with authenticated proxy endpoints

- Add MediaHandler with token-based proxy endpoints for serving media:
  - GET /api/media/document/:id
  - GET /api/media/document-image/:id
  - GET /api/media/completion-image/:id
- Add MediaURL fields to response DTOs for documents and task completions
- Add FindImageByID and FindCompletionImageByID repository methods
- Preload Completions.Images in all task queries for proper media URLs
- Remove public /uploads static file serving for security
- Verify residence access before serving any media files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-02 19:47:39 -06:00
parent ed21d9267d
commit 76579e8bf8
7 changed files with 250 additions and 6 deletions

View File

@@ -1,6 +1,7 @@
package responses
import (
"fmt"
"time"
"github.com/shopspring/decimal"
@@ -20,6 +21,7 @@ type DocumentUserResponse struct {
type DocumentImageResponse struct {
ID uint `json:"id"`
ImageURL string `json:"image_url"`
MediaURL string `json:"media_url"` // Authenticated endpoint: /api/media/document-image/{id}
Caption string `json:"caption"`
}
@@ -34,6 +36,7 @@ type DocumentResponse struct {
Description string `json:"description"`
DocumentType models.DocumentType `json:"document_type"`
FileURL string `json:"file_url"`
MediaURL string `json:"media_url"` // Authenticated endpoint: /api/media/document/{id}
FileName string `json:"file_name"`
FileSize *int64 `json:"file_size"`
MimeType string `json:"mime_type"`
@@ -78,6 +81,7 @@ func NewDocumentResponse(d *models.Document) DocumentResponse {
Description: d.Description,
DocumentType: d.DocumentType,
FileURL: d.FileURL,
MediaURL: fmt.Sprintf("/api/media/document/%d", d.ID), // Authenticated endpoint
FileName: d.FileName,
FileSize: d.FileSize,
MimeType: d.MimeType,
@@ -98,11 +102,12 @@ func NewDocumentResponse(d *models.Document) DocumentResponse {
resp.CreatedBy = NewDocumentUserResponse(&d.CreatedBy)
}
// Convert images
// Convert images with authenticated media URLs
for _, img := range d.Images {
resp.Images = append(resp.Images, DocumentImageResponse{
ID: img.ID,
ImageURL: img.ImageURL,
MediaURL: fmt.Sprintf("/api/media/document-image/%d", img.ID), // Authenticated endpoint
Caption: img.Caption,
})
}

View File

@@ -58,6 +58,7 @@ type TaskUserResponse struct {
type TaskCompletionImageResponse struct {
ID uint `json:"id"`
ImageURL string `json:"image_url"`
MediaURL string `json:"media_url"` // Authenticated endpoint: /api/media/completion-image/{id}
Caption string `json:"caption"`
}
@@ -213,11 +214,12 @@ func NewTaskCompletionResponse(c *models.TaskCompletion) TaskCompletionResponse
if c.CompletedBy.ID != 0 {
resp.CompletedBy = NewTaskUserResponse(&c.CompletedBy)
}
// Convert images
// Convert images with authenticated media URLs
for _, img := range c.Images {
resp.Images = append(resp.Images, TaskCompletionImageResponse{
ID: img.ID,
ImageURL: img.ImageURL,
MediaURL: fmt.Sprintf("/api/media/completion-image/%d", img.ID), // Authenticated endpoint
Caption: img.Caption,
})
}