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

@@ -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++