get('api/profile', 'ProfileController::index', ['filter' => 'jwtApiRateLimit']); * * Register in app/Config/Filters.php: * 'JwtApiRateLimitFilter' => \App\Filters\JwtApiRateLimitFilter::class */ class JwtApiRateLimitFilter implements FilterInterface { protected RateLimiterService $limiter; public function __construct() { $this->limiter = new RateLimiterService(); } // ------------------------------------------------------------------------- // BEFORE — runs before the controller // ------------------------------------------------------------------------- public function before(RequestInterface $request, $arguments = null) { $fingerprint = generateFingerprint(exclude_ua: true); // echo $fingerprint;die; // 1. IP-level throttle + block check $ipResult = $this->limiter->checkIp($fingerprint, 'jwtApi'); if ($ipResult) { return $this->jsonResponse($ipResult); } // 2. Resolve user identity from JWT // Uses your existing JWT helper functions. // If neither returns a value, fall back to IP-only limiting. // $identity = $this->resolveIdentityFromJwt(); $identity = ''; if ($identity) { // User-level throttle (request count based for JWT routes) $userResult = $this->limiter->checkUserThrottle($identity, 'jwtApi'); if ($userResult) { return $this->jsonResponse($userResult); } } // Stash for after() use $request->setGlobal('rateLimitFingerprint', $fingerprint); if ($identity) { $request->setGlobal('rateLimitIdentity', $identity); } return null; // pass through } // ------------------------------------------------------------------------- // AFTER — records IP failure on controller-level bad responses // ------------------------------------------------------------------------- public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { $statusCode = $response->getStatusCode(); // Only act on auth-related failures from the controller (401, 422, etc.) // 429/403/451 are already handled by before(); skip 2xx/3xx. if ($statusCode < 400 || in_array($statusCode, [429, 403, 451])) { return; } $fingerprint = $request->getGlobal('rateLimitFingerprint') ?? generateFingerprint(exclude_ua: true); // $identity = $request->getGlobal('rateLimitIdentity') ?? $this->resolveIdentityFromJwt(); $identity = $request->getGlobal('rateLimitIdentity') ?? ''; $this->limiter->recordIpFailure($fingerprint); if ($identity) { $this->limiter->recordUserFailure($identity, 'jwtApi'); } } // ------------------------------------------------------------------------- // HELPERS // ------------------------------------------------------------------------- /** * Resolve user identity from JWT using your existing helper functions. * Tries email first, then mobile. Returns null if JWT is absent/invalid. * * IMPORTANT: Replace getEmailFromJWT() / getMobileFromJWT() with your * actual function names if they differ. */ protected function resolveIdentityFromJwt(): ?string { try { // Try email from JWT if (function_exists('getEmailFromJWT')) { $email = getEmailFromJWT(); if ($email) { return strtolower(trim($email)); } } // Try mobile from JWT if (function_exists('getMobileFromJWT')) { $mobile = getMobileFromJWT(); if ($mobile) { return trim($mobile); } } } catch (\Throwable $e) { // JWT invalid or expired — fall through to IP-only limiting log_message('debug', '[RateLimiter] JWT identity resolution failed: ' . $e->getMessage()); } return null; } /** * 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', ], ])); return $response; } }