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

@@ -63,7 +63,7 @@ func (r *ContractorRepository) FindByUser(userID uint, residenceIDs []uint) ([]m
query = query.Where("residence_id IS NULL AND created_by_id = ?", userID)
}
err := query.Order("is_favorite DESC, name ASC").Find(&contractors).Error
err := query.Order("is_favorite DESC, name ASC").Limit(500).Find(&contractors).Error
return contractors, err
}
@@ -85,18 +85,31 @@ func (r *ContractorRepository) Delete(id uint) error {
Update("is_active", false).Error
}
// ToggleFavorite toggles the favorite status of a contractor
// ToggleFavorite toggles the favorite status of a contractor atomically.
// Uses a single UPDATE with NOT to avoid read-then-write race conditions.
// Only toggles active contractors to prevent toggling soft-deleted records.
func (r *ContractorRepository) ToggleFavorite(id uint) (bool, error) {
var contractor models.Contractor
if err := r.db.First(&contractor, id).Error; err != nil {
return false, err
}
newStatus := !contractor.IsFavorite
err := r.db.Model(&models.Contractor{}).
Where("id = ?", id).
Update("is_favorite", newStatus).Error
var newStatus bool
err := r.db.Transaction(func(tx *gorm.DB) error {
// Atomic toggle: SET is_favorite = NOT is_favorite for active contractors only
result := tx.Model(&models.Contractor{}).
Where("id = ? AND is_active = ?", id, true).
Update("is_favorite", gorm.Expr("NOT is_favorite"))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
// Read back the new value within the same transaction
var contractor models.Contractor
if err := tx.Select("is_favorite").First(&contractor, id).Error; err != nil {
return err
}
newStatus = contractor.IsFavorite
return nil
})
return newStatus, err
}
@@ -145,6 +158,19 @@ func (r *ContractorRepository) CountByResidence(residenceID uint) (int64, error)
return count, err
}
// CountByResidenceIDs counts all active contractors across multiple residences in a single query.
// Returns the total count of active contractors for the given residence IDs.
func (r *ContractorRepository) CountByResidenceIDs(residenceIDs []uint) (int64, error) {
if len(residenceIDs) == 0 {
return 0, nil
}
var count int64
err := r.db.Model(&models.Contractor{}).
Where("residence_id IN ? AND is_active = ?", residenceIDs, true).
Count(&count).Error
return count, err
}
// === Specialty Operations ===
// GetAllSpecialties returns all contractor specialties