` mapped * to an api_keys row. Per-key hourly quota enforced here. */ final class ApiAuth implements FilterInterface { public function before(RequestInterface $request, $arguments = null) { if (! $request instanceof \CodeIgniter\HTTP\IncomingRequest) { return null; } $header = $request->getHeaderLine('Authorization'); if ($header === '' || ! preg_match('/^Bearer\s+(\S+)$/i', $header, $m)) { return $this->deny(401, 'Missing bearer token.'); } $key = model(\App\Models\ApiKeyModel::class)->findByPlainKey($m[1]); if ($key === null) { return $this->deny(401, 'Invalid token.'); } // hourly per-key quota $bucket = 'rl_api_' . $key['key_prefix'] . date('YmdH'); $used = (int) (cache()->get($bucket) ?? 0); if ($used >= (int) $key['rate_limit_per_hour']) { return response()->setStatusCode(429) ->setJSON(['error' => 'quota_exceeded', 'message' => 'Hourly API quota exhausted.']); } cache()->save($bucket, $used + 1, 3700); service('request')->api_key = $key; // available to controllers return null; } private function deny(int $status, string $message): ResponseInterface { return response()->setStatusCode($status) ->setJSON(['error' => 'unauthorized', 'message' => $message]); } public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return null; } }