package user import ( "context" "time" "github.com/rs/zerolog/log" ) // MagicLinkCleanupRunner periodically deletes expired magic-link tokens // (ADR-0028 Phase A consequence — the rows accumulate without cleanup // otherwise, and stale rows are pure overhead since the token plaintext // is never stored). type MagicLinkCleanupRunner struct { repo MagicLinkRepository } // NewMagicLinkCleanupRunner creates a new cleanup runner. func NewMagicLinkCleanupRunner(repo MagicLinkRepository) *MagicLinkCleanupRunner { return &MagicLinkCleanupRunner{repo: repo} } // StartCleanupLoop runs the cleanup pass every `interval`. Stops when ctx // is cancelled. interval <= 0 disables the loop. func (r *MagicLinkCleanupRunner) StartCleanupLoop(ctx context.Context, interval time.Duration) { if interval <= 0 { return } go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: _, _ = r.runOnce(ctx) } } }() } // runOnce performs a single cleanup pass. Returns the count of deleted rows. // Exposed for testing — tests drive runOnce directly instead of waiting on // the ticker. func (r *MagicLinkCleanupRunner) runOnce(ctx context.Context) (int64, error) { n, err := r.repo.DeleteExpiredMagicLinkTokens(ctx, time.Now()) if err != nil { log.Error().Ctx(ctx).Err(err).Msg("magic-link cleanup: delete failed") return 0, err } if n > 0 { log.Trace().Ctx(ctx).Int64("deleted", n).Msg("magic-link cleanup: removed expired tokens") } return n, nil }