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:
@@ -139,6 +139,12 @@ func (h *Handler) HandleTaskReminder(ctx context.Context, task *asynq.Task) erro
|
||||
|
||||
log.Info().Int("count", len(dueSoonTasks)).Msg("Found tasks due today/tomorrow for eligible users")
|
||||
|
||||
// Build set for O(1) eligibility lookups instead of O(N) linear scan
|
||||
eligibleSet := make(map[uint]bool, len(eligibleUserIDs))
|
||||
for _, id := range eligibleUserIDs {
|
||||
eligibleSet[id] = true
|
||||
}
|
||||
|
||||
// Group tasks by user (assigned_to or residence owner)
|
||||
userTasks := make(map[uint][]models.Task)
|
||||
for _, t := range dueSoonTasks {
|
||||
@@ -150,12 +156,9 @@ func (h *Handler) HandleTaskReminder(ctx context.Context, task *asynq.Task) erro
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
// Only include if user is in eligible list
|
||||
for _, eligibleID := range eligibleUserIDs {
|
||||
if userID == eligibleID {
|
||||
userTasks[userID] = append(userTasks[userID], t)
|
||||
break
|
||||
}
|
||||
// Only include if user is in eligible set (O(1) lookup)
|
||||
if eligibleSet[userID] {
|
||||
userTasks[userID] = append(userTasks[userID], t)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +239,12 @@ func (h *Handler) HandleOverdueReminder(ctx context.Context, task *asynq.Task) e
|
||||
|
||||
log.Info().Int("count", len(overdueTasks)).Msg("Found overdue tasks for eligible users")
|
||||
|
||||
// Build set for O(1) eligibility lookups instead of O(N) linear scan
|
||||
eligibleSet := make(map[uint]bool, len(eligibleUserIDs))
|
||||
for _, id := range eligibleUserIDs {
|
||||
eligibleSet[id] = true
|
||||
}
|
||||
|
||||
// Group tasks by user (assigned_to or residence owner)
|
||||
userTasks := make(map[uint][]models.Task)
|
||||
for _, t := range overdueTasks {
|
||||
@@ -247,12 +256,9 @@ func (h *Handler) HandleOverdueReminder(ctx context.Context, task *asynq.Task) e
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
// Only include if user is in eligible list
|
||||
for _, eligibleID := range eligibleUserIDs {
|
||||
if userID == eligibleID {
|
||||
userTasks[userID] = append(userTasks[userID], t)
|
||||
break
|
||||
}
|
||||
// Only include if user is in eligible set (O(1) lookup)
|
||||
if eligibleSet[userID] {
|
||||
userTasks[userID] = append(userTasks[userID], t)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,10 +690,20 @@ func (h *Handler) HandleSmartReminder(ctx context.Context, task *asynq.Task) err
|
||||
|
||||
log.Info().Int("count", len(activeTasks)).Msg("Found active tasks for eligible users")
|
||||
|
||||
// Step 3: Process each task once, sending appropriate notification based on user prefs
|
||||
var dueSoonSent, dueSoonSkipped, overdueSent, overdueSkipped int
|
||||
// Step 3: Pre-process tasks to determine stages and build batch reminder check
|
||||
type candidateReminder struct {
|
||||
taskIndex int
|
||||
userID uint
|
||||
effectiveDate time.Time
|
||||
stage string
|
||||
isOverdue bool
|
||||
reminderStage models.ReminderStage
|
||||
}
|
||||
|
||||
for _, t := range activeTasks {
|
||||
var candidates []candidateReminder
|
||||
var reminderKeys []repositories.ReminderKey
|
||||
|
||||
for i, t := range activeTasks {
|
||||
// Determine which user to notify
|
||||
var userID uint
|
||||
if t.AssignedToID != nil {
|
||||
@@ -737,15 +753,36 @@ func (h *Handler) HandleSmartReminder(ctx context.Context, task *asynq.Task) err
|
||||
|
||||
reminderStage := models.ReminderStage(stage)
|
||||
|
||||
// Check if already sent
|
||||
alreadySent, err := h.reminderRepo.HasSentReminder(t.ID, userID, effectiveDate, reminderStage)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Uint("task_id", t.ID).Msg("Failed to check reminder log")
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, candidateReminder{
|
||||
taskIndex: i,
|
||||
userID: userID,
|
||||
effectiveDate: effectiveDate,
|
||||
stage: stage,
|
||||
isOverdue: isOverdueStage,
|
||||
reminderStage: reminderStage,
|
||||
})
|
||||
|
||||
if alreadySent {
|
||||
if isOverdueStage {
|
||||
reminderKeys = append(reminderKeys, repositories.ReminderKey{
|
||||
TaskID: t.ID,
|
||||
UserID: userID,
|
||||
DueDate: effectiveDate,
|
||||
Stage: reminderStage,
|
||||
})
|
||||
}
|
||||
|
||||
// Batch check which reminders have already been sent (single query)
|
||||
alreadySentMap, err := h.reminderRepo.HasSentReminderBatch(reminderKeys)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to batch check reminder logs")
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 4: Send notifications for candidates that haven't been sent yet
|
||||
var dueSoonSent, dueSoonSkipped, overdueSent, overdueSkipped int
|
||||
|
||||
for i, c := range candidates {
|
||||
if alreadySentMap[i] {
|
||||
if c.isOverdue {
|
||||
overdueSkipped++
|
||||
} else {
|
||||
dueSoonSkipped++
|
||||
@@ -753,30 +790,32 @@ func (h *Handler) HandleSmartReminder(ctx context.Context, task *asynq.Task) err
|
||||
continue
|
||||
}
|
||||
|
||||
t := activeTasks[c.taskIndex]
|
||||
|
||||
// Determine notification type
|
||||
var notificationType models.NotificationType
|
||||
if isOverdueStage {
|
||||
if c.isOverdue {
|
||||
notificationType = models.NotificationTaskOverdue
|
||||
} else {
|
||||
notificationType = models.NotificationTaskDueSoon
|
||||
}
|
||||
|
||||
// Send notification
|
||||
if err := h.notificationService.CreateAndSendTaskNotification(ctx, userID, notificationType, &t); err != nil {
|
||||
if err := h.notificationService.CreateAndSendTaskNotification(ctx, c.userID, notificationType, &t); err != nil {
|
||||
log.Error().Err(err).
|
||||
Uint("user_id", userID).
|
||||
Uint("user_id", c.userID).
|
||||
Uint("task_id", t.ID).
|
||||
Str("stage", stage).
|
||||
Str("stage", c.stage).
|
||||
Msg("Failed to send smart reminder")
|
||||
continue
|
||||
}
|
||||
|
||||
// Log the reminder
|
||||
if _, err := h.reminderRepo.LogReminder(t.ID, userID, effectiveDate, reminderStage, nil); err != nil {
|
||||
log.Error().Err(err).Uint("task_id", t.ID).Str("stage", stage).Msg("Failed to log reminder")
|
||||
if _, err := h.reminderRepo.LogReminder(t.ID, c.userID, c.effectiveDate, c.reminderStage, nil); err != nil {
|
||||
log.Error().Err(err).Uint("task_id", t.ID).Str("stage", c.stage).Msg("Failed to log reminder")
|
||||
}
|
||||
|
||||
if isOverdueStage {
|
||||
if c.isOverdue {
|
||||
overdueSent++
|
||||
} else {
|
||||
dueSoonSent++
|
||||
|
||||
Reference in New Issue
Block a user