Harden API security: input validation, safe auth extraction, new tests, and deploy config

Comprehensive security hardening from audit findings:
- Add validation tags to all DTO request structs (max lengths, ranges, enums)
- Replace unsafe type assertions with MustGetAuthUser helper across all handlers
- Remove query-param token auth from admin middleware (prevents URL token leakage)
- Add request validation calls in handlers that were missing c.Validate()
- Remove goroutines in handlers (timezone update now synchronous)
- Add sanitize middleware and path traversal protection (path_utils)
- Stop resetting admin passwords on migration restart
- Warn on well-known default SECRET_KEY
- Add ~30 new test files covering security regressions, auth safety, repos, and services
- Add deploy/ config, audit digests, and AUDIT_FINDINGS documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-02 09:48:01 -06:00
parent 56d6fa4514
commit 7690f07a2b
123 changed files with 8321 additions and 750 deletions

View File

@@ -52,7 +52,8 @@ func (r *DocumentRepository) FindByResidence(residenceID uint) ([]models.Documen
return documents, err
}
// FindByUser finds all documents accessible to a user
// FindByUser finds all documents accessible to a user.
// A default limit of 500 is applied to prevent unbounded result sets.
func (r *DocumentRepository) FindByUser(residenceIDs []uint) ([]models.Document, error) {
var documents []models.Document
err := r.db.Preload("CreatedBy").
@@ -60,6 +61,7 @@ func (r *DocumentRepository) FindByUser(residenceIDs []uint) ([]models.Document,
Preload("Images").
Where("residence_id IN ? AND is_active = ?", residenceIDs, true).
Order("created_at DESC").
Limit(500).
Find(&documents).Error
return documents, err
}
@@ -89,7 +91,8 @@ func (r *DocumentRepository) FindByUserFiltered(residenceIDs []uint, filter *Doc
query = query.Where("expiry_date IS NOT NULL AND expiry_date > ? AND expiry_date <= ?", now, threshold)
}
if filter.Search != "" {
searchPattern := "%" + filter.Search + "%"
escaped := escapeLikeWildcards(filter.Search)
searchPattern := "%" + escaped + "%"
query = query.Where("(title ILIKE ? OR description ILIKE ?)", searchPattern, searchPattern)
}
}
@@ -169,6 +172,19 @@ func (r *DocumentRepository) CountByResidence(residenceID uint) (int64, error) {
return count, err
}
// CountByResidenceIDs counts all active documents across multiple residences in a single query.
// Returns the total count of active documents for the given residence IDs.
func (r *DocumentRepository) CountByResidenceIDs(residenceIDs []uint) (int64, error) {
if len(residenceIDs) == 0 {
return 0, nil
}
var count int64
err := r.db.Model(&models.Document{}).
Where("residence_id IN ? AND is_active = ?", residenceIDs, true).
Count(&count).Error
return count, err
}
// FindByIDIncludingInactive finds a document by ID including inactive ones
func (r *DocumentRepository) FindByIDIncludingInactive(id uint, document *models.Document) error {
return r.db.Preload("CreatedBy").Preload("Images").First(document, id).Error