Fix 113 hardening issues across entire Go backend
Security: - Replace all binding: tags with validate: + c.Validate() in admin handlers - Add rate limiting to auth endpoints (login, register, password reset) - Add security headers (HSTS, XSS protection, nosniff, frame options) - Wire Google Pub/Sub token verification into webhook handler - Replace ParseUnverified with proper OIDC/JWKS key verification - Verify inner Apple JWS signatures in webhook handler - Add io.LimitReader (1MB) to all webhook body reads - Add ownership verification to file deletion - Move hardcoded admin credentials to env vars - Add uniqueIndex to User.Email - Hide ConfirmationCode from JSON serialization - Mask confirmation codes in admin responses - Use http.DetectContentType for upload validation - Fix path traversal in storage service - Replace os.Getenv with Viper in stripe service - Sanitize Redis URLs before logging - Separate DEBUG_FIXED_CODES from DEBUG flag - Reject weak SECRET_KEY in production - Add host check on /_next/* proxy routes - Use explicit localhost CORS origins in debug mode - Replace err.Error() with generic messages in all admin error responses Critical fixes: - Rewrite FCM to HTTP v1 API with OAuth 2.0 service account auth - Fix user_customuser -> auth_user table names in raw SQL - Fix dashboard verified query to use UserProfile model - Add escapeLikeWildcards() to prevent SQL wildcard injection Bug fixes: - Add bounds checks for days/expiring_soon query params (1-3650) - Add receipt_data/transaction_id empty-check to RestoreSubscription - Change Active bool -> *bool in device handler - Check all unchecked GORM/FindByIDWithProfile errors - Add validation for notification hour fields (0-23) - Add max=10000 validation on task description updates Transactions & data integrity: - Wrap registration flow in transaction - Wrap QuickComplete in transaction - Move image creation inside completion transaction - Wrap SetSpecialties in transaction - Wrap GetOrCreateToken in transaction - Wrap completion+image deletion in transaction Performance: - Batch completion summaries (2 queries vs 2N) - Reuse single http.Client in IAP validation - Cache dashboard counts (30s TTL) - Batch COUNT queries in admin user list - Add Limit(500) to document queries - Add reminder_stage+due_date filters to reminder queries - Parse AllowedTypes once at init - In-memory user cache in auth middleware (30s TTL) - Timezone change detection cache - Optimize P95 with per-endpoint sorted buffers - Replace crypto/md5 with hash/fnv for ETags Code quality: - Add sync.Once to all monitoring Stop()/Close() methods - Replace 8 fmt.Printf with zerolog in auth service - Log previously discarded errors - Standardize delete response shapes - Route hardcoded English through i18n - Remove FileURL from DocumentResponse (keep MediaURL only) - Thread user timezone through kanban board responses - Initialize empty slices to prevent null JSON - Extract shared field map for task Update/UpdateTx - Delete unused SoftDeleteModel, min(), formatCron, legacy handlers Worker & jobs: - Wire Asynq email infrastructure into worker - Register HandleReminderLogCleanup with daily 3AM cron - Use per-user timezone in HandleSmartReminder - Replace direct DB queries with repository calls - Delete legacy reminder handlers (~200 lines) - Delete unused task type constants Dependencies: - Replace archived jung-kurt/gofpdf with go-pdf/fpdf - Replace unmaintained gomail.v2 with wneessen/go-mail - Add TODO for Echo jwt v3 transitive dep removal Test infrastructure: - Fix MakeRequest/SeedLookupData error handling - Replace os.Exit(0) with t.Skip() in scope/consistency tests - Add 11 new FCM v1 tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,7 @@ 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 {
|
||||
// TODO(hardening): Replace with OnboardingEmailRepository.HasSentEmail()
|
||||
var count int64
|
||||
if err := s.db.Model(&models.OnboardingEmail{}).
|
||||
Where("user_id = ? AND email_type = ?", userID, emailType).
|
||||
@@ -51,6 +52,7 @@ func (s *OnboardingEmailService) HasSentEmail(userID uint, emailType models.Onbo
|
||||
|
||||
// RecordEmailSent records that an email was sent to a user
|
||||
func (s *OnboardingEmailService) RecordEmailSent(userID uint, emailType models.OnboardingEmailType, trackingID string) error {
|
||||
// TODO(hardening): Replace with OnboardingEmailRepository.Create()
|
||||
email := &models.OnboardingEmail{
|
||||
UserID: userID,
|
||||
EmailType: emailType,
|
||||
@@ -66,6 +68,7 @@ func (s *OnboardingEmailService) RecordEmailSent(userID uint, emailType models.O
|
||||
|
||||
// RecordEmailOpened records that an email was opened based on tracking ID
|
||||
func (s *OnboardingEmailService) RecordEmailOpened(trackingID string) error {
|
||||
// TODO(hardening): Replace with OnboardingEmailRepository.MarkOpened()
|
||||
now := time.Now().UTC()
|
||||
result := s.db.Model(&models.OnboardingEmail{}).
|
||||
Where("tracking_id = ? AND opened_at IS NULL", trackingID).
|
||||
@@ -84,6 +87,7 @@ func (s *OnboardingEmailService) RecordEmailOpened(trackingID string) error {
|
||||
|
||||
// GetEmailHistory gets all onboarding emails for a specific user
|
||||
func (s *OnboardingEmailService) GetEmailHistory(userID uint) ([]models.OnboardingEmail, error) {
|
||||
// TODO(hardening): Replace with OnboardingEmailRepository.FindByUserID()
|
||||
var emails []models.OnboardingEmail
|
||||
if err := s.db.Where("user_id = ?", userID).Order("sent_at DESC").Find(&emails).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -105,11 +109,13 @@ func (s *OnboardingEmailService) GetAllEmailHistory(page, pageSize int) ([]model
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
// TODO(hardening): Replace with OnboardingEmailRepository.CountAll()
|
||||
// Count total
|
||||
if err := s.db.Model(&models.OnboardingEmail{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// TODO(hardening): Replace with OnboardingEmailRepository.FindAllPaginated()
|
||||
// Get paginated results with user info
|
||||
if err := s.db.Preload("User").
|
||||
Order("sent_at DESC").
|
||||
@@ -126,6 +132,7 @@ func (s *OnboardingEmailService) GetAllEmailHistory(page, pageSize int) ([]model
|
||||
func (s *OnboardingEmailService) GetEmailStats() (*OnboardingEmailStats, error) {
|
||||
stats := &OnboardingEmailStats{}
|
||||
|
||||
// TODO(hardening): Replace with OnboardingEmailRepository.GetStats()
|
||||
// No residence email stats
|
||||
var noResTotal, noResOpened int64
|
||||
if err := s.db.Model(&models.OnboardingEmail{}).
|
||||
@@ -159,6 +166,7 @@ func (s *OnboardingEmailService) GetEmailStats() (*OnboardingEmailStats, error)
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// TODO(hardening): Move to internal/dto/responses/onboarding_email.go
|
||||
// OnboardingEmailStats represents statistics about onboarding emails
|
||||
type OnboardingEmailStats struct {
|
||||
NoResidenceTotal int64 `json:"no_residence_total"`
|
||||
@@ -173,6 +181,7 @@ func (s *OnboardingEmailService) UsersNeedingNoResidenceEmail() ([]models.User,
|
||||
|
||||
twoDaysAgo := time.Now().UTC().AddDate(0, 0, -2)
|
||||
|
||||
// TODO(hardening): Replace with OnboardingEmailRepository.FindUsersWithoutResidence()
|
||||
// Find users who:
|
||||
// 1. Are verified
|
||||
// 2. Registered 2+ days ago
|
||||
@@ -201,6 +210,7 @@ func (s *OnboardingEmailService) UsersNeedingNoTasksEmail() ([]models.User, erro
|
||||
|
||||
fiveDaysAgo := time.Now().UTC().AddDate(0, 0, -5)
|
||||
|
||||
// TODO(hardening): Replace with OnboardingEmailRepository.FindUsersWithoutTasks()
|
||||
// Find users who:
|
||||
// 1. Are verified
|
||||
// 2. Have at least one residence
|
||||
@@ -325,6 +335,7 @@ func (s *OnboardingEmailService) sendNoTasksEmail(user models.User) error {
|
||||
// SendOnboardingEmailToUser manually sends an onboarding email to a specific user
|
||||
// This is used by admin to force-send emails regardless of eligibility criteria
|
||||
func (s *OnboardingEmailService) SendOnboardingEmailToUser(userID uint, emailType models.OnboardingEmailType) error {
|
||||
// TODO(hardening): Replace with UserRepository.FindByID() (inject UserRepository)
|
||||
// Load the user
|
||||
var user models.User
|
||||
if err := s.db.First(&user, userID).Error; err != nil {
|
||||
@@ -362,6 +373,7 @@ 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 {
|
||||
// TODO(hardening): Replace with OnboardingEmailRepository.DeleteByUserAndType()
|
||||
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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user