Merge branch 'dev' of bitbucket.org:jubilian/nhance-enrollment into dev
This commit is contained in:
commit
2d64ae4779
117
app/Commands/RateLimitBlocksReconcile.php
Normal file
117
app/Commands/RateLimitBlocksReconcile.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -63,6 +63,12 @@ class Acl
|
||||
'teams' => []
|
||||
],
|
||||
|
||||
// ===================== RATE LIMIT ADMIN =====================
|
||||
'#^/security/rate-limits#' => [
|
||||
'roles' => [ADMIN_ROLE_ID],
|
||||
'teams' => []
|
||||
],
|
||||
|
||||
// ===================== INTERNAL TEST =====================
|
||||
'#^/test#' => [
|
||||
'roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID],
|
||||
|
||||
@ -552,6 +552,7 @@ $routes->post("calculatePremium", "EmployeeRestController::calculatePremium");
|
||||
$routes->get("getBackToEnrolledDetails", "EmployeeRestController::getBackToEnrolledDetails");
|
||||
|
||||
//crone job
|
||||
$routes->cli('cli/reset-token-timeout', 'RestAuthenticationController::resetTokenTimeOut');
|
||||
$routes->cli('cli/enrollOpendAndClose', 'DashboardController::updatePolicyEnrollmentStatus');
|
||||
$routes->cli("cli/sendCroneRemainderMail", "DashboardController::sendCroneRemainderMail");
|
||||
$routes->cli('cli/update-emp-policy-status', 'ClientController::updateEmpAndPolicyStatus');
|
||||
@ -574,7 +575,11 @@ $routes->group('test',function($routes){
|
||||
});
|
||||
|
||||
|
||||
|
||||
$routes->group('security/rate-limits', ['filter' => 'authMVC'], function ($routes) {
|
||||
$routes->get('/', 'RateLimitAdminController::index');
|
||||
$routes->post('unblock-ip', 'RateLimitAdminController::unblockIp');
|
||||
$routes->post('unblock-user', 'RateLimitAdminController::unblockUser');
|
||||
});
|
||||
|
||||
//saml - routes
|
||||
// $routes->group("employeeRest", ['filter' => ['appSignature','AuthApiRateLimitFilter'] ], function ($routes) {
|
||||
|
||||
65
app/Controllers/RateLimitAdminController.php
Normal file
65
app/Controllers/RateLimitAdminController.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\RateLimiterService;
|
||||
|
||||
class RateLimitAdminController extends BaseController
|
||||
{
|
||||
protected RateLimiterService $limiter;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->limiter = new RateLimiterService();
|
||||
}
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$tab = $this->request->getGet('tab');
|
||||
if (! in_array($tab, ['ip', 'user'], true)) {
|
||||
$tab = 'ip';
|
||||
}
|
||||
|
||||
$data = [
|
||||
'title' => 'Rate Limit Blocks',
|
||||
'activeTab' => $tab,
|
||||
'blockedIps' => $this->limiter->listBlockedIps(),
|
||||
'blockedUsers' => $this->limiter->listBlockedUsers(),
|
||||
];
|
||||
|
||||
$this->loadLayout('admin/rate_limit_blocks', $data);
|
||||
}
|
||||
|
||||
public function unblockIp(): \CodeIgniter\HTTP\RedirectResponse
|
||||
{
|
||||
$fingerprint = trim((string) $this->request->getPost('cache_identifier'));
|
||||
$reason = trim((string) $this->request->getPost('reason'));
|
||||
$actorId = (int) (session()->get('userid') ?? 0);
|
||||
|
||||
if ($fingerprint === '') {
|
||||
return redirect()->back()->with('error', 'Missing IP block identifier.');
|
||||
}
|
||||
|
||||
$this->limiter->unblockIpByAdmin($fingerprint, $reason !== '' ? $reason : null, $actorId ?: null);
|
||||
|
||||
return redirect()->to(base_url('security/rate-limits?tab=ip'))
|
||||
->with('success', 'Blocked IP entry unblocked successfully.');
|
||||
}
|
||||
|
||||
public function unblockUser(): \CodeIgniter\HTTP\RedirectResponse
|
||||
{
|
||||
$identity = trim((string) $this->request->getPost('display_identifier'));
|
||||
$reason = trim((string) $this->request->getPost('reason'));
|
||||
$actorId = (int) (session()->get('userid') ?? 0);
|
||||
|
||||
if ($identity === '') {
|
||||
return redirect()->back()->with('error', 'Missing user identity.');
|
||||
}
|
||||
|
||||
$this->limiter->unblockUserByAdmin($identity, $reason !== '' ? $reason : null, $actorId ?: null);
|
||||
|
||||
return redirect()->to(base_url('security/rate-limits?tab=user'))
|
||||
->with('success', 'Blocked user entry unblocked successfully.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +32,10 @@ class RestAuthenticationController extends AdminController
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
/** Default grace period after token_time_out epoch before cron clears it (seconds). */
|
||||
private const RESET_TOKEN_TIMEOUT_BUFFER_SECONDS_DEFAULT = 180;
|
||||
|
||||
|
||||
protected $myLogger;
|
||||
protected $employeeModel;
|
||||
protected $authHistoryModel;
|
||||
@ -2063,5 +2067,72 @@ class RestAuthenticationController extends AdminController
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cron: clear expired token_time_out on active employees (epoch expiry + buffer).
|
||||
*
|
||||
* Buffer default: 3 minutes. Override in .env: TOKEN_TIMEOUT_RESET_BUFFER_SECONDS
|
||||
*
|
||||
* php public/index.php cli/reset-token-timeout
|
||||
*/
|
||||
public function resetTokenTimeOut()
|
||||
{
|
||||
if (! is_cli()) {
|
||||
return $this->respond([
|
||||
'status' => false,
|
||||
'message' => 'This endpoint is CLI only.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$buffer = (int) (getenv('TOKEN_TIMEOUT_RESET_BUFFER_SECONDS')
|
||||
?: self::RESET_TOKEN_TIMEOUT_BUFFER_SECONDS_DEFAULT);
|
||||
if ($buffer < 0) {
|
||||
$buffer = self::RESET_TOKEN_TIMEOUT_BUFFER_SECONDS_DEFAULT;
|
||||
}
|
||||
|
||||
$cutoffEpoch = time() - $buffer;
|
||||
$resetTokenTimeoutCronRan = date('Y-m-d H:i:s');
|
||||
|
||||
$rows = $this->employeeModel
|
||||
->select('id')
|
||||
->where('token_time_out IS NOT NULL', null, false)
|
||||
->where('is_active', 1)
|
||||
->where('emp_status', 'active')
|
||||
->where('token_time_out <=', $cutoffEpoch)
|
||||
->findAll();
|
||||
|
||||
$employeeIds = array_map(static fn (array $row): int => (int) $row['id'], $rows);
|
||||
|
||||
if ($employeeIds !== []) {
|
||||
$this->employeeModel
|
||||
->whereIn('id', $employeeIds)
|
||||
->set(['token_time_out' => null])
|
||||
->update();
|
||||
}
|
||||
|
||||
$logPayload = $employeeIds !== []
|
||||
? json_encode([
|
||||
'employee_ids' => $employeeIds,
|
||||
'reset_token_timeout_cron_ran_at' => $resetTokenTimeoutCronRan,
|
||||
])
|
||||
: json_encode(['reset_token_timeout_cron_ran_at' => $resetTokenTimeoutCronRan]);
|
||||
|
||||
$this->myLogger->logme('error', 'reset-token-timeout cron: ' . $logPayload);
|
||||
|
||||
$result = [
|
||||
'status' => true,
|
||||
'message' => 'Token timeout reset completed.',
|
||||
'buffer_seconds' => $buffer,
|
||||
'cutoff_epoch' => $cutoffEpoch,
|
||||
'reset_token_timeout_cron_ran_at' => $resetTokenTimeoutCronRan,
|
||||
'updated' => [
|
||||
'employees' => count($employeeIds),
|
||||
'employee_ids' => $employeeIds,
|
||||
],
|
||||
];
|
||||
|
||||
echo json_encode($result, JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ class AuthApiRateLimitFilter implements FilterInterface
|
||||
{
|
||||
protected RateLimiterService $limiter;
|
||||
protected Cors $corsFilter;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->limiter = new RateLimiterService();
|
||||
@ -40,22 +41,21 @@ class AuthApiRateLimitFilter implements FilterInterface
|
||||
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
// $this->limiter->unblockUser('9698262411');die;
|
||||
$fingerprint = generateFingerprint(exclude_ua: true);
|
||||
// echo $fingerprint;die;
|
||||
|
||||
|
||||
// 1. IP-level check
|
||||
$ipResult = $this->limiter->checkIp($fingerprint, 'authApi');
|
||||
if ($ipResult) {
|
||||
return $this->jsonResponse($ipResult);
|
||||
return $this->jsonResponse($request, $ipResult);
|
||||
}
|
||||
|
||||
// 2. User-level block check (identity may not be present yet on first hit)
|
||||
$identity = resolveIdentity($request);
|
||||
// echo $identity;die;
|
||||
$identity = $this->resolveIdentity($request);
|
||||
if ($identity) {
|
||||
$userResult = $this->limiter->checkUser($identity);
|
||||
if ($userResult) {
|
||||
return $this->jsonResponse($userResult);
|
||||
return $this->jsonResponse($request, $userResult);
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,34 +74,16 @@ class AuthApiRateLimitFilter implements FilterInterface
|
||||
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||
{
|
||||
|
||||
// Routes where rate-limit failure should be recorded
|
||||
// $allowedRoutes = [
|
||||
// 'employeeRest/verifyEmployeeNumber',
|
||||
// 'employeeRest/getVerifiedUserData',
|
||||
// 'employeeRest/verifyEmployeeEmailId',
|
||||
// 'employeeRest/verifyHrWithMobileNumber',
|
||||
// 'employeeRest/verifyHrWithEmail',
|
||||
// 'employeeRest/verifyHrWithEmail',
|
||||
// 'employeeRest/getVerifiedHrData',
|
||||
// ];
|
||||
|
||||
// $currentPath = service('request')->getPath();
|
||||
// if (!in_array($currentPath, $allowedRoutes)) {
|
||||
// return; // Don't record failures for unrelated routes
|
||||
// }
|
||||
|
||||
// Only act on failed responses (4xx from auth failures)
|
||||
$statusCode = $response->getStatusCode();
|
||||
// print_r($response);die();
|
||||
if ($statusCode < 400 || $statusCode === 429 || $statusCode === 403 || $statusCode === 451) {
|
||||
return; // 2xx/3xx = success; 429/403/451 already handled
|
||||
}
|
||||
|
||||
$fingerprint = $request->getVar('rateLimitFingerprint') ?? generateFingerprint(exclude_ua: true);
|
||||
$fingerprint = $request->getGlobal('rateLimitFingerprint') ?? generateFingerprint(exclude_ua: true);
|
||||
|
||||
$identity = $request->getVar('rateLimitIdentity')
|
||||
?? resolveIdentity($request);
|
||||
$identity = $request->getGlobal('rateLimitIdentity')
|
||||
?? $this->resolveIdentity($request);
|
||||
|
||||
// Record failure at IP level
|
||||
$this->limiter->recordIpFailure($fingerprint);
|
||||
@ -112,10 +94,41 @@ class AuthApiRateLimitFilter implements FilterInterface
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// HELPERS
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Extract identity from POST body or GET params.
|
||||
* Looks for 'email' or 'mobile_number'.
|
||||
*/
|
||||
protected function resolveIdentity(RequestInterface $request): ?string
|
||||
{
|
||||
// Try POST body first
|
||||
$email = $request->getPost('email');
|
||||
$mobile = $request->getPost('mobile_number');
|
||||
|
||||
// Fallback to GET params
|
||||
if (! $email && ! $mobile) {
|
||||
$email = $request->getGet('email');
|
||||
$mobile = $request->getGet('mobile_number');
|
||||
}
|
||||
|
||||
if ($email) {
|
||||
return strtolower(trim($email));
|
||||
}
|
||||
|
||||
if ($mobile) {
|
||||
return trim($mobile);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and return a JSON response for blocked/throttled requests.
|
||||
*/
|
||||
protected function jsonResponse(array $result): ResponseInterface
|
||||
protected function jsonResponse(RequestInterface $request, array $result): ResponseInterface
|
||||
{
|
||||
$response = service('response');
|
||||
$response->setStatusCode($result['status']);
|
||||
|
||||
134
app/Filters/AuthApiRateLimitFilter_OLD.php
Normal file
134
app/Filters/AuthApiRateLimitFilter_OLD.php
Normal file
@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filters;
|
||||
|
||||
use App\Libraries\RateLimiterService;
|
||||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
use App\Filters\Cors;
|
||||
/**
|
||||
* AuthApiFilter
|
||||
*
|
||||
* Applied to API routes that do NOT use JWT — e.g. verifyMobileNumber, verifyOTP.
|
||||
* Identity is extracted from request params: 'email' or 'mobile_number'.
|
||||
*
|
||||
* Performs:
|
||||
* - IP-level throttle + progressive block check (via fingerprint)
|
||||
* - User-level block check (if identity present in params)
|
||||
*
|
||||
* Usage in Routes.php:
|
||||
* $routes->post('api/auth/verify-otp', 'AuthController::verifyOtp', ['filter' => 'authApiRateLimit']);
|
||||
*
|
||||
* Register in app/Config/Filters.php:
|
||||
* 'AuthApiRateLimitFilter' => \App\Filters\AuthApiRateLimitFilter::class
|
||||
*/
|
||||
class AuthApiRateLimitFilter implements FilterInterface
|
||||
{
|
||||
protected RateLimiterService $limiter;
|
||||
protected Cors $corsFilter;
|
||||
public function __construct()
|
||||
{
|
||||
$this->limiter = new RateLimiterService();
|
||||
$this->corsFilter = new Cors();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// BEFORE — runs before the controller
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
// $this->limiter->unblockUser('9698262411');die;
|
||||
$fingerprint = generateFingerprint(exclude_ua: true);
|
||||
// echo $fingerprint;die;
|
||||
// 1. IP-level check
|
||||
$ipResult = $this->limiter->checkIp($fingerprint, 'authApi');
|
||||
if ($ipResult) {
|
||||
return $this->jsonResponse($ipResult);
|
||||
}
|
||||
|
||||
// 2. User-level block check (identity may not be present yet on first hit)
|
||||
$identity = resolveIdentity($request);
|
||||
// echo $identity;die;
|
||||
if ($identity) {
|
||||
$userResult = $this->limiter->checkUser($identity);
|
||||
if ($userResult) {
|
||||
return $this->jsonResponse($userResult);
|
||||
}
|
||||
}
|
||||
|
||||
// Store resolved identity in request for use in after()
|
||||
if ($identity) {
|
||||
$request->setGlobal('rateLimitIdentity', $identity);
|
||||
}
|
||||
$request->setGlobal('rateLimitFingerprint', $fingerprint);
|
||||
|
||||
return null; // pass through
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// AFTER — runs after the controller; records failures on bad responses
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||
{
|
||||
|
||||
// Routes where rate-limit failure should be recorded
|
||||
// $allowedRoutes = [
|
||||
// 'employeeRest/verifyEmployeeNumber',
|
||||
// 'employeeRest/getVerifiedUserData',
|
||||
// 'employeeRest/verifyEmployeeEmailId',
|
||||
// 'employeeRest/verifyHrWithMobileNumber',
|
||||
// 'employeeRest/verifyHrWithEmail',
|
||||
// 'employeeRest/verifyHrWithEmail',
|
||||
// 'employeeRest/getVerifiedHrData',
|
||||
// ];
|
||||
|
||||
// $currentPath = service('request')->getPath();
|
||||
// if (!in_array($currentPath, $allowedRoutes)) {
|
||||
// return; // Don't record failures for unrelated routes
|
||||
// }
|
||||
|
||||
// Only act on failed responses (4xx from auth failures)
|
||||
$statusCode = $response->getStatusCode();
|
||||
// print_r($response);die();
|
||||
if ($statusCode < 400 || $statusCode === 429 || $statusCode === 403 || $statusCode === 451) {
|
||||
return; // 2xx/3xx = success; 429/403/451 already handled
|
||||
}
|
||||
|
||||
$fingerprint = $request->getVar('rateLimitFingerprint') ?? generateFingerprint(exclude_ua: true);
|
||||
|
||||
$identity = $request->getVar('rateLimitIdentity')
|
||||
?? resolveIdentity($request);
|
||||
|
||||
// Record failure at IP level
|
||||
$this->limiter->recordIpFailure($fingerprint);
|
||||
|
||||
// Record failure at user level
|
||||
if ($identity) {
|
||||
$this->limiter->recordUserFailure($identity, 'authApi');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and return a JSON response for blocked/throttled requests.
|
||||
*/
|
||||
protected function jsonResponse(array $result): ResponseInterface
|
||||
{
|
||||
$response = service('response');
|
||||
$response->setStatusCode($result['status']);
|
||||
$response->setContentType('application/json');
|
||||
$response->setBody(json_encode([
|
||||
'success' => false,
|
||||
'error' => [
|
||||
'code' => strtoupper('RATE_LIMIT_' . $result['level']),
|
||||
'message' => $result['message'],
|
||||
'type' => $result['type'] ?? 'request',
|
||||
],
|
||||
]));
|
||||
$this->corsFilter->after($request, $response);
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ namespace App\Libraries;
|
||||
|
||||
use Config\RateLimiter as RateLimiterConfig;
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
|
||||
/**
|
||||
* RateLimiterService
|
||||
@ -21,11 +22,14 @@ class RateLimiterService
|
||||
{
|
||||
protected RateLimiterConfig $config;
|
||||
protected CacheInterface $cache;
|
||||
protected BaseConnection $db;
|
||||
protected string $blockTable = 'rate_limit_blocks';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = config('RateLimiter');
|
||||
$this->cache = \Config\Services::cache();
|
||||
$this->db = \Config\Database::connect();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@ -38,10 +42,8 @@ class RateLimiterService
|
||||
*/
|
||||
public function checkIp(string $fingerprint, string $routeType = 'jwtApi'): ?array
|
||||
{
|
||||
// echo $fingerprint;die;
|
||||
// 1. Is the IP already blocked?
|
||||
$blockInfo = $this->getIpBlock($fingerprint);
|
||||
// print_rr($blockInfo);die();
|
||||
if ($blockInfo) {
|
||||
// Count hit while blocked → maybe escalate
|
||||
$this->recordIpBlockHit($fingerprint, $blockInfo['level']);
|
||||
@ -119,18 +121,28 @@ class RateLimiterService
|
||||
// Duration 0 = store for 10 years (permanent until manual unblock)
|
||||
$ttl = $duration > 0 ? $duration : (10 * 365 * 24 * 3600);
|
||||
$this->cache->save($blockKey, $data, $ttl);
|
||||
$this->upsertBlockRecord('ip', $fingerprint, (string) ($data['ip'] ?? $fingerprint), $level, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually unblock an IP. Clears block, violations, and counters.
|
||||
*/
|
||||
public function unblockIp(string $fingerprint): void
|
||||
{
|
||||
$this->purgeIpBlockCaches($fingerprint);
|
||||
$this->markBlockAsUnblocked('ip', $fingerprint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove IP rate-limit cache entries (block, hits, violations, counter) without touching DB.
|
||||
*/
|
||||
public function purgeIpBlockCaches(string $fingerprint): void
|
||||
{
|
||||
$keys = $this->config->cacheKeys;
|
||||
$this->cache->delete($keys['ip_block'] . $fingerprint);
|
||||
$this->cache->delete($keys['ip_violations'] . $fingerprint);
|
||||
$this->cache->delete($keys['ip_count'] . $fingerprint);
|
||||
$this->cache->delete($keys['ip_block_hits'] . $fingerprint);
|
||||
$this->cache->delete($keys['ip_block'] . $fingerprint);
|
||||
$this->cache->delete($keys['ip_violations'] . $fingerprint);
|
||||
$this->cache->delete($keys['ip_count'] . $fingerprint);
|
||||
$this->cache->delete($keys['ip_block_hits'] . $fingerprint);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -195,7 +207,8 @@ class RateLimiterService
|
||||
public function blockUser(string $identity, string $level = 'soft'): void
|
||||
{
|
||||
$cfg = $this->config->userBlock;
|
||||
$blockKey = $this->config->cacheKeys['user_block'] . $this->hashIdentity($identity);
|
||||
$hashed = $this->hashIdentity($identity);
|
||||
$blockKey = $this->config->cacheKeys['user_block'] . $hashed;
|
||||
|
||||
$duration = $this->blockDuration($cfg, $level);
|
||||
$ttl = $duration > 0 ? $duration : (10 * 365 * 24 * 3600);
|
||||
@ -207,6 +220,7 @@ class RateLimiterService
|
||||
];
|
||||
|
||||
$this->cache->save($blockKey, $data, $ttl);
|
||||
$this->upsertBlockRecord('user', $hashed, $identity, $level, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -214,13 +228,78 @@ class RateLimiterService
|
||||
*/
|
||||
public function unblockUser(string $identity): void
|
||||
{
|
||||
$keys = $this->config->cacheKeys;
|
||||
$hashed = $this->hashIdentity($identity);
|
||||
$this->purgeUserBlockCaches($this->hashIdentity($identity));
|
||||
$this->markBlockAsUnblocked('user', $this->hashIdentity($identity));
|
||||
}
|
||||
|
||||
$this->cache->delete($keys['user_block'] . $hashed);
|
||||
$this->cache->delete($keys['user_violations'] . $hashed);
|
||||
$this->cache->delete($keys['user_count'] . $hashed);
|
||||
$this->cache->delete($keys['user_block_hits'] . $hashed);
|
||||
/**
|
||||
* Remove user rate-limit cache entries for a hashed identity without touching DB.
|
||||
*/
|
||||
public function purgeUserBlockCaches(string $hashedIdentity): void
|
||||
{
|
||||
$keys = $this->config->cacheKeys;
|
||||
$this->cache->delete($keys['user_block'] . $hashedIdentity);
|
||||
$this->cache->delete($keys['user_violations'] . $hashedIdentity);
|
||||
$this->cache->delete($keys['user_count'] . $hashedIdentity);
|
||||
$this->cache->delete($keys['user_block_hits'] . $hashedIdentity);
|
||||
}
|
||||
|
||||
/**
|
||||
* List active blocked IP records for admin views.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function listBlockedIps(int $limit = 200): array
|
||||
{
|
||||
if (! $this->hasBlockTable()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->db->table($this->blockTable)
|
||||
->where('block_type', 'ip')
|
||||
->where('status', 'active')
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* List active blocked user records for admin views.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function listBlockedUsers(int $limit = 200): array
|
||||
{
|
||||
if (! $this->hasBlockTable()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->db->table($this->blockTable)
|
||||
->where('block_type', 'user')
|
||||
->where('status', 'active')
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin helper for unblocking via UI.
|
||||
*/
|
||||
public function unblockIpByAdmin(string $fingerprint, ?string $reason = null, ?int $actorId = null): void
|
||||
{
|
||||
$this->unblockIp($fingerprint);
|
||||
$this->markBlockAsUnblocked('ip', $fingerprint, $reason, $actorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin helper for unblocking via UI.
|
||||
*/
|
||||
public function unblockUserByAdmin(string $identity, ?string $reason = null, ?int $actorId = null): void
|
||||
{
|
||||
$this->unblockUser($identity);
|
||||
$this->markBlockAsUnblocked('user', $this->hashIdentity($identity), $reason, $actorId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -380,7 +459,93 @@ class RateLimiterService
|
||||
*/
|
||||
protected function hashIdentity(string $identity): string
|
||||
{
|
||||
// return strtolower(trim($identity));
|
||||
return hash('sha256', strtolower(trim($identity)));
|
||||
}
|
||||
|
||||
protected function hasBlockTable(): bool
|
||||
{
|
||||
try {
|
||||
return $this->db->tableExists($this->blockTable);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', '[RateLimiter] Failed checking block table: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep an admin-readable block index in DB without affecting runtime decisions.
|
||||
*/
|
||||
protected function upsertBlockRecord(
|
||||
string $blockType,
|
||||
string $cacheIdentifier,
|
||||
string $displayIdentifier,
|
||||
string $level,
|
||||
array $meta = []
|
||||
): void {
|
||||
if (! $this->hasBlockTable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$builder = $this->db->table($this->blockTable);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$existing = $builder
|
||||
->select('id')
|
||||
->where('block_type', $blockType)
|
||||
->where('cache_identifier', $cacheIdentifier)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
$payload = [
|
||||
'display_identifier' => $displayIdentifier,
|
||||
'block_level' => $level,
|
||||
'status' => 'active',
|
||||
'blocked_at' => $now,
|
||||
'unblocked_at' => null,
|
||||
'unblocked_by' => null,
|
||||
'unblock_reason' => null,
|
||||
'meta_json' => ! empty($meta) ? json_encode($meta) : null,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if ($existing) {
|
||||
$builder->where('id', $existing['id'])->update($payload);
|
||||
return;
|
||||
}
|
||||
|
||||
$payload['block_type'] = $blockType;
|
||||
$payload['cache_identifier'] = $cacheIdentifier;
|
||||
$payload['created_at'] = $now;
|
||||
$builder->insert($payload);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', '[RateLimiter] Failed upserting block record: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected function markBlockAsUnblocked(
|
||||
string $blockType,
|
||||
string $cacheIdentifier,
|
||||
?string $reason = null,
|
||||
?int $actorId = null
|
||||
): void {
|
||||
if (! $this->hasBlockTable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$this->db->table($this->blockTable)
|
||||
->where('block_type', $blockType)
|
||||
->where('cache_identifier', $cacheIdentifier)
|
||||
->update([
|
||||
'status' => 'unblocked',
|
||||
'unblocked_at' => $now,
|
||||
'unblocked_by' => $actorId,
|
||||
'unblock_reason' => $reason,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', '[RateLimiter] Failed marking unblock state: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
386
app/Libraries/RateLimiterService_OLD.php
Normal file
386
app/Libraries/RateLimiterService_OLD.php
Normal file
@ -0,0 +1,386 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use Config\RateLimiter as RateLimiterConfig;
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
|
||||
/**
|
||||
* RateLimiterService
|
||||
*
|
||||
* Handles all rate limiting logic:
|
||||
* - IP-level throttle + progressive blocking (soft/medium/hard)
|
||||
* - User-level progressive blocking (soft/medium/hard) by email or mobile_number
|
||||
* - Manual block / unblock helpers callable from anywhere
|
||||
*
|
||||
* Block levels: 'soft' | 'medium' | 'hard'
|
||||
* All blocks are MANUAL UNBLOCK ONLY (no auto-expiry on block state).
|
||||
* Counters and violation counts use cache TTLs; block records do not expire.
|
||||
*/
|
||||
class RateLimiterService
|
||||
{
|
||||
protected RateLimiterConfig $config;
|
||||
protected CacheInterface $cache;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = config('RateLimiter');
|
||||
$this->cache = \Config\Services::cache();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PUBLIC — IP LEVEL
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Check & throttle by fingerprint (IP+UA based).
|
||||
* Returns null on pass, or an array ['level'=>..., 'message'=>...] on block.
|
||||
*/
|
||||
public function checkIp(string $fingerprint, string $routeType = 'jwtApi'): ?array
|
||||
{
|
||||
// echo $fingerprint;die;
|
||||
// 1. Is the IP already blocked?
|
||||
$blockInfo = $this->getIpBlock($fingerprint);
|
||||
// print_rr($blockInfo);die();
|
||||
if ($blockInfo) {
|
||||
// Count hit while blocked → maybe escalate
|
||||
$this->recordIpBlockHit($fingerprint, $blockInfo['level']);
|
||||
return $this->blockedResponse('ip', $blockInfo['level']);
|
||||
}
|
||||
|
||||
// 2. Throttle check
|
||||
$cfg = $this->config->ipBlock;
|
||||
$countKey = $this->config->cacheKeys['ip_count'] . $fingerprint;
|
||||
$count = (int) ($this->cache->get($countKey) ?? 0);
|
||||
|
||||
if ($count === 0) {
|
||||
$this->cache->save($countKey, 1, $cfg['window']);
|
||||
} else {
|
||||
$this->cache->save($countKey, $count + 1, $cfg['window']);
|
||||
}
|
||||
|
||||
if (($count + 1) > $cfg['limit']) {
|
||||
// Over limit → record violation
|
||||
$violated = $this->incrementIpViolation($fingerprint);
|
||||
if ($violated >= $cfg['violation_soft']) {
|
||||
$this->blockIp($fingerprint, 'soft');
|
||||
return $this->blockedResponse('ip', 'soft');
|
||||
}
|
||||
return [
|
||||
'level' => 'throttle',
|
||||
'message' => 'Too many requests. Please slow down.',
|
||||
'status' => $this->config->statusCodes['throttle'],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a "bad outcome" for IP (e.g. controller calls this after failed auth).
|
||||
* Same escalation path as throttle violations.
|
||||
*/
|
||||
public function recordIpFailure(string $fingerprint): ?array
|
||||
{
|
||||
$blockInfo = $this->getIpBlock($fingerprint);
|
||||
if ($blockInfo) {
|
||||
$this->recordIpBlockHit($fingerprint, $blockInfo['level']);
|
||||
return $this->blockedResponse('ip', $blockInfo['level']);
|
||||
}
|
||||
|
||||
$violated = $this->incrementIpViolation($fingerprint);
|
||||
$cfg = $this->config->ipBlock;
|
||||
|
||||
if ($violated >= $cfg['violation_soft']) {
|
||||
$this->blockIp($fingerprint, 'soft');
|
||||
return $this->blockedResponse('ip', 'soft');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually block an IP at a given level.
|
||||
*/
|
||||
public function blockIp(string $fingerprint, string $level = 'soft'): void
|
||||
{
|
||||
$cfg = $this->config->ipBlock;
|
||||
$blockKey = $this->config->cacheKeys['ip_block'] . $fingerprint;
|
||||
|
||||
$duration = $this->blockDuration($cfg, $level);
|
||||
|
||||
$data = [
|
||||
'level' => $level,
|
||||
'blocked_at' => time(),
|
||||
'fingerprint'=> $fingerprint,
|
||||
'ip' => getRealClientIP(),
|
||||
];
|
||||
|
||||
// Duration 0 = store for 10 years (permanent until manual unblock)
|
||||
$ttl = $duration > 0 ? $duration : (10 * 365 * 24 * 3600);
|
||||
$this->cache->save($blockKey, $data, $ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually unblock an IP. Clears block, violations, and counters.
|
||||
*/
|
||||
public function unblockIp(string $fingerprint): void
|
||||
{
|
||||
$keys = $this->config->cacheKeys;
|
||||
$this->cache->delete($keys['ip_block'] . $fingerprint);
|
||||
$this->cache->delete($keys['ip_violations'] . $fingerprint);
|
||||
$this->cache->delete($keys['ip_count'] . $fingerprint);
|
||||
$this->cache->delete($keys['ip_block_hits'] . $fingerprint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current IP block info or null if not blocked.
|
||||
*/
|
||||
public function getIpBlock(string $fingerprint): ?array
|
||||
{
|
||||
$blockKey = $this->config->cacheKeys['ip_block'] . $fingerprint;
|
||||
$data = $this->cache->get($blockKey);
|
||||
return $data ?: null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PUBLIC — USER LEVEL
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Check if a user (by email or mobile) is blocked.
|
||||
* Returns null on pass, or block response array on block.
|
||||
*/
|
||||
public function checkUser(string $identity): ?array
|
||||
{
|
||||
$blockInfo = $this->getUserBlock($identity);
|
||||
if ($blockInfo) {
|
||||
$this->recordUserBlockHit($identity, $blockInfo['level']);
|
||||
return $this->blockedResponse('user', $blockInfo['level']);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a failed attempt for a user identity.
|
||||
* Called from controller after() or manually after a failed verification.
|
||||
* Handles escalation: free → soft → medium → hard
|
||||
*/
|
||||
public function recordUserFailure(string $identity, string $routeType = 'authApi'): ?array
|
||||
{
|
||||
$blockInfo = $this->getUserBlock($identity);
|
||||
|
||||
if ($blockInfo) {
|
||||
// Already blocked — count hit and maybe escalate
|
||||
$this->recordUserBlockHit($identity, $blockInfo['level']);
|
||||
return $this->blockedResponse('user', $blockInfo['level']);
|
||||
}
|
||||
|
||||
// Not blocked yet — increment violation count
|
||||
$violated = $this->incrementUserViolation($identity, $routeType);
|
||||
$cfg = $this->config->userBlock;
|
||||
$routeCfg = $this->config->{$routeType};
|
||||
|
||||
if ($violated >= $routeCfg['violation_soft']) {
|
||||
$this->blockUser($identity, 'soft');
|
||||
return $this->blockedResponse('user', 'soft');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually block a user identity at a given level.
|
||||
*/
|
||||
public function blockUser(string $identity, string $level = 'soft'): void
|
||||
{
|
||||
$cfg = $this->config->userBlock;
|
||||
$blockKey = $this->config->cacheKeys['user_block'] . $this->hashIdentity($identity);
|
||||
|
||||
$duration = $this->blockDuration($cfg, $level);
|
||||
$ttl = $duration > 0 ? $duration : (10 * 365 * 24 * 3600);
|
||||
|
||||
$data = [
|
||||
'level' => $level,
|
||||
'blocked_at' => time(),
|
||||
'identity' => $identity,
|
||||
];
|
||||
|
||||
$this->cache->save($blockKey, $data, $ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually unblock a user identity. Independent — does NOT touch IP block.
|
||||
*/
|
||||
public function unblockUser(string $identity): void
|
||||
{
|
||||
$keys = $this->config->cacheKeys;
|
||||
$hashed = $this->hashIdentity($identity);
|
||||
|
||||
$this->cache->delete($keys['user_block'] . $hashed);
|
||||
$this->cache->delete($keys['user_violations'] . $hashed);
|
||||
$this->cache->delete($keys['user_count'] . $hashed);
|
||||
$this->cache->delete($keys['user_block_hits'] . $hashed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user block info or null if not blocked.
|
||||
*/
|
||||
public function getUserBlock(string $identity): ?array
|
||||
{
|
||||
$blockKey = $this->config->cacheKeys['user_block'] . $this->hashIdentity($identity);
|
||||
$data = $this->cache->get($blockKey);
|
||||
return $data ?: null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// USER THROTTLE (for JWT API routes — request count based)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Throttle check for a known user on JWT routes.
|
||||
* Increments request counter; if over limit records violation.
|
||||
*/
|
||||
public function checkUserThrottle(string $identity, string $routeType = 'jwtApi'): ?array
|
||||
{
|
||||
$blockCheck = $this->checkUser($identity);
|
||||
if ($blockCheck) {
|
||||
return $blockCheck;
|
||||
}
|
||||
|
||||
$cfg = $this->config->{$routeType};
|
||||
$hashed = $this->hashIdentity($identity);
|
||||
$countKey = $this->config->cacheKeys['user_count'] . $hashed;
|
||||
$count = (int) ($this->cache->get($countKey) ?? 0);
|
||||
|
||||
if ($count === 0) {
|
||||
$this->cache->save($countKey, 1, $cfg['window']);
|
||||
} else {
|
||||
$this->cache->save($countKey, $count + 1, $cfg['window']);
|
||||
}
|
||||
|
||||
if (($count + 1) > $cfg['limit']) {
|
||||
$violated = $this->incrementUserViolation($identity, $routeType);
|
||||
if ($violated >= $cfg['violation_soft']) {
|
||||
$this->blockUser($identity, 'soft');
|
||||
return $this->blockedResponse('user', 'soft');
|
||||
}
|
||||
return [
|
||||
'level' => 'throttle',
|
||||
'message' => 'Too many requests. Please slow down.',
|
||||
'status' => $this->config->statusCodes['throttle'],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PRIVATE HELPERS
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Increment IP violation counter and return new count.
|
||||
*/
|
||||
protected function incrementIpViolation(string $fingerprint): int
|
||||
{
|
||||
$key = $this->config->cacheKeys['ip_violations'] . $fingerprint;
|
||||
$count = (int) ($this->cache->get($key) ?? 0) + 1;
|
||||
// Keep violation record for the block window duration
|
||||
$this->cache->save($key, $count, $this->config->ipBlock['window'] * 10);
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a hit while IP is already blocked; escalate if thresholds met.
|
||||
*/
|
||||
protected function recordIpBlockHit(string $fingerprint, string $currentLevel): void
|
||||
{
|
||||
$cfg = $this->config->ipBlock;
|
||||
$hitKey = $this->config->cacheKeys['ip_block_hits'] . $fingerprint;
|
||||
$hits = (int) ($this->cache->get($hitKey) ?? 0) + 1;
|
||||
$this->cache->save($hitKey, $hits, 10 * 365 * 24 * 3600);
|
||||
|
||||
if ($currentLevel === 'soft' && $hits >= $cfg['medium_trigger']) {
|
||||
$this->cache->delete($hitKey);
|
||||
$this->blockIp($fingerprint, 'medium');
|
||||
} elseif ($currentLevel === 'medium' && $hits >= $cfg['hard_trigger']) {
|
||||
$this->cache->delete($hitKey);
|
||||
$this->blockIp($fingerprint, 'hard');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment user violation counter and return new count.
|
||||
*/
|
||||
protected function incrementUserViolation(string $identity, string $routeType): int
|
||||
{
|
||||
$hashed = $this->hashIdentity($identity);
|
||||
$key = $this->config->cacheKeys['user_violations'] . $hashed;
|
||||
$count = (int) ($this->cache->get($key) ?? 0) + 1;
|
||||
$window = $this->config->{$routeType}['window'] ?? 180;
|
||||
$this->cache->save($key, $count, $window * 10);
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a hit while user is already blocked; escalate if thresholds met.
|
||||
*/
|
||||
protected function recordUserBlockHit(string $identity, string $currentLevel): void
|
||||
{
|
||||
$cfg = $this->config->userBlock;
|
||||
$hashed = $this->hashIdentity($identity);
|
||||
$hitKey = $this->config->cacheKeys['user_block_hits'] . $hashed;
|
||||
$hits = (int) ($this->cache->get($hitKey) ?? 0) + 1;
|
||||
$this->cache->save($hitKey, $hits, 10 * 365 * 24 * 3600);
|
||||
|
||||
if ($currentLevel === 'soft' && $hits >= $cfg['medium_trigger']) {
|
||||
$this->cache->delete($hitKey);
|
||||
$this->blockUser($identity, 'medium');
|
||||
} elseif ($currentLevel === 'medium' && $hits >= $cfg['hard_trigger']) {
|
||||
$this->cache->delete($hitKey);
|
||||
$this->blockUser($identity, 'hard');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve block duration from config based on level.
|
||||
*/
|
||||
protected function blockDuration(array $cfg, string $level): int
|
||||
{
|
||||
return match ($level) {
|
||||
'soft' => $cfg['soft_duration'],
|
||||
'medium' => $cfg['medium_duration'],
|
||||
'hard' => $cfg['hard_duration'],
|
||||
default => 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a standardised blocked response array.
|
||||
*/
|
||||
protected function blockedResponse(string $type, string $level): array
|
||||
{
|
||||
$messages = [
|
||||
'soft' => 'Your access has been temporarily suspended. Please contact support.',
|
||||
'medium' => 'Your access has been restricted due to repeated violations.',
|
||||
'hard' => 'Your access has been permanently blocked. Please contact support.',
|
||||
];
|
||||
|
||||
return [
|
||||
'level' => $level,
|
||||
'type' => $type,
|
||||
'message' => $messages[$level] ?? 'Access denied.',
|
||||
'status' => $this->config->statusCodes[$level],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash user identity (email or mobile) for cache key safety.
|
||||
*/
|
||||
protected function hashIdentity(string $identity): string
|
||||
{
|
||||
// return strtolower(trim($identity));
|
||||
return hash('sha256', strtolower(trim($identity)));
|
||||
}
|
||||
}
|
||||
@ -131,14 +131,17 @@ class EmployeePolicyModel extends Model
|
||||
// Name audit trail
|
||||
'(SELECT old_value FROM auditing_history WHERE pk = emp.id AND field_name = "name" AND table_name = "employees" ORDER BY id ASC LIMIT 1) AS name_first_old',
|
||||
'(SELECT new_value FROM auditing_history WHERE pk = emp.id AND field_name = "name" AND table_name = "employees" ORDER BY id DESC LIMIT 1) AS name_last_new',
|
||||
'(SELECT DATE_FORMAT(COALESCE(created_at, emp.updated_at), "%d/%m/%Y %h:%i %p") FROM auditing_history WHERE pk = emp.id AND field_name = "name" AND table_name = "employees" ORDER BY id DESC LIMIT 1) AS name_created_at',
|
||||
|
||||
// DOB audit trail
|
||||
'(SELECT old_value FROM auditing_history WHERE pk = emp.id AND field_name = "dob" AND table_name = "employees" ORDER BY id ASC LIMIT 1) AS dob_first_old',
|
||||
'(SELECT new_value FROM auditing_history WHERE pk = emp.id AND field_name = "dob" AND table_name = "employees" ORDER BY id DESC LIMIT 1) AS dob_last_new',
|
||||
'(SELECT DATE_FORMAT(COALESCE(created_at, emp.updated_at), "%d/%m/%Y %h:%i %p") FROM auditing_history WHERE pk = emp.id AND field_name = "dob" AND table_name = "employees" ORDER BY id DESC LIMIT 1) AS dob_created_at',
|
||||
|
||||
// Gender audit trail
|
||||
'(SELECT old_value FROM auditing_history WHERE pk = emp.id AND field_name = "gender" AND table_name = "employees" ORDER BY id ASC LIMIT 1) AS gender_first_old',
|
||||
'(SELECT new_value FROM auditing_history WHERE pk = emp.id AND field_name = "gender" AND table_name = "employees" ORDER BY id DESC LIMIT 1) AS gender_last_new',
|
||||
'(SELECT DATE_FORMAT(COALESCE(created_at, emp.updated_at), "%d/%m/%Y %h:%i %p") FROM auditing_history WHERE pk = emp.id AND field_name = "gender" AND table_name = "employees" ORDER BY id DESC LIMIT 1) AS gender_created_at',
|
||||
|
||||
"(CASE
|
||||
WHEN emp.relationship = 'Self'
|
||||
@ -160,6 +163,19 @@ class EmployeePolicyModel extends Model
|
||||
ELSE NULL
|
||||
END) AS removed_summary",
|
||||
|
||||
"(CASE
|
||||
WHEN emp.relationship = 'Self'
|
||||
THEN (
|
||||
SELECT DATE_FORMAT(MAX(e.updated_at), '%d/%m/%Y %h:%i %p')
|
||||
FROM employees e
|
||||
JOIN employee_polices ep ON e.id = ep.employee_id
|
||||
WHERE e.emp_code = emp.emp_code
|
||||
AND (e.is_dependent_modified = 0 OR (e.is_active = 0 AND e.emp_status != 'truncated'))
|
||||
AND ep.client_policy_id = employee_polices.client_policy_id
|
||||
)
|
||||
ELSE NULL
|
||||
END) AS removed_summary_updated_at",
|
||||
|
||||
"(IF(COALESCE(emp.is_dependent_modified, 0) = 1, 'Newly Added, ',
|
||||
CASE
|
||||
WHEN emp.relationship != 'Self'
|
||||
@ -180,6 +196,25 @@ class EmployeePolicyModel extends Model
|
||||
END
|
||||
)) AS newly_added",
|
||||
|
||||
"(IF(COALESCE(emp.is_dependent_modified, 0) = 1, DATE_FORMAT(emp.updated_at, '%d/%m/%Y %h:%i %p'),
|
||||
CASE
|
||||
WHEN emp.relationship != 'Self'
|
||||
AND emp.file_id IS NULL
|
||||
AND emp.created_by = (
|
||||
SELECT e_sub.id
|
||||
FROM employees e_sub
|
||||
JOIN employee_polices ep_sub ON e_sub.id = ep_sub.employee_id
|
||||
WHERE e_sub.emp_code = emp.emp_code
|
||||
AND e_sub.relationship = 'Self'
|
||||
AND e_sub.is_active = 1
|
||||
AND ep_sub.client_policy_id = employee_polices.client_policy_id
|
||||
LIMIT 1
|
||||
)
|
||||
THEN DATE_FORMAT(emp.updated_at, '%d/%m/%Y %h:%i %p')
|
||||
ELSE NULL
|
||||
END
|
||||
)) AS newly_added_updated_at",
|
||||
|
||||
$status_query
|
||||
], false)
|
||||
->join('employees emp', 'employee_polices.employee_id = emp.id')
|
||||
|
||||
102
app/Views/admin/rate_limit_blocks.php
Normal file
102
app/Views/admin/rate_limit_blocks.php
Normal file
@ -0,0 +1,102 @@
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0">Rate Limit Blocks</h4>
|
||||
<small class="text-muted">URL-only admin utility</small>
|
||||
</div>
|
||||
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $activeTab === 'ip' ? 'active' : '' ?>" href="<?= base_url('security/rate-limits?tab=ip') ?>">
|
||||
Blocked IP List (<?= count($blockedIps) ?>)
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $activeTab === 'user' ? 'active' : '' ?>" href="<?= base_url('security/rate-limits?tab=user') ?>">
|
||||
Blocked User List (<?= count($blockedUsers) ?>)
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<?php if ($activeTab === 'ip'): ?>
|
||||
<div class="card">
|
||||
<div class="card-body table-responsive">
|
||||
<table class="table table-striped table-bordered align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP</th>
|
||||
<th>Block Level</th>
|
||||
<th>Cache Identifier (Fingerprint)</th>
|
||||
<th>Blocked At</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($blockedIps)): ?>
|
||||
<tr><td colspan="5" class="text-center text-muted">No active blocked IP records.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($blockedIps as $row): ?>
|
||||
<tr>
|
||||
<td><?= esc($row['display_identifier'] ?? '-') ?></td>
|
||||
<td><span class="badge bg-danger"><?= esc(strtoupper((string) ($row['block_level'] ?? '-'))) ?></span></td>
|
||||
<td><small><?= esc($row['cache_identifier'] ?? '-') ?></small></td>
|
||||
<td><?= esc((string) ($row['blocked_at'] ?? '-')) ?></td>
|
||||
<td>
|
||||
<form method="post" action="<?= base_url('security/rate-limits/unblock-ip') ?>" class="d-flex gap-2">
|
||||
<input type="hidden" name="cache_identifier" value="<?= esc($row['cache_identifier'] ?? '') ?>">
|
||||
<input type="text" name="reason" class="form-control form-control-sm" placeholder="Reason (optional)">
|
||||
<button type="submit" class="btn btn-sm btn-success" onclick="return confirm('Unblock this IP entry?')">Unblock</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="card">
|
||||
<div class="card-body table-responsive">
|
||||
<table class="table table-striped table-bordered align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User Identity</th>
|
||||
<th>Block Level</th>
|
||||
<th>Identity Hash Key</th>
|
||||
<th>Blocked At</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($blockedUsers)): ?>
|
||||
<tr><td colspan="5" class="text-center text-muted">No active blocked user records.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($blockedUsers as $row): ?>
|
||||
<tr>
|
||||
<td><?= esc($row['display_identifier'] ?? '-') ?></td>
|
||||
<td><span class="badge bg-danger"><?= esc(strtoupper((string) ($row['block_level'] ?? '-'))) ?></span></td>
|
||||
<td><small><?= esc($row['cache_identifier'] ?? '-') ?></small></td>
|
||||
<td><?= esc((string) ($row['blocked_at'] ?? '-')) ?></td>
|
||||
<td>
|
||||
<form method="post" action="<?= base_url('security/rate-limits/unblock-user') ?>" class="d-flex gap-2">
|
||||
<input type="hidden" name="display_identifier" value="<?= esc($row['display_identifier'] ?? '') ?>">
|
||||
<input type="text" name="reason" class="form-control form-control-sm" placeholder="Reason (optional)">
|
||||
<button type="submit" class="btn btn-sm btn-success" onclick="return confirm('Unblock this user entry?')">Unblock</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@ -201,22 +201,42 @@
|
||||
<td>
|
||||
<?php
|
||||
if(!empty($employee['newly_added'])){
|
||||
echo "<p>" . $employee['newly_added'] . "</p>";
|
||||
echo "<p>" . $employee['newly_added'];
|
||||
if (!empty($employee['newly_added_updated_at'])) {
|
||||
echo ' (' . $employee['newly_added_updated_at'] . ')';
|
||||
}
|
||||
echo "</p>";
|
||||
}
|
||||
if(!empty($employee['name_first_old']) ){
|
||||
echo "<p> Name : " . $employee['name_first_old'] . ' => ' . $employee['name_last_new'] . "</p>";
|
||||
echo "<p> Name : " . $employee['name_first_old'] . ' => ' . $employee['name_last_new'];
|
||||
if (!empty($employee['name_created_at'])) {
|
||||
echo ' (' . $employee['name_created_at'] . ')';
|
||||
}
|
||||
echo "</p>";
|
||||
}
|
||||
if(!empty($employee['dob_first_old'])){
|
||||
echo "<p> DOB : " . change_date_format($employee['dob_first_old'], null, 'd/m/Y') . ' => ' . change_date_format($employee['dob_last_new'], null, 'd/m/Y') . "</p>";
|
||||
echo "<p> DOB : " . change_date_format($employee['dob_first_old'], null, 'd/m/Y') . ' => ' . change_date_format($employee['dob_last_new'], null, 'd/m/Y');
|
||||
if (!empty($employee['dob_created_at'])) {
|
||||
echo ' (' . $employee['dob_created_at'] . ')';
|
||||
}
|
||||
echo "</p>";
|
||||
}
|
||||
if(!empty($employee['gender_first_old'])){
|
||||
echo "<p> Gender : " . $employee['gender_first_old'] . ' => ' . $employee['gender_last_new'] . "</p>";
|
||||
echo "<p> Gender : " . $employee['gender_first_old'] . ' => ' . $employee['gender_last_new'];
|
||||
if (!empty($employee['gender_created_at'])) {
|
||||
echo ' (' . $employee['gender_created_at'] . ')';
|
||||
}
|
||||
echo "</p>";
|
||||
}
|
||||
// if(!empty($employee['removed_count'])){
|
||||
// echo "<p>" . $employee['removed_count'] . " People removed </p>";
|
||||
// }
|
||||
if(!empty($employee['removed_summary'])){
|
||||
echo "<p>" . $employee['removed_summary'] . " </p>";
|
||||
echo "<p>" . $employee['removed_summary'];
|
||||
if (!empty($employee['removed_summary_updated_at'])) {
|
||||
echo ' (' . $employee['removed_summary_updated_at'] . ')';
|
||||
}
|
||||
echo " </p>";
|
||||
}
|
||||
if(empty($employee['name_first_old']) && empty($employee['dob_first_old']) && empty($employee['gender_first_old']) && empty($employee['removed_summary'] ) && empty($employee['newly_added'] )){
|
||||
echo " - ";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user