diff --git a/apps/api/plane/api/rate_limit.py b/apps/api/plane/api/rate_limit.py index 33df895cbfb..7567ec156fc 100644 --- a/apps/api/plane/api/rate_limit.py +++ b/apps/api/plane/api/rate_limit.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: AGPL-3.0-only # See the LICENSE file for details. -# python imports -import os +# Django imports +from django.conf import settings # Third party imports from rest_framework.throttling import SimpleRateThrottle @@ -11,48 +11,7 @@ class ApiKeyRateThrottle(SimpleRateThrottle): scope = "api_key" - rate = os.environ.get("API_KEY_RATE_LIMIT", "60/minute") - - def get_cache_key(self, request, view): - # Retrieve the API key from the request header - api_key = request.headers.get("X-Api-Key") - if not api_key: - return None # Allow the request if there's no API key - - # Use the API key as part of the cache key - return f"{self.scope}:{api_key}" - - def allow_request(self, request, view): - allowed = super().allow_request(request, view) - - if allowed: - now = self.timer() - # Calculate the remaining limit and reset time - history = self.cache.get(self.key, []) - - # Remove old histories - while history and history[-1] <= now - self.duration: - history.pop() - - # Calculate the requests - num_requests = len(history) - - # Check available requests - available = self.num_requests - num_requests - - # Unix timestamp for when the rate limit will reset - reset_time = int(now + self.duration) - - # Add headers - request.META["X-RateLimit-Remaining"] = max(0, available) - request.META["X-RateLimit-Reset"] = reset_time - - return allowed - - -class ServiceTokenRateThrottle(SimpleRateThrottle): - scope = "service_token" - rate = "300/minute" + rate = settings.API_KEY_RATE_LIMIT def get_cache_key(self, request, view): # Retrieve the API key from the request header diff --git a/apps/api/plane/api/views/base.py b/apps/api/plane/api/views/base.py index fc65e7abdcf..2fb20c8d178 100644 --- a/apps/api/plane/api/views/base.py +++ b/apps/api/plane/api/views/base.py @@ -22,9 +22,8 @@ from rest_framework.generics import GenericAPIView # Module imports -from plane.db.models.api import APIToken from plane.api.middleware.api_authentication import APIKeyAuthentication -from plane.api.rate_limit import ApiKeyRateThrottle, ServiceTokenRateThrottle +from plane.api.rate_limit import ApiKeyRateThrottle from plane.utils.exception_logger import log_exception from plane.utils.paginator import BasePaginator from plane.utils.core.mixins import ReadReplicaControlMixin @@ -60,19 +59,7 @@ def filter_queryset(self, queryset): return queryset def get_throttles(self): - throttle_classes = [] - api_key = self.request.headers.get("X-Api-Key") - - if api_key: - service_token = APIToken.objects.filter(token=api_key, is_service=True).first() - - if service_token: - throttle_classes.append(ServiceTokenRateThrottle()) - return throttle_classes - - throttle_classes.append(ApiKeyRateThrottle()) - - return throttle_classes + return [ApiKeyRateThrottle()] def handle_exception(self, exc): """ diff --git a/apps/api/plane/settings/common.py b/apps/api/plane/settings/common.py index 8dba3066e70..65b5d7b9f82 100644 --- a/apps/api/plane/settings/common.py +++ b/apps/api/plane/settings/common.py @@ -132,6 +132,9 @@ "SCHEMA_COERCE_PATH_PK": False, } +# API key throttle rate (DRF SimpleRateThrottle format, e.g. "60/minute") +API_KEY_RATE_LIMIT = os.environ.get("API_KEY_RATE_LIMIT", "60/minute") + # Django Auth Backend AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",) # default diff --git a/apps/web/app/entry.client.tsx b/apps/web/app/entry.client.tsx index 9c665ede072..8dc2a587717 100644 --- a/apps/web/app/entry.client.tsx +++ b/apps/web/app/entry.client.tsx @@ -8,6 +8,10 @@ import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; import { HydratedRouter } from "react-router/dom"; +import polyfills from "@/lib/polyfills"; + +void polyfills; + startTransition(() => { hydrateRoot( document, diff --git a/apps/web/core/lib/idle-task.ts b/apps/web/core/lib/idle-task.ts index 34d070bfe40..3d6db72b8f5 100644 --- a/apps/web/core/lib/idle-task.ts +++ b/apps/web/core/lib/idle-task.ts @@ -8,20 +8,47 @@ export type IdleTaskHandle = { cancel: () => void; }; +const requestIdleFallback = (callback: IdleRequestCallback, options?: IdleRequestOptions): number => { + const start = Date.now(); + + return globalThis.setTimeout(() => { + callback({ + didTimeout: false, + timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), + }); + }, options?.timeout ?? 1) as unknown as number; +}; + +const cancelIdleFallback = (id: number) => { + globalThis.clearTimeout(id); +}; + +export const requestIdle = (callback: IdleRequestCallback, options?: IdleRequestOptions): number => { + if (typeof globalThis.requestIdleCallback === "function") return globalThis.requestIdleCallback(callback, options); + + return requestIdleFallback(callback, options); +}; + +export const cancelIdle = (id: number) => { + if (typeof globalThis.cancelIdleCallback === "function") return globalThis.cancelIdleCallback(id); + + return cancelIdleFallback(id); +}; + +export const installIdleCallbackPolyfill = () => { + if (typeof globalThis === "undefined") return; + + globalThis.requestIdleCallback = globalThis.requestIdleCallback ?? requestIdleFallback; + globalThis.cancelIdleCallback = globalThis.cancelIdleCallback ?? cancelIdleFallback; +}; + /** * Schedule lightweight work for idle time and return a cancel handle. * Falls back to setTimeout when requestIdleCallback is unavailable. */ -export const runIdleTask = (callback: () => void): IdleTaskHandle => { - if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") { - const idleId = window.requestIdleCallback(callback, { timeout: 300 }); - return { - cancel: () => window.cancelIdleCallback(idleId), - }; - } - - const timeoutId = window.setTimeout(callback, 0); +export const runIdleTask = (callback: IdleRequestCallback): IdleTaskHandle => { + const idleId = requestIdle(callback, { timeout: 300 }); return { - cancel: () => window.clearTimeout(timeoutId), + cancel: () => cancelIdle(idleId), }; }; diff --git a/apps/web/core/lib/polyfills/index.ts b/apps/web/core/lib/polyfills/index.ts index 2243dc620c2..fae765ebf8e 100644 --- a/apps/web/core/lib/polyfills/index.ts +++ b/apps/web/core/lib/polyfills/index.ts @@ -4,27 +4,8 @@ * See the LICENSE file for details. */ -if (typeof window !== "undefined" && window) { - // Add request callback polyfill to browser in case it does not exist - window.requestIdleCallback = - window.requestIdleCallback ?? - function (cb) { - const start = Date.now(); - return setTimeout(function () { - cb({ - didTimeout: false, - timeRemaining: function () { - return Math.max(0, 50 - (Date.now() - start)); - }, - }); - }, 1); - }; +import { installIdleCallbackPolyfill } from "@/lib/idle-task"; - window.cancelIdleCallback = - window.cancelIdleCallback ?? - function (id) { - clearTimeout(id); - }; -} +installIdleCallbackPolyfill(); -export {}; +export default true;