diff --git a/apiserver/plane/authentication/adapter/error.py b/apiserver/plane/authentication/adapter/error.py index bf45f1d6a7..85fb69cfcf 100644 --- a/apiserver/plane/authentication/adapter/error.py +++ b/apiserver/plane/authentication/adapter/error.py @@ -61,6 +61,8 @@ AUTHENTICATION_ERROR_CODES = { "ADMIN_USER_ALREADY_EXIST": 5180, "ADMIN_USER_DOES_NOT_EXIST": 5185, "ADMIN_USER_DEACTIVATED": 5190, + # Rate limit + "RATE_LIMIT_EXCEEDED": 5900, } diff --git a/apiserver/plane/authentication/adapter/exception.py b/apiserver/plane/authentication/adapter/exception.py index 12845ea025..a6f7637a98 100644 --- a/apiserver/plane/authentication/adapter/exception.py +++ b/apiserver/plane/authentication/adapter/exception.py @@ -1,5 +1,10 @@ +# Third party imports from rest_framework.views import exception_handler from rest_framework.exceptions import NotAuthenticated +from rest_framework.exceptions import Throttled + +# Module imports +from plane.authentication.adapter.error import AuthenticationException, AUTHENTICATION_ERROR_CODES def auth_exception_handler(exc, context): @@ -9,4 +14,14 @@ def auth_exception_handler(exc, context): if isinstance(exc, NotAuthenticated): response.status_code = 401 + # Check if an Throttled exception is raised. + if isinstance(exc, Throttled): + exc = AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"], + error_message="RATE_LIMIT_EXCEEDED", + ) + response.data = exc.get_error_dict() + response.status_code = 429 + + # Return the response that is generated by the default exception handler. return response diff --git a/apiserver/plane/authentication/rate_limit.py b/apiserver/plane/authentication/rate_limit.py new file mode 100644 index 0000000000..744bd38fec --- /dev/null +++ b/apiserver/plane/authentication/rate_limit.py @@ -0,0 +1,26 @@ +# Third party imports +from rest_framework.throttling import AnonRateThrottle +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.authentication.adapter.error import ( + AuthenticationException, + AUTHENTICATION_ERROR_CODES, +) + + +class AuthenticationThrottle(AnonRateThrottle): + rate = "30/minute" + scope = "authentication" + + def throttle_failure_view(self, request, *args, **kwargs): + try: + raise AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"], + error_message="RATE_LIMIT_EXCEEDED", + ) + except AuthenticationException as e: + return Response( + e.get_error_dict(), status=status.HTTP_429_TOO_MANY_REQUESTS + ) diff --git a/apiserver/plane/authentication/views/app/check.py b/apiserver/plane/authentication/views/app/check.py index 4f164e3088..2448aee559 100644 --- a/apiserver/plane/authentication/views/app/check.py +++ b/apiserver/plane/authentication/views/app/check.py @@ -15,7 +15,7 @@ from plane.authentication.adapter.error import ( AuthenticationException, AUTHENTICATION_ERROR_CODES, ) - +from plane.authentication.rate_limit import AuthenticationThrottle class EmailCheckSignUpEndpoint(APIView): @@ -23,6 +23,10 @@ class EmailCheckSignUpEndpoint(APIView): AllowAny, ] + throttle_classes = [ + AuthenticationThrottle, + ] + def post(self, request): try: # Check instance configuration @@ -86,6 +90,10 @@ class EmailCheckSignInEndpoint(APIView): AllowAny, ] + throttle_classes = [ + AuthenticationThrottle, + ] + def post(self, request): try: # Check instance configuration diff --git a/apiserver/plane/authentication/views/app/password_management.py b/apiserver/plane/authentication/views/app/password_management.py index dd14ceb914..43054867ef 100644 --- a/apiserver/plane/authentication/views/app/password_management.py +++ b/apiserver/plane/authentication/views/app/password_management.py @@ -32,7 +32,7 @@ from plane.authentication.adapter.error import ( AuthenticationException, AUTHENTICATION_ERROR_CODES, ) - +from plane.authentication.rate_limit import AuthenticationThrottle def generate_password_token(user): uidb64 = urlsafe_base64_encode(smart_bytes(user.id)) @@ -46,6 +46,10 @@ class ForgotPasswordEndpoint(APIView): AllowAny, ] + throttle_classes = [ + AuthenticationThrottle, + ] + def post(self, request): email = request.data.get("email") diff --git a/apiserver/plane/authentication/views/common.py b/apiserver/plane/authentication/views/common.py index 640f744ceb..3e95d6ed88 100644 --- a/apiserver/plane/authentication/views/common.py +++ b/apiserver/plane/authentication/views/common.py @@ -1,3 +1,6 @@ +# Django imports +from django.shortcuts import render + # Third party imports from rest_framework import status from rest_framework.permissions import AllowAny @@ -17,7 +20,7 @@ from plane.authentication.adapter.error import ( ) from django.middleware.csrf import get_token from plane.utils.cache import invalidate_cache - +from plane.authentication.utils.host import base_host class CSRFTokenEndpoint(APIView): @@ -34,6 +37,11 @@ class CSRFTokenEndpoint(APIView): ) +def csrf_failure(request, reason=""): + """Custom CSRF failure view""" + return render(request, "csrf_failure.html", {"reason": reason, "root_url": base_host(request=request)}) + + class ChangePasswordEndpoint(APIView): def post(self, request): user = User.objects.get(pk=request.user.id) diff --git a/apiserver/plane/authentication/views/space/check.py b/apiserver/plane/authentication/views/space/check.py index 83f52e28f5..1b20d19a26 100644 --- a/apiserver/plane/authentication/views/space/check.py +++ b/apiserver/plane/authentication/views/space/check.py @@ -15,7 +15,7 @@ from plane.authentication.adapter.error import ( AUTHENTICATION_ERROR_CODES, AuthenticationException, ) - +from plane.authentication.rate_limit import AuthenticationThrottle class EmailCheckEndpoint(APIView): @@ -23,6 +23,10 @@ class EmailCheckEndpoint(APIView): AllowAny, ] + throttle_classes = [ + AuthenticationThrottle, + ] + def post(self, request): # Check instance configuration instance = Instance.objects.first() diff --git a/apiserver/plane/authentication/views/space/password_management.py b/apiserver/plane/authentication/views/space/password_management.py index fa20fa618b..3e0379b964 100644 --- a/apiserver/plane/authentication/views/space/password_management.py +++ b/apiserver/plane/authentication/views/space/password_management.py @@ -32,6 +32,7 @@ from plane.authentication.adapter.error import ( AuthenticationException, AUTHENTICATION_ERROR_CODES, ) +from plane.authentication.rate_limit import AuthenticationThrottle def generate_password_token(user): @@ -46,6 +47,10 @@ class ForgotPasswordSpaceEndpoint(APIView): AllowAny, ] + throttle_classes = [ + AuthenticationThrottle, + ] + def post(self, request): email = request.data.get("email") diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index d2a6b023a4..be040830de 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -348,6 +348,7 @@ CSRF_COOKIE_SECURE = secure_origins CSRF_COOKIE_HTTPONLY = True CSRF_TRUSTED_ORIGINS = cors_allowed_origins CSRF_COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", None) +CSRF_FAILURE_VIEW = "plane.authentication.views.common.csrf_failure" # Base URLs ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None) diff --git a/apiserver/templates/csrf_failure.html b/apiserver/templates/csrf_failure.html new file mode 100644 index 0000000000..b5a58cb021 --- /dev/null +++ b/apiserver/templates/csrf_failure.html @@ -0,0 +1,66 @@ + + + + + + + CSRF Verification Failed + + + +
+
+

CSRF Verification Failed

+
+
+

+ It looks like your form submission has expired or there was a problem + with your request. +

+

Please try the following:

+ + Go to Home Page +
+
+ + diff --git a/web/components/labels/label-block/drag-handle.tsx b/packages/ui/src/drag-handle.tsx similarity index 57% rename from web/components/labels/label-block/drag-handle.tsx rename to packages/ui/src/drag-handle.tsx index 64aaa075f6..d04f7929f2 100644 --- a/web/components/labels/label-block/drag-handle.tsx +++ b/packages/ui/src/drag-handle.tsx @@ -1,19 +1,29 @@ +import React from "react"; import { forwardRef } from "react"; import { MoreVertical } from "lucide-react"; interface IDragHandle { isDragging: boolean; + disabled?: boolean; } export const DragHandle = forwardRef((props, ref) => { - const { isDragging } = props; + const { isDragging, disabled = false } = props; + + if (disabled) { + return
; + } return ( )} = observer((props) => { className="relative flex-shrink-0 text-left" > - m.key === peekMode)?.icon ?? ""} /> + m.key === peekMode)?.icon ?? ""} className="text-[1rem]" /> = observer((props) => { {(peekMode === "side" || peekMode === "modal") && (
)} diff --git a/space/components/issues/peek-overview/issue-reaction.tsx b/space/components/issues/peek-overview/issue-reaction.tsx index 28090d36aa..9911253122 100644 --- a/space/components/issues/peek-overview/issue-reaction.tsx +++ b/space/components/issues/peek-overview/issue-reaction.tsx @@ -19,7 +19,6 @@ export const IssueReactions: React.FC = () => {
-
)} {canReact && ( diff --git a/space/components/issues/peek-overview/issue-vote-reactions.tsx b/space/components/issues/peek-overview/issue-vote-reactions.tsx index 073938daad..388d590723 100644 --- a/space/components/issues/peek-overview/issue-vote-reactions.tsx +++ b/space/components/issues/peek-overview/issue-vote-reactions.tsx @@ -97,7 +97,7 @@ export const IssueVotes: React.FC = observer((props) => { if (user) handleVote(e, 1); else router.push(`/?next_path=${pathName}?${queryParam}`); }} - className={`flex items-center justify-center gap-x-1 overflow-hidden rounded border px-2 focus:outline-none ${ + className={`flex items-center justify-center gap-x-1 overflow-hidden rounded border px-2 h-7 focus:outline-none ${ isUpVotedByUser ? "border-custom-primary-200 text-custom-primary-200" : "border-custom-border-300" }`} > @@ -131,7 +131,7 @@ export const IssueVotes: React.FC = observer((props) => { if (user) handleVote(e, -1); else router.push(`/?next_path=${pathName}?${queryParam}`); }} - className={`flex items-center justify-center gap-x-1 overflow-hidden rounded border px-2 focus:outline-none ${ + className={`flex items-center justify-center gap-x-1 h-7 overflow-hidden rounded border px-2 focus:outline-none ${ isDownVotedByUser ? "border-red-600 text-red-600" : "border-custom-border-300" }`} > diff --git a/web/components/account/oauth/github-button.tsx b/web/components/account/oauth/github-button.tsx index be9b59f249..6d274c125f 100644 --- a/web/components/account/oauth/github-button.tsx +++ b/web/components/account/oauth/github-button.tsx @@ -25,8 +25,8 @@ export const GithubOAuthButton: FC = (props) => { return (