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

@@ -40,9 +40,12 @@ func generateTrackingID() string {
// HasSentEmail checks if a specific email type has already been sent to a user
func (s *OnboardingEmailService) HasSentEmail(userID uint, emailType models.OnboardingEmailType) bool {
var count int64
s.db.Model(&models.OnboardingEmail{}).
if err := s.db.Model(&models.OnboardingEmail{}).
Where("user_id = ? AND email_type = ?", userID, emailType).
Count(&count)
Count(&count).Error; err != nil {
log.Error().Err(err).Uint("user_id", userID).Str("email_type", string(emailType)).Msg("Failed to check if email was sent")
return false
}
return count > 0
}
@@ -125,23 +128,31 @@ func (s *OnboardingEmailService) GetEmailStats() (*OnboardingEmailStats, error)
// No residence email stats
var noResTotal, noResOpened int64
s.db.Model(&models.OnboardingEmail{}).
if err := s.db.Model(&models.OnboardingEmail{}).
Where("email_type = ?", models.OnboardingEmailNoResidence).
Count(&noResTotal)
s.db.Model(&models.OnboardingEmail{}).
Count(&noResTotal).Error; err != nil {
log.Error().Err(err).Msg("Failed to count no-residence emails")
}
if err := s.db.Model(&models.OnboardingEmail{}).
Where("email_type = ? AND opened_at IS NOT NULL", models.OnboardingEmailNoResidence).
Count(&noResOpened)
Count(&noResOpened).Error; err != nil {
log.Error().Err(err).Msg("Failed to count opened no-residence emails")
}
stats.NoResidenceTotal = noResTotal
stats.NoResidenceOpened = noResOpened
// No tasks email stats
var noTasksTotal, noTasksOpened int64
s.db.Model(&models.OnboardingEmail{}).
if err := s.db.Model(&models.OnboardingEmail{}).
Where("email_type = ?", models.OnboardingEmailNoTasks).
Count(&noTasksTotal)
s.db.Model(&models.OnboardingEmail{}).
Count(&noTasksTotal).Error; err != nil {
log.Error().Err(err).Msg("Failed to count no-tasks emails")
}
if err := s.db.Model(&models.OnboardingEmail{}).
Where("email_type = ? AND opened_at IS NOT NULL", models.OnboardingEmailNoTasks).
Count(&noTasksOpened)
Count(&noTasksOpened).Error; err != nil {
log.Error().Err(err).Msg("Failed to count opened no-tasks emails")
}
stats.NoTasksTotal = noTasksTotal
stats.NoTasksOpened = noTasksOpened
@@ -351,7 +362,9 @@ func (s *OnboardingEmailService) SendOnboardingEmailToUser(userID uint, emailTyp
// If already sent before, delete the old record first to allow re-recording
// This allows admins to "resend" emails while still tracking them
if alreadySent {
s.db.Where("user_id = ? AND email_type = ?", userID, emailType).Delete(&models.OnboardingEmail{})
if err := s.db.Where("user_id = ? AND email_type = ?", userID, emailType).Delete(&models.OnboardingEmail{}).Error; err != nil {
log.Error().Err(err).Uint("user_id", userID).Str("email_type", string(emailType)).Msg("Failed to delete old onboarding email record before resend")
}
}
// Record that email was sent