mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-08 21:51:49 +02:00
🔒️ Rate limit registration endpoint (#1153)
This commit is contained in:
parent
05d1e56805
commit
491af5c71b
9 changed files with 75 additions and 9 deletions
|
@ -36,6 +36,8 @@
|
||||||
"@trpc/client": "^10.13.0",
|
"@trpc/client": "^10.13.0",
|
||||||
"@trpc/next": "^10.13.0",
|
"@trpc/next": "^10.13.0",
|
||||||
"@trpc/react-query": "^10.13.0",
|
"@trpc/react-query": "^10.13.0",
|
||||||
|
"@upstash/ratelimit": "^1.2.1",
|
||||||
|
"@vercel/kv": "^2.0.0",
|
||||||
"@vercel/functions": "^1.0.2",
|
"@vercel/functions": "^1.0.2",
|
||||||
"accept-language-parser": "^1.5.0",
|
"accept-language-parser": "^1.5.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { createTRPCContext } from "@rallly/backend/trpc/context";
|
||||||
import { AppRouter, appRouter } from "@rallly/backend/trpc/routers";
|
import { AppRouter, appRouter } from "@rallly/backend/trpc/routers";
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
import { createNextApiHandler } from "@trpc/server/adapters/next";
|
import { createNextApiHandler } from "@trpc/server/adapters/next";
|
||||||
|
import { Ratelimit } from "@upstash/ratelimit";
|
||||||
|
import { kv } from "@vercel/kv";
|
||||||
|
|
||||||
import { posthog, posthogApiHandler } from "@/app/posthog";
|
import { posthog, posthogApiHandler } from "@/app/posthog";
|
||||||
import { absoluteUrl, shortUrl } from "@/utils/absolute-url";
|
import { absoluteUrl, shortUrl } from "@/utils/absolute-url";
|
||||||
|
@ -10,6 +12,12 @@ import { isSelfHosted } from "@/utils/constants";
|
||||||
import { emailClient } from "@/utils/emails";
|
import { emailClient } from "@/utils/emails";
|
||||||
import { composeApiHandlers } from "@/utils/next";
|
import { composeApiHandlers } from "@/utils/next";
|
||||||
|
|
||||||
|
const ratelimit = new Ratelimit({
|
||||||
|
redis: kv,
|
||||||
|
// 5 requests from the same user in 10 seconds
|
||||||
|
limiter: Ratelimit.slidingWindow(5, "10 s"),
|
||||||
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
externalResolver: true,
|
externalResolver: true,
|
||||||
|
@ -38,6 +46,12 @@ const trpcApiHandler = createNextApiHandler<AppRouter>({
|
||||||
isEmailBlocked,
|
isEmailBlocked,
|
||||||
absoluteUrl,
|
absoluteUrl,
|
||||||
shortUrl,
|
shortUrl,
|
||||||
|
ratelimit: async (key: string) => {
|
||||||
|
if (!process.env.KV_REST_API_URL) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
return ratelimit.limit(key);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|
|
@ -21,6 +21,7 @@ export interface TRPCContextParams {
|
||||||
*/
|
*/
|
||||||
absoluteUrl: (path?: string) => string;
|
absoluteUrl: (path?: string) => string;
|
||||||
shortUrl: (path?: string) => string;
|
shortUrl: (path?: string) => string;
|
||||||
|
ratelimit: (key: string) => Promise<{ success: boolean }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createTRPCContext = async (
|
export const createTRPCContext = async (
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createToken, decryptToken } from "../../session";
|
import { createToken, decryptToken } from "../../session";
|
||||||
|
@ -23,6 +24,16 @@ export const auth = router({
|
||||||
| { ok: true; token: string }
|
| { ok: true; token: string }
|
||||||
| { ok: false; reason: "userAlreadyExists" | "emailNotAllowed" }
|
| { ok: false; reason: "userAlreadyExists" | "emailNotAllowed" }
|
||||||
> => {
|
> => {
|
||||||
|
if (process.env.KV_REST_API_URL) {
|
||||||
|
const { success } = await ctx.ratelimit(ctx.user.id);
|
||||||
|
if (!success) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "TOO_MANY_REQUESTS",
|
||||||
|
message: "Too many requests",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ctx.isEmailBlocked?.(input.email)) {
|
if (ctx.isEmailBlocked?.(input.email)) {
|
||||||
return { ok: false, reason: "emailNotAllowed" };
|
return { ok: false, reason: "emailNotAllowed" };
|
||||||
}
|
}
|
||||||
|
@ -50,10 +61,9 @@ export const auth = router({
|
||||||
|
|
||||||
await ctx.emailClient.sendTemplate("RegisterEmail", {
|
await ctx.emailClient.sendTemplate("RegisterEmail", {
|
||||||
to: input.email,
|
to: input.email,
|
||||||
subject: `${input.name}, please verify your email address`,
|
subject: "Please verify your email address",
|
||||||
props: {
|
props: {
|
||||||
code,
|
code,
|
||||||
name: input.name,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,14 @@ export const participants = router({
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input: { pollId, votes, name, email } }) => {
|
.mutation(async ({ ctx, input: { pollId, votes, name, email } }) => {
|
||||||
|
const { success } = await ctx.ratelimit(ctx.user.id);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "TOO_MANY_REQUESTS",
|
||||||
|
message: "You are doing that too much",
|
||||||
|
});
|
||||||
|
}
|
||||||
const { user } = ctx;
|
const { user } = ctx;
|
||||||
|
|
||||||
const poll = await prisma.poll.findUnique({
|
const poll = await prisma.poll.findUnique({
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { fontFamily, Section, Text } from "./styled-components";
|
||||||
|
|
||||||
export interface EmailLayoutProps {
|
export interface EmailLayoutProps {
|
||||||
preview: string;
|
preview: string;
|
||||||
recipientName: string;
|
recipientName?: string;
|
||||||
footNote?: React.ReactNode;
|
footNote?: React.ReactNode;
|
||||||
ctx: EmailContext;
|
ctx: EmailContext;
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ const linkStyles = {
|
||||||
|
|
||||||
export const EmailLayout = ({
|
export const EmailLayout = ({
|
||||||
preview,
|
preview,
|
||||||
recipientName = "Guest",
|
recipientName,
|
||||||
children,
|
children,
|
||||||
footNote,
|
footNote,
|
||||||
ctx,
|
ctx,
|
||||||
|
@ -60,7 +60,7 @@ export const EmailLayout = ({
|
||||||
<Container style={containerStyles}>
|
<Container style={containerStyles}>
|
||||||
<Img src={logoUrl} alt="Rallly" width={128} />
|
<Img src={logoUrl} alt="Rallly" width={128} />
|
||||||
<Section style={sectionStyles}>
|
<Section style={sectionStyles}>
|
||||||
<Text>Hi {recipientName},</Text>
|
{recipientName ? <Text>Hi {recipientName},</Text> : null}
|
||||||
{children}
|
{children}
|
||||||
{footNote ? (
|
{footNote ? (
|
||||||
<Text
|
<Text
|
||||||
|
|
|
@ -8,13 +8,11 @@ import {
|
||||||
} from "./_components/styled-components";
|
} from "./_components/styled-components";
|
||||||
|
|
||||||
interface RegisterEmailProps {
|
interface RegisterEmailProps {
|
||||||
name: string;
|
|
||||||
code: string;
|
code: string;
|
||||||
ctx: EmailContext;
|
ctx: EmailContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RegisterEmail = ({
|
export const RegisterEmail = ({
|
||||||
name = "John",
|
|
||||||
code = "123456",
|
code = "123456",
|
||||||
ctx = defaultEmailContext,
|
ctx = defaultEmailContext,
|
||||||
}: RegisterEmailProps) => {
|
}: RegisterEmailProps) => {
|
||||||
|
@ -28,7 +26,6 @@ export const RegisterEmail = ({
|
||||||
please ignore this email.
|
please ignore this email.
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
recipientName={name}
|
|
||||||
preview={`Your 6-digit code is: ${code}`}
|
preview={`Your 6-digit code is: ${code}`}
|
||||||
>
|
>
|
||||||
<Text>
|
<Text>
|
||||||
|
|
|
@ -119,6 +119,7 @@
|
||||||
"STRIPE_SECRET_KEY",
|
"STRIPE_SECRET_KEY",
|
||||||
"STRIPE_SIGNING_SECRET",
|
"STRIPE_SIGNING_SECRET",
|
||||||
"STRIPE_YEARLY_PRICE",
|
"STRIPE_YEARLY_PRICE",
|
||||||
"SUPPORT_EMAIL"
|
"SUPPORT_EMAIL",
|
||||||
|
"KV_REST_API_URL"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
33
yarn.lock
33
yarn.lock
|
@ -4715,6 +4715,27 @@
|
||||||
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
|
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
|
||||||
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
|
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
|
||||||
|
|
||||||
|
"@upstash/core-analytics@^0.0.9":
|
||||||
|
version "0.0.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@upstash/core-analytics/-/core-analytics-0.0.9.tgz#59f29a920099084e049726c0aca67be1c1ecfc1f"
|
||||||
|
integrity sha512-9NXXxZ5y1/A/zqKLlVT7NsAWSggJfOjB0hG6Ffx29b4jbzHOiQVWB55h5+j2clT9Ib+mNPXn0iB5zN3aWLkICw==
|
||||||
|
dependencies:
|
||||||
|
"@upstash/redis" "^1.28.3"
|
||||||
|
|
||||||
|
"@upstash/ratelimit@^1.2.1":
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@upstash/ratelimit/-/ratelimit-1.2.1.tgz#835a33ce715e999d646431f70a71a69de7d439ee"
|
||||||
|
integrity sha512-o01lV1yFS5Fzj5KONZmNyVch/Qrlj785B2ob+kStUmxn8F6xXk7IHTQqVcHE+Ce3CmT/qQIwvMxDZftyJ5wYpQ==
|
||||||
|
dependencies:
|
||||||
|
"@upstash/core-analytics" "^0.0.9"
|
||||||
|
|
||||||
|
"@upstash/redis@^1.28.3", "@upstash/redis@^1.31.3":
|
||||||
|
version "1.31.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@upstash/redis/-/redis-1.31.5.tgz#8d5fe439a2a28638b3a354a23680ecf7f7eb4f54"
|
||||||
|
integrity sha512-2MatqeqftroSJ9Q+pqbyGAIwXX6KEPtUTUna2c/fq09h12ffwvltDTgfppeF+NzJo/SyZfHY8e1RoflduMbz1A==
|
||||||
|
dependencies:
|
||||||
|
crypto-js "^4.2.0"
|
||||||
|
|
||||||
"@vercel/analytics@^0.1.8":
|
"@vercel/analytics@^0.1.8":
|
||||||
version "0.1.11"
|
version "0.1.11"
|
||||||
resolved "https://registry.npmjs.org/@vercel/analytics/-/analytics-0.1.11.tgz"
|
resolved "https://registry.npmjs.org/@vercel/analytics/-/analytics-0.1.11.tgz"
|
||||||
|
@ -4725,6 +4746,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@vercel/functions/-/functions-1.0.2.tgz#c26ed4e3b0ed701e28c4ebd71c76b1bfe14db02a"
|
resolved "https://registry.yarnpkg.com/@vercel/functions/-/functions-1.0.2.tgz#c26ed4e3b0ed701e28c4ebd71c76b1bfe14db02a"
|
||||||
integrity sha512-j3udyHOv/05Y8o3WQ/ANMWa1aYagsY5B3ouImiwgYsz5z4CBUHTY5dk74oQAXYr+bgoVDpdDlmxkpnxGzKEdLQ==
|
integrity sha512-j3udyHOv/05Y8o3WQ/ANMWa1aYagsY5B3ouImiwgYsz5z4CBUHTY5dk74oQAXYr+bgoVDpdDlmxkpnxGzKEdLQ==
|
||||||
|
|
||||||
|
"@vercel/kv@^2.0.0":
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vercel/kv/-/kv-2.0.0.tgz#a0baa12563946cb35cee23d638b68f0fbbf76172"
|
||||||
|
integrity sha512-zdVrhbzZBYo5d1Hfn4bKtqCeKf0FuzW8rSHauzQVMUgv1+1JOwof2mWcBuI+YMJy8s0G0oqAUfQ7HgUDzb8EbA==
|
||||||
|
dependencies:
|
||||||
|
"@upstash/redis" "^1.31.3"
|
||||||
|
|
||||||
"@vitest/expect@1.3.1":
|
"@vitest/expect@1.3.1":
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.3.1.tgz#d4c14b89c43a25fd400a6b941f51ba27fe0cb918"
|
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.3.1.tgz#d4c14b89c43a25fd400a6b941f51ba27fe0cb918"
|
||||||
|
@ -5991,6 +6019,11 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||||
shebang-command "^2.0.0"
|
shebang-command "^2.0.0"
|
||||||
which "^2.0.1"
|
which "^2.0.1"
|
||||||
|
|
||||||
|
crypto-js@^4.2.0:
|
||||||
|
version "4.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
|
||||||
|
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
|
||||||
|
|
||||||
crypto@^1.0.1:
|
crypto@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue