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

@@ -54,10 +54,12 @@ func SetupRouter(deps *Dependencies) *gin.Engine {
// Health check endpoint (no auth required)
r.GET("/api/health/", healthCheck)
// Serve static files from uploads directory
if cfg.Storage.UploadDir != "" {
r.Static("/uploads", cfg.Storage.UploadDir)
}
// NOTE: Public static file serving removed for security.
// All uploaded media is now served through authenticated proxy endpoints:
// - GET /api/media/document/:id
// - GET /api/media/document-image/:id
// - GET /api/media/completion-image/:id
// These endpoints verify the user has access to the residence before serving files.
// Initialize repositories
userRepo := repositories.NewUserRepository(deps.DB)
@@ -104,8 +106,10 @@ func SetupRouter(deps *Dependencies) *gin.Engine {
// Initialize upload handler (if storage service is available)
var uploadHandler *handlers.UploadHandler
var mediaHandler *handlers.MediaHandler
if deps.StorageService != nil {
uploadHandler = handlers.NewUploadHandler(deps.StorageService)
mediaHandler = handlers.NewMediaHandler(documentRepo, taskRepo, residenceRepo, deps.StorageService)
}
// Set up admin routes (separate auth system)
@@ -141,6 +145,11 @@ func SetupRouter(deps *Dependencies) *gin.Engine {
if uploadHandler != nil {
setupUploadRoutes(protected, uploadHandler)
}
// Media routes (authenticated media serving)
if mediaHandler != nil {
setupMediaRoutes(protected, mediaHandler)
}
}
}
@@ -354,3 +363,13 @@ func setupUploadRoutes(api *gin.RouterGroup, uploadHandler *handlers.UploadHandl
uploads.DELETE("/", uploadHandler.DeleteFile)
}
}
// setupMediaRoutes configures authenticated media serving routes
func setupMediaRoutes(api *gin.RouterGroup, mediaHandler *handlers.MediaHandler) {
media := api.Group("/media")
{
media.GET("/document/:id", mediaHandler.ServeDocument)
media.GET("/document-image/:id", mediaHandler.ServeDocumentImage)
media.GET("/completion-image/:id", mediaHandler.ServeCompletionImage)
}
}