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:
@@ -37,6 +37,84 @@ func (r *ReminderRepository) HasSentReminder(taskID, userID uint, dueDate time.T
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// ReminderKey uniquely identifies a reminder that may have been sent.
|
||||
type ReminderKey struct {
|
||||
TaskID uint
|
||||
UserID uint
|
||||
DueDate time.Time
|
||||
Stage models.ReminderStage
|
||||
}
|
||||
|
||||
// HasSentReminderBatch checks which reminders from the given list have already been sent.
|
||||
// Returns a set of indices into the input slice that have already been sent.
|
||||
// This replaces N individual HasSentReminder calls with a single query.
|
||||
func (r *ReminderRepository) HasSentReminderBatch(keys []ReminderKey) (map[int]bool, error) {
|
||||
result := make(map[int]bool)
|
||||
if len(keys) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Build a lookup from (task_id, user_id, due_date, stage) -> index
|
||||
type normalizedKey struct {
|
||||
TaskID uint
|
||||
UserID uint
|
||||
DueDate string
|
||||
Stage models.ReminderStage
|
||||
}
|
||||
keyToIdx := make(map[normalizedKey][]int, len(keys))
|
||||
|
||||
// Collect unique task IDs and user IDs for the WHERE clause
|
||||
taskIDSet := make(map[uint]bool)
|
||||
userIDSet := make(map[uint]bool)
|
||||
for i, k := range keys {
|
||||
taskIDSet[k.TaskID] = true
|
||||
userIDSet[k.UserID] = true
|
||||
dueDateOnly := time.Date(k.DueDate.Year(), k.DueDate.Month(), k.DueDate.Day(), 0, 0, 0, 0, time.UTC)
|
||||
nk := normalizedKey{
|
||||
TaskID: k.TaskID,
|
||||
UserID: k.UserID,
|
||||
DueDate: dueDateOnly.Format("2006-01-02"),
|
||||
Stage: k.Stage,
|
||||
}
|
||||
keyToIdx[nk] = append(keyToIdx[nk], i)
|
||||
}
|
||||
|
||||
taskIDs := make([]uint, 0, len(taskIDSet))
|
||||
for id := range taskIDSet {
|
||||
taskIDs = append(taskIDs, id)
|
||||
}
|
||||
userIDs := make([]uint, 0, len(userIDSet))
|
||||
for id := range userIDSet {
|
||||
userIDs = append(userIDs, id)
|
||||
}
|
||||
|
||||
// Query all matching reminder logs in one query
|
||||
var logs []models.TaskReminderLog
|
||||
err := r.db.Where("task_id IN ? AND user_id IN ?", taskIDs, userIDs).
|
||||
Find(&logs).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Match returned logs against our key set
|
||||
for _, l := range logs {
|
||||
dueDateStr := l.DueDate.Format("2006-01-02")
|
||||
nk := normalizedKey{
|
||||
TaskID: l.TaskID,
|
||||
UserID: l.UserID,
|
||||
DueDate: dueDateStr,
|
||||
Stage: l.ReminderStage,
|
||||
}
|
||||
if indices, ok := keyToIdx[nk]; ok {
|
||||
for _, idx := range indices {
|
||||
result[idx] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// LogReminder records that a reminder was sent.
|
||||
// Returns the created log entry or an error if the reminder was already sent
|
||||
// (unique constraint violation).
|
||||
|
||||
Reference in New Issue
Block a user