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:
@@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/honeydue-api/internal/apperrors"
|
||||
@@ -184,8 +186,33 @@ func (s *NotificationService) GetPreferences(userID uint) (*NotificationPreferen
|
||||
return NewNotificationPreferencesResponse(prefs), nil
|
||||
}
|
||||
|
||||
// validateHourField checks that an optional hour value is in the valid range 0-23.
|
||||
func validateHourField(val *int, fieldName string) error {
|
||||
if val != nil && (*val < 0 || *val > 23) {
|
||||
return apperrors.BadRequest("error.invalid_hour").
|
||||
WithMessage(fmt.Sprintf("%s must be between 0 and 23", fieldName))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePreferences updates notification preferences
|
||||
func (s *NotificationService) UpdatePreferences(userID uint, req *UpdatePreferencesRequest) (*NotificationPreferencesResponse, error) {
|
||||
// B-12: Validate hour fields are in range 0-23
|
||||
hourFields := []struct {
|
||||
value *int
|
||||
name string
|
||||
}{
|
||||
{req.TaskDueSoonHour, "task_due_soon_hour"},
|
||||
{req.TaskOverdueHour, "task_overdue_hour"},
|
||||
{req.WarrantyExpiringHour, "warranty_expiring_hour"},
|
||||
{req.DailyDigestHour, "daily_digest_hour"},
|
||||
}
|
||||
for _, hf := range hourFields {
|
||||
if err := validateHourField(hf.value, hf.name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
prefs, err := s.notificationRepo.GetOrCreatePreferences(userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
@@ -256,7 +283,10 @@ func (s *NotificationService) UpdateUserTimezone(userID uint, timezone string) {
|
||||
// Only update if timezone changed (avoid unnecessary DB writes)
|
||||
if prefs.Timezone == nil || *prefs.Timezone != timezone {
|
||||
prefs.Timezone = &timezone
|
||||
_ = s.notificationRepo.UpdatePreferences(prefs)
|
||||
if err := s.notificationRepo.UpdatePreferences(prefs); err != nil {
|
||||
log.Error().Err(err).Uint("user_id", userID).Str("timezone", timezone).
|
||||
Msg("Failed to update user timezone in notification preferences")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,6 +460,7 @@ func (s *NotificationService) UnregisterDevice(registrationID, platform string,
|
||||
|
||||
// === Response/Request Types ===
|
||||
|
||||
// TODO(hardening): Move to internal/dto/responses/notification.go
|
||||
// NotificationResponse represents a notification in API response
|
||||
type NotificationResponse struct {
|
||||
ID uint `json:"id"`
|
||||
@@ -473,6 +504,7 @@ func NewNotificationResponse(n *models.Notification) NotificationResponse {
|
||||
return resp
|
||||
}
|
||||
|
||||
// TODO(hardening): Move to internal/dto/responses/notification.go
|
||||
// NotificationPreferencesResponse represents notification preferences
|
||||
type NotificationPreferencesResponse struct {
|
||||
TaskDueSoon bool `json:"task_due_soon"`
|
||||
@@ -511,6 +543,7 @@ func NewNotificationPreferencesResponse(p *models.NotificationPreference) *Notif
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(hardening): Move to internal/dto/requests/notification.go
|
||||
// UpdatePreferencesRequest represents preferences update request
|
||||
type UpdatePreferencesRequest struct {
|
||||
TaskDueSoon *bool `json:"task_due_soon"`
|
||||
@@ -532,6 +565,7 @@ type UpdatePreferencesRequest struct {
|
||||
DailyDigestHour *int `json:"daily_digest_hour"`
|
||||
}
|
||||
|
||||
// TODO(hardening): Move to internal/dto/responses/notification.go
|
||||
// DeviceResponse represents a device in API response
|
||||
type DeviceResponse struct {
|
||||
ID uint `json:"id"`
|
||||
@@ -569,6 +603,7 @@ func NewGCMDeviceResponse(d *models.GCMDevice) *DeviceResponse {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(hardening): Move to internal/dto/requests/notification.go
|
||||
// RegisterDeviceRequest represents device registration request
|
||||
type RegisterDeviceRequest struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
Reference in New Issue
Block a user