backend: GDPR export + retention cleanups + worker metrics (BE-1/2/3)
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled
Backend CI / Build (push) Has been cancelled

BE-3 observability: expose the worker's Prometheus metrics on :6060/metrics
(apns/fcm/asynq histograms + a new cache_ops_total counter were recorded all
along but never scraped — which is why those dashboard panels read empty); add
the worker containerPort, the vmagent worker scrape job, and two additive
NetworkPolicies. Instrument cache Get/Set hit/miss.

BE-2 retention: three periodic Asynq cleanup crons mirroring the reminder-log
cleanup — notifications (90d), webhook dedup log (180d), audit_log (365d).

BE-1 GDPR data export: POST /api/auth/export/ enqueues a low-priority Asynq job
that gathers all of the user's data (owned residences + their tasks/contractors/
documents/share-codes, plus profile/notifications/prefs/push-tokens/subscription/
audit log), zips one JSON file per category, and emails it as an attachment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-06-08 22:15:26 -05:00
parent 3b2ea9959a
commit b54493f785
14 changed files with 421 additions and 13 deletions
+69
View File
@@ -0,0 +1,69 @@
package jobs
import (
"context"
"time"
"github.com/hibiken/asynq"
"github.com/rs/zerolog/log"
"github.com/treytartt/honeydue-api/internal/models"
"github.com/treytartt/honeydue-api/internal/repositories"
)
// Data-retention cleanup job types. Registered as periodic crons in
// cmd/worker/main.go. These keep transient/log tables from growing unbounded;
// none touch user-facing data that the app reads back.
const (
TypeNotificationCleanup = "maintenance:notification_cleanup"
TypeWebhookLogCleanup = "maintenance:webhook_log_cleanup"
TypeAuditLogCleanup = "maintenance:audit_log_cleanup"
)
// Retention windows (days).
const (
notificationRetentionDays = 90
webhookLogRetentionDays = 180
auditLogRetentionDays = 365 // keep 1 year of security events
)
// HandleNotificationCleanup deletes notification rows older than the retention
// window. Notifications are delivery records (push/digest history); 90 days is
// ample for any in-app history a client might show.
func (h *Handler) HandleNotificationCleanup(ctx context.Context, _ *asynq.Task) error {
cutoff := time.Now().UTC().AddDate(0, 0, -notificationRetentionDays)
res := h.db.WithContext(ctx).Where("created_at < ?", cutoff).Delete(&models.Notification{})
if res.Error != nil {
log.Error().Err(res.Error).Msg("notification cleanup failed")
return res.Error
}
log.Info().Int64("deleted", res.RowsAffected).Int("retention_days", notificationRetentionDays).Msg("notification cleanup completed")
return nil
}
// HandleWebhookLogCleanup prunes the webhook dedup log. Rows only matter for the
// window in which a provider (Apple/Google) might redeliver an event; 180 days
// is a generous safety margin past any real redelivery.
func (h *Handler) HandleWebhookLogCleanup(ctx context.Context, _ *asynq.Task) error {
cutoff := time.Now().UTC().AddDate(0, 0, -webhookLogRetentionDays)
res := h.db.WithContext(ctx).Where("processed_at < ?", cutoff).Delete(&repositories.WebhookEvent{})
if res.Error != nil {
log.Error().Err(res.Error).Msg("webhook log cleanup failed")
return res.Error
}
log.Info().Int64("deleted", res.RowsAffected).Int("retention_days", webhookLogRetentionDays).Msg("webhook log cleanup completed")
return nil
}
// HandleAuditLogCleanup prunes audit events older than the retention window.
// One year of security events is retained for compliance/forensics.
func (h *Handler) HandleAuditLogCleanup(ctx context.Context, _ *asynq.Task) error {
cutoff := time.Now().UTC().AddDate(0, 0, -auditLogRetentionDays)
res := h.db.WithContext(ctx).Where("created_at < ?", cutoff).Delete(&models.AuditLog{})
if res.Error != nil {
log.Error().Err(res.Error).Msg("audit log cleanup failed")
return res.Error
}
log.Info().Int64("deleted", res.RowsAffected).Int("retention_days", auditLogRetentionDays).Msg("audit log cleanup completed")
return nil
}