diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index e8b990e0fa..97b6925ea3 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -12,7 +12,7 @@ import "react-phone-number-input/style.css"; import { SENDER_ID } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; -import { trpc } from "@calcom/trpc/react"; +import { trpc, TRPCClientError } from "@calcom/trpc/react"; import { Button, Checkbox, @@ -134,10 +134,16 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { showToast(t("notification_sent"), "success"); }, onError: (err) => { - if (err instanceof HttpError) { - const message = `${err.statusCode}: ${err.message}`; - showToast(message, "error"); + let message = t("unexpected_error_try_again"); + if (err instanceof TRPCClientError) { + if (err.message === "rate-limit-exceeded") { + message = t("rate_limit_exceeded"); + } } + if (err instanceof HttpError) { + message = `${err.statusCode}: ${err.message}`; + } + showToast(message, "error"); }, }); diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 295afe17ac..1a970bf546 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -31,7 +31,7 @@ import { getErrorFromUnknown } from "@calcom/lib/errors"; import { TRPCError } from "@trpc/server"; -import { router, authedProcedure } from "../../trpc"; +import { router, authedProcedure, authedRateLimitedProcedure } from "../../trpc"; export const workflowsRouter = router({ list: authedProcedure.query(async ({ ctx }) => { @@ -801,7 +801,7 @@ export const workflowsRouter = router({ workflow, }; }), - testAction: authedProcedure + testAction: authedRateLimitedProcedure({ intervalInMs: 10000, limit: 3 }) .input( z.object({ action: z.enum(WORKFLOW_ACTIONS), diff --git a/packages/trpc/server/trpc.ts b/packages/trpc/server/trpc.ts index dafa1da90b..cd2317c8b1 100644 --- a/packages/trpc/server/trpc.ts +++ b/packages/trpc/server/trpc.ts @@ -1,5 +1,7 @@ import superjson from "superjson"; +import rateLimit from "@calcom/lib/rateLimit"; + import { initTRPC, TRPCError } from "@trpc/server"; import { Context } from "./createContext"; @@ -41,10 +43,41 @@ const isAdminMiddleware = t.middleware(({ ctx, next }) => { }, }); }); +interface IRateLimitOptions { + intervalInMs: number; + limit: number; +} +const isRateLimitedByUserIdMiddleware = ({ intervalInMs, limit }: IRateLimitOptions) => + t.middleware(({ ctx, next }) => { + // validate user exists + if (!ctx.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + const { isRateLimited } = rateLimit({ intervalInMs }).check(limit, ctx.user.id.toString()); + + if (isRateLimited) { + throw new TRPCError({ code: "TOO_MANY_REQUESTS" }); + } + + return next({ + ctx: { + // infers that `user` and `session` are non-nullable to downstream procedures + session: ctx.session, + user: ctx.user, + }, + }); + }); export const router = t.router; export const mergeRouters = t.mergeRouters; export const middleware = t.middleware; export const publicProcedure = t.procedure.use(perfMiddleware); export const authedProcedure = t.procedure.use(perfMiddleware).use(isAuthedMiddleware); +export const authedRateLimitedProcedure = ({ intervalInMs, limit }: IRateLimitOptions) => + t.procedure + .use(perfMiddleware) + .use(isAuthedMiddleware) + .use(isRateLimitedByUserIdMiddleware({ intervalInMs, limit })); + export const authedAdminProcedure = t.procedure.use(perfMiddleware).use(isAdminMiddleware);