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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user