diff --git a/packages/server/src/utils/quotaUsage.ts b/packages/server/src/utils/quotaUsage.ts
index e2cf382d4..35cf855aa 100644
--- a/packages/server/src/utils/quotaUsage.ts
+++ b/packages/server/src/utils/quotaUsage.ts
@@ -70,7 +70,7 @@ export const checkUsageLimit = async (
if (limit === -1) return
if (currentUsage > limit) {
- throw new InternalFlowiseError(StatusCodes.TOO_MANY_REQUESTS, `Limit exceeded: ${type}`)
+ throw new InternalFlowiseError(StatusCodes.PAYMENT_REQUIRED, `Limit exceeded: ${type}`)
}
}
@@ -135,7 +135,7 @@ export const checkPredictions = async (orgId: string, subscriptionId: string, us
if (predictionsLimit === -1) return
if (currentPredictions >= predictionsLimit) {
- throw new InternalFlowiseError(StatusCodes.TOO_MANY_REQUESTS, 'Predictions limit exceeded')
+ throw new InternalFlowiseError(StatusCodes.PAYMENT_REQUIRED, 'Predictions limit exceeded')
}
return {
@@ -161,7 +161,7 @@ export const checkStorage = async (orgId: string, subscriptionId: string, usageC
if (storageLimit === -1) return
if (currentStorageUsage >= storageLimit) {
- throw new InternalFlowiseError(StatusCodes.TOO_MANY_REQUESTS, 'Storage limit exceeded')
+ throw new InternalFlowiseError(StatusCodes.PAYMENT_REQUIRED, 'Storage limit exceeded')
}
return {
diff --git a/packages/ui/src/routes/AuthRoutes.jsx b/packages/ui/src/routes/AuthRoutes.jsx
index 2d63fc387..eb303b98c 100644
--- a/packages/ui/src/routes/AuthRoutes.jsx
+++ b/packages/ui/src/routes/AuthRoutes.jsx
@@ -10,6 +10,7 @@ const VerifyEmailPage = Loadable(lazy(() => import('@/views/auth/verify-email'))
const ForgotPasswordPage = Loadable(lazy(() => import('@/views/auth/forgotPassword')))
const ResetPasswordPage = Loadable(lazy(() => import('@/views/auth/resetPassword')))
const UnauthorizedPage = Loadable(lazy(() => import('@/views/auth/unauthorized')))
+const RateLimitedPage = Loadable(lazy(() => import('@/views/auth/rateLimited')))
const OrganizationSetupPage = Loadable(lazy(() => import('@/views/organization/index')))
const LicenseExpiredPage = Loadable(lazy(() => import('@/views/auth/expired')))
@@ -45,6 +46,10 @@ const AuthRoutes = {
path: '/unauthorized',
element:
},
+ {
+ path: '/rate-limited',
+ element:
+ },
{
path: '/organization-setup',
element:
diff --git a/packages/ui/src/store/context/ErrorContext.jsx b/packages/ui/src/store/context/ErrorContext.jsx
index e41070a15..8f97bb980 100644
--- a/packages/ui/src/store/context/ErrorContext.jsx
+++ b/packages/ui/src/store/context/ErrorContext.jsx
@@ -10,11 +10,29 @@ const ErrorContext = createContext()
export const ErrorProvider = ({ children }) => {
const [error, setError] = useState(null)
+ const [authRateLimitError, setAuthRateLimitError] = useState(null)
const navigate = useNavigate()
const handleError = async (err) => {
console.error(err)
- if (err?.response?.status === 403) {
+ if (err?.response?.status === 429 && err?.response?.data?.type === 'authentication_rate_limit') {
+ setAuthRateLimitError("You're making a lot of requests. Please wait and try again later.")
+ } else if (err?.response?.status === 429 && err?.response?.data?.type !== 'authentication_rate_limit') {
+ const retryAfterHeader = err?.response?.headers?.['retry-after']
+ let retryAfter = 60 // Default in seconds
+ if (retryAfterHeader) {
+ const parsedSeconds = parseInt(retryAfterHeader, 10)
+ if (Number.isNaN(parsedSeconds)) {
+ const retryDate = new Date(retryAfterHeader)
+ if (!Number.isNaN(retryDate.getTime())) {
+ retryAfter = Math.max(0, Math.ceil((retryDate.getTime() - Date.now()) / 1000))
+ }
+ } else {
+ retryAfter = parsedSeconds
+ }
+ }
+ navigate('/rate-limited', { state: { retryAfter } })
+ } else if (err?.response?.status === 403) {
navigate('/unauthorized')
} else if (err?.response?.status === 401) {
if (ErrorMessage.INVALID_MISSING_TOKEN === err?.response?.data?.message) {
@@ -44,7 +62,9 @@ export const ErrorProvider = ({ children }) => {
value={{
error,
setError,
- handleError
+ handleError,
+ authRateLimitError,
+ setAuthRateLimitError
}}
>
{children}
diff --git a/packages/ui/src/views/auth/forgotPassword.jsx b/packages/ui/src/views/auth/forgotPassword.jsx
index 9e17f2436..7e375a125 100644
--- a/packages/ui/src/views/auth/forgotPassword.jsx
+++ b/packages/ui/src/views/auth/forgotPassword.jsx
@@ -16,6 +16,7 @@ import accountApi from '@/api/account.api'
// Hooks
import useApi from '@/hooks/useApi'
import { useConfig } from '@/store/context/ConfigContext'
+import { useError } from '@/store/context/ErrorContext'
// utils
import useNotifier from '@/utils/useNotifier'
@@ -41,10 +42,13 @@ const ForgotPasswordPage = () => {
const [isLoading, setLoading] = useState(false)
const [responseMsg, setResponseMsg] = useState(undefined)
+ const { authRateLimitError, setAuthRateLimitError } = useError()
+
const forgotPasswordApi = useApi(accountApi.forgotPassword)
const sendResetRequest = async (event) => {
event.preventDefault()
+ setAuthRateLimitError(null)
const body = {
user: {
email: usernameVal
@@ -54,6 +58,11 @@ const ForgotPasswordPage = () => {
await forgotPasswordApi.request(body)
}
+ useEffect(() => {
+ setAuthRateLimitError(null)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [setAuthRateLimitError])
+
useEffect(() => {
if (forgotPasswordApi.error) {
const errMessage =
@@ -89,6 +98,11 @@ const ForgotPasswordPage = () => {
{responseMsg.msg}
)}
+ {authRateLimitError && (
+ } variant='filled' severity='error'>
+ {authRateLimitError}
+
+ )}
{responseMsg && responseMsg?.type !== 'error' && (
} variant='filled' severity='success'>
{responseMsg.msg}
diff --git a/packages/ui/src/views/auth/rateLimited.jsx b/packages/ui/src/views/auth/rateLimited.jsx
new file mode 100644
index 000000000..44b8a85dd
--- /dev/null
+++ b/packages/ui/src/views/auth/rateLimited.jsx
@@ -0,0 +1,51 @@
+import { Box, Button, Stack, Typography } from '@mui/material'
+import { Link, useLocation } from 'react-router-dom'
+import unauthorizedSVG from '@/assets/images/unauthorized.svg'
+import MainCard from '@/ui-component/cards/MainCard'
+
+// ==============================|| RateLimitedPage ||============================== //
+
+const RateLimitedPage = () => {
+ const location = useLocation()
+
+ const retryAfter = location.state?.retryAfter || 60
+
+ return (
+
+
+
+
+
+
+
+ 429 Too Many Requests
+
+
+ {`You have made too many requests in a short period of time. Please wait ${retryAfter}s before trying again.`}
+
+
+
+
+
+
+
+ )
+}
+
+export default RateLimitedPage
diff --git a/packages/ui/src/views/auth/register.jsx b/packages/ui/src/views/auth/register.jsx
index 30c18b12e..3273f3402 100644
--- a/packages/ui/src/views/auth/register.jsx
+++ b/packages/ui/src/views/auth/register.jsx
@@ -18,6 +18,7 @@ import ssoApi from '@/api/sso'
// Hooks
import useApi from '@/hooks/useApi'
import { useConfig } from '@/store/context/ConfigContext'
+import { useError } from '@/store/context/ErrorContext'
// utils
import useNotifier from '@/utils/useNotifier'
@@ -111,7 +112,9 @@ const RegisterPage = () => {
const [loading, setLoading] = useState(false)
const [authError, setAuthError] = useState('')
- const [successMsg, setSuccessMsg] = useState(undefined)
+ const [successMsg, setSuccessMsg] = useState('')
+
+ const { authRateLimitError, setAuthRateLimitError } = useError()
const registerApi = useApi(accountApi.registerAccount)
const ssoLoginApi = useApi(ssoApi.ssoLogin)
@@ -120,6 +123,7 @@ const RegisterPage = () => {
const register = async (event) => {
event.preventDefault()
+ setAuthRateLimitError(null)
if (isEnterpriseLicensed) {
const result = RegisterEnterpriseUserSchema.safeParse({
username,
@@ -192,6 +196,7 @@ const RegisterPage = () => {
}, [registerApi.error])
useEffect(() => {
+ setAuthRateLimitError(null)
if (!isOpenSource) {
getDefaultProvidersApi.request()
}
@@ -274,6 +279,11 @@ const RegisterPage = () => {
)}
)}
+ {authRateLimitError && (
+ } variant='filled' severity='error'>
+ {authRateLimitError}
+
+ )}
{successMsg && (
} variant='filled' severity='success'>
{successMsg}
diff --git a/packages/ui/src/views/auth/resetPassword.jsx b/packages/ui/src/views/auth/resetPassword.jsx
index 3ca33f8cd..f451b8a20 100644
--- a/packages/ui/src/views/auth/resetPassword.jsx
+++ b/packages/ui/src/views/auth/resetPassword.jsx
@@ -1,4 +1,4 @@
-import { useState } from 'react'
+import { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
@@ -19,6 +19,9 @@ import accountApi from '@/api/account.api'
import useNotifier from '@/utils/useNotifier'
import { validatePassword } from '@/utils/validation'
+// Hooks
+import { useError } from '@/store/context/ErrorContext'
+
// Icons
import { IconExclamationCircle, IconX } from '@tabler/icons-react'
@@ -70,6 +73,8 @@ const ResetPasswordPage = () => {
const [loading, setLoading] = useState(false)
const [authErrors, setAuthErrors] = useState([])
+ const { authRateLimitError, setAuthRateLimitError } = useError()
+
const goLogin = () => {
navigate('/signin', { replace: true })
}
@@ -78,6 +83,7 @@ const ResetPasswordPage = () => {
event.preventDefault()
const validationErrors = []
setAuthErrors([])
+ setAuthRateLimitError(null)
if (!tokenVal) {
validationErrors.push('Token cannot be left blank!')
}
@@ -142,6 +148,11 @@ const ResetPasswordPage = () => {
}
}
+ useEffect(() => {
+ setAuthRateLimitError(null)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
return (
<>
@@ -155,6 +166,11 @@ const ResetPasswordPage = () => {
)}
+ {authRateLimitError && (
+ } variant='filled' severity='error'>
+ {authRateLimitError}
+
+ )}
Reset Password
diff --git a/packages/ui/src/views/auth/signIn.jsx b/packages/ui/src/views/auth/signIn.jsx
index 1e0a7d3cf..f097cd425 100644
--- a/packages/ui/src/views/auth/signIn.jsx
+++ b/packages/ui/src/views/auth/signIn.jsx
@@ -14,6 +14,7 @@ import { Input } from '@/ui-component/input/Input'
// Hooks
import useApi from '@/hooks/useApi'
import { useConfig } from '@/store/context/ConfigContext'
+import { useError } from '@/store/context/ErrorContext'
// API
import authApi from '@/api/auth'
@@ -62,6 +63,8 @@ const SignInPage = () => {
const [showResendButton, setShowResendButton] = useState(false)
const [successMessage, setSuccessMessage] = useState('')
+ const { authRateLimitError, setAuthRateLimitError } = useError()
+
const loginApi = useApi(authApi.login)
const ssoLoginApi = useApi(ssoApi.ssoLogin)
const getDefaultProvidersApi = useApi(loginMethodApi.getDefaultLoginMethods)
@@ -71,6 +74,7 @@ const SignInPage = () => {
const doLogin = (event) => {
event.preventDefault()
+ setAuthRateLimitError(null)
setLoading(true)
const body = {
email: usernameVal,
@@ -92,11 +96,12 @@ const SignInPage = () => {
useEffect(() => {
store.dispatch(logoutSuccess())
+ setAuthRateLimitError(null)
if (!isOpenSource) {
getDefaultProvidersApi.request()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
+ }, [setAuthRateLimitError, isOpenSource])
useEffect(() => {
// Parse the "user" query parameter from the URL
@@ -179,6 +184,11 @@ const SignInPage = () => {
{successMessage}
)}
+ {authRateLimitError && (
+ } variant='filled' severity='error'>
+ {authRateLimitError}
+
+ )}
{authError && (
} variant='filled' severity='error'>
{authError}