nhance-enrollment/app/Commands/RateLimitBlocksReconcile.php
2026-05-18 12:31:18 +05:30

118 lines
3.5 KiB
PHP

<?php
namespace App\Commands;
use App\Libraries\RateLimiterService;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use Config\Database;
use Config\RateLimiter as RateLimiterConfig;
/**
* Align rate_limit_blocks with timed cache TTL: purge stale cache keys and delete DB rows.
*
* Schedule (example every 10 minutes):
* *\/10 * * * * cd /path/to/project && php spark rate-limit:reconcile-blocks
*/
class RateLimitBlocksReconcile extends BaseCommand
{
protected $group = 'Rate limit';
protected $name = 'rate-limit:reconcile-blocks';
protected $description = 'For active rows whose block TTL has passed: purge cache keys, then delete the DB row.';
protected $usage = 'rate-limit:reconcile-blocks [--dry-run]';
/** @var array<string, string> */
protected $options = [
'--dry-run' => 'Show which rows would be purged without deleting cache or DB.',
];
public function run(array $params)
{
$dryRun = CLI::getOption('dry-run') !== null;
$db = Database::connect();
if (! $db->tableExists('rate_limit_blocks')) {
CLI::write('Table rate_limit_blocks does not exist. Nothing to do.', 'yellow');
return;
}
/** @var RateLimiterConfig $rl */
$rl = config('RateLimiter');
$limiter = new RateLimiterService();
$rows = $db->table('rate_limit_blocks')
->where('status', 'active')
->get()
->getResultArray();
$count = 0;
foreach ($rows as $row) {
$duration = $this->blockSecondsForRow($row, $rl);
if ($duration <= 0) {
continue;
}
$blockedAt = strtotime((string) $row['blocked_at']);
if ($blockedAt === false) {
CLI::write('Skipping id ' . $row['id'] . ': invalid blocked_at.', 'red');
continue;
}
if (time() < $blockedAt + $duration) {
continue;
}
$id = (int) $row['id'];
$cacheId = (string) $row['cache_identifier'];
$blockType = (string) $row['block_type'];
CLI::write(
($dryRun ? '[dry-run] Would reconcile ' : 'Reconciling ')
. "{$blockType} id={$id} level={$row['block_level']} display=" . $row['display_identifier'],
'cyan'
);
if (! $dryRun) {
if ($blockType === 'ip') {
$limiter->purgeIpBlockCaches($cacheId);
} elseif ($blockType === 'user') {
$limiter->purgeUserBlockCaches($cacheId);
}
$db->table('rate_limit_blocks')->delete(['id' => $id], 1);
}
$count++;
}
CLI::write(
$dryRun
? "Dry run complete. {$count} row(s) would be purged and deleted."
: "Done. Reconciled {$count} expired row(s).",
'yellow'
);
}
/**
* @param array<string, mixed> $row
*/
protected function blockSecondsForRow(array $row, RateLimiterConfig $cfg): int
{
$blockCfg = ($row['block_type'] ?? '') === 'ip' ? $cfg->ipBlock : $cfg->userBlock;
$level = (string) ($row['block_level'] ?? '');
return match ($level) {
'soft' => (int) $blockCfg['soft_duration'],
'medium' => (int) $blockCfg['medium_duration'],
'hard' => (int) $blockCfg['hard_duration'],
default => 0,
};
}
}