package services import ( "github.com/treytartt/honeydue-api/internal/models" "gorm.io/gorm" ) // FileOwnershipService checks whether a user has access to a file referenced // by URL. It queries task completion images, document files, and document // images, resolving access through residence ownership or membership. type FileOwnershipService struct { db *gorm.DB } // NewFileOwnershipService creates a new FileOwnershipService func NewFileOwnershipService(db *gorm.DB) *FileOwnershipService { return &FileOwnershipService{db: db} } // accessibleResidenceIDs returns a subquery of residence IDs the user can // access: residences they own (residence_residence.owner_id) UNION residences // they are a member of (residence_residence_users). // // Audit C7: the previous queries joined residence_residence_users only, so a // residence owner who was not also a member of the join table could not pass // the ownership check for files in their own property. func (s *FileOwnershipService) accessibleResidenceIDs(userID uint) *gorm.DB { return s.db.Raw(` SELECT id FROM residence_residence WHERE owner_id = ? UNION SELECT residence_id FROM residence_residence_users WHERE user_id = ? `, userID, userID) } // IsFileOwnedByUser checks if the given file URL belongs to a record in a // residence the user owns or is a member of. func (s *FileOwnershipService) IsFileOwnedByUser(fileURL string, userID uint) (bool, error) { // Task completion images: image_url -> completion -> task -> residence. var completionImageCount int64 err := s.db.Model(&models.TaskCompletionImage{}). Joins("JOIN task_taskcompletion ON task_taskcompletion.id = task_taskcompletionimage.completion_id"). Joins("JOIN task_task ON task_task.id = task_taskcompletion.task_id"). Where("task_taskcompletionimage.image_url = ?", fileURL). Where("task_task.residence_id IN (?)", s.accessibleResidenceIDs(userID)). Count(&completionImageCount).Error if err != nil { return false, err } if completionImageCount > 0 { return true, nil } // Document files: file_url -> document -> residence. var documentCount int64 err = s.db.Model(&models.Document{}). Where("task_document.file_url = ?", fileURL). Where("task_document.residence_id IN (?)", s.accessibleResidenceIDs(userID)). Count(&documentCount).Error if err != nil { return false, err } if documentCount > 0 { return true, nil } // Document images: image_url -> document_image -> document -> residence. var documentImageCount int64 err = s.db.Model(&models.DocumentImage{}). Joins("JOIN task_document ON task_document.id = task_documentimage.document_id"). Where("task_documentimage.image_url = ?", fileURL). Where("task_document.residence_id IN (?)", s.accessibleResidenceIDs(userID)). Count(&documentImageCount).Error if err != nil { return false, err } if documentImageCount > 0 { return true, nil } return false, nil }