Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 3 additions & 44 deletions apps/api/plane/api/rate_limit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,16 @@
# 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


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
Expand Down
17 changes: 2 additions & 15 deletions apps/api/plane/api/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down
3 changes: 3 additions & 0 deletions apps/api/plane/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions apps/web/app/entry.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
47 changes: 37 additions & 10 deletions apps/web/core/lib/idle-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
};
25 changes: 3 additions & 22 deletions apps/web/core/lib/polyfills/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading