From d3eff4f3e8c10e04878670ad3aec675b24ae0842 Mon Sep 17 00:00:00 2001 From: Atridad Lahiji <88056492+atridadl@users.noreply.github.com> Date: Tue, 20 Jun 2023 12:54:48 -0600 Subject: [PATCH] Rate limits! --- package.json | 7 ++- pnpm-lock.yaml | 106 ++++++++++++++++++++------------- src/env.mjs | 4 ++ src/server/api/routers/room.ts | 8 +-- src/server/api/trpc.ts | 23 ++++++- 5 files changed, 93 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 128674c..0a82c39 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,14 @@ "dependencies": { "@ably-labs/react-hooks": "^2.1.1", "@auth/prisma-adapter": "^1.0.0", - "@prisma/client": "4.15.0", + "@prisma/client": "4.16.0", "@react-email/components": "^0.0.7", - "@tanstack/react-query": "^4.29.14", + "@tanstack/react-query": "^4.29.15", "@trpc/client": "10.31.0", "@trpc/next": "10.31.0", "@trpc/react-query": "10.31.0", "@trpc/server": "10.31.0", + "@upstash/ratelimit": "^0.4.3", "@upstash/redis": "^1.21.0", "ably": "^1.2.40", "autoprefixer": "^10.4.14", @@ -47,7 +48,7 @@ "daisyui": "^3.1.1", "eslint": "^8.43.0", "eslint-config-next": "^13.4.6", - "prisma": "4.15.0", + "prisma": "4.16.0", "tailwindcss": "^3.3.2", "typescript": "^5.1.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eac33b0..d616664 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,28 +10,31 @@ dependencies: version: 2.1.1(react-dom@18.2.0)(react@18.2.0) '@auth/prisma-adapter': specifier: ^1.0.0 - version: 1.0.0(@prisma/client@4.15.0) + version: 1.0.0(@prisma/client@4.16.0) '@prisma/client': - specifier: 4.15.0 - version: 4.15.0(prisma@4.15.0) + specifier: 4.16.0 + version: 4.16.0(prisma@4.16.0) '@react-email/components': specifier: ^0.0.7 version: 0.0.7 '@tanstack/react-query': - specifier: ^4.29.14 - version: 4.29.14(react-dom@18.2.0)(react@18.2.0) + specifier: ^4.29.15 + version: 4.29.15(react-dom@18.2.0)(react@18.2.0) '@trpc/client': specifier: 10.31.0 version: 10.31.0(@trpc/server@10.31.0) '@trpc/next': specifier: 10.31.0 - version: 10.31.0(@tanstack/react-query@4.29.14)(@trpc/client@10.31.0)(@trpc/react-query@10.31.0)(@trpc/server@10.31.0)(next@13.4.6)(react-dom@18.2.0)(react@18.2.0) + version: 10.31.0(@tanstack/react-query@4.29.15)(@trpc/client@10.31.0)(@trpc/react-query@10.31.0)(@trpc/server@10.31.0)(next@13.4.6)(react-dom@18.2.0)(react@18.2.0) '@trpc/react-query': specifier: 10.31.0 - version: 10.31.0(@tanstack/react-query@4.29.14)(@trpc/client@10.31.0)(@trpc/server@10.31.0)(react-dom@18.2.0)(react@18.2.0) + version: 10.31.0(@tanstack/react-query@4.29.15)(@trpc/client@10.31.0)(@trpc/server@10.31.0)(react-dom@18.2.0)(react@18.2.0) '@trpc/server': specifier: 10.31.0 version: 10.31.0 + '@upstash/ratelimit': + specifier: ^0.4.3 + version: 0.4.3 '@upstash/redis': specifier: ^1.21.0 version: 1.21.0 @@ -107,8 +110,8 @@ devDependencies: specifier: ^13.4.6 version: 13.4.6(eslint@8.43.0)(typescript@5.1.3) prisma: - specifier: 4.15.0 - version: 4.15.0 + specifier: 4.16.0 + version: 4.16.0 tailwindcss: specifier: ^3.3.2 version: 3.3.2 @@ -159,13 +162,13 @@ packages: preact-render-to-string: 5.2.3(preact@10.11.3) dev: false - /@auth/prisma-adapter@1.0.0(@prisma/client@4.15.0): + /@auth/prisma-adapter@1.0.0(@prisma/client@4.16.0): resolution: {integrity: sha512-+x+s5dgpNmqrcQC2ZRAXZIM6yhkWP/EXjIUgqUyMepLiX1OHi2AXIUAAbXsW4oG9OpYr/rvPIzPBpuGt6sPFwQ==} peerDependencies: '@prisma/client': '>=2.26.0 || >=3 || >=4' dependencies: '@auth/core': 0.8.1 - '@prisma/client': 4.15.0(prisma@4.15.0) + '@prisma/client': 4.16.0(prisma@4.16.0) transitivePeerDependencies: - nodemailer dev: false @@ -763,8 +766,8 @@ packages: tslib: 2.5.3 dev: true - /@prisma/client@4.15.0(prisma@4.15.0): - resolution: {integrity: sha512-xnROvyABcGiwqRNdrObHVZkD9EjkJYHOmVdlKy1yGgI+XOzvMzJ4tRg3dz1pUlsyhKxXGCnjIQjWW+2ur+YXuw==} + /@prisma/client@4.16.0(prisma@4.16.0): + resolution: {integrity: sha512-CBD+5IdZPiavhLkQokvsz1uz4r9ppixaqY/ajybWs4WXNnsDVMBKEqN3BiPzpSo79jiy22VKj/67pqt4VwIg9w==} engines: {node: '>=14.17'} requiresBuild: true peerDependencies: @@ -773,16 +776,16 @@ packages: prisma: optional: true dependencies: - '@prisma/engines-version': 4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944 - prisma: 4.15.0 + '@prisma/engines-version': 4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c + prisma: 4.16.0 dev: false - /@prisma/engines-version@4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944: - resolution: {integrity: sha512-sVOig4tjGxxlYaFcXgE71f/rtFhzyYrfyfNFUsxCIEJyVKU9rdOWIlIwQ2NQ7PntvGnn+x0XuFo4OC1jvPJKzg==} + /@prisma/engines-version@4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c: + resolution: {integrity: sha512-tMWAF/qF00fbUH1HB4Yjmz6bjh7fzkb7Y3NRoUfMlHu6V+O45MGvqwYxqwBjn1BIUXkl3r04W351D4qdJjrgvA==} dev: false - /@prisma/engines@4.15.0: - resolution: {integrity: sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA==} + /@prisma/engines@4.16.0: + resolution: {integrity: sha512-M6XoMRXnqL0rqZGQS8ZpNiHYG4G1fKBdoqW/oBtHnr1in5UYgerZqal3CXchmd6OBD/770PE9dtjQuqcilZJUA==} requiresBuild: true /@radix-ui/react-compose-refs@1.0.0(react@18.2.0): @@ -995,12 +998,12 @@ packages: defer-to-connect: 2.0.1 dev: false - /@tanstack/query-core@4.29.14: - resolution: {integrity: sha512-ElEAahtLWA7Y7c2Haw10KdEf2tx+XZl/Z8dmyWxZehxWK3TPL5qtXtb7kUEhvt/8u2cSP62fLxgh2qqzMMGnDQ==} + /@tanstack/query-core@4.29.15: + resolution: {integrity: sha512-Recc1d5rjHesKhzlH3Aw66v+vQxtB9OHEXP/vxgEcEJ0DwEpfe3EQ4id20vuBJHY2XRjfgWGmUs6ZgK6PSsTXA==} dev: false - /@tanstack/react-query@4.29.14(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-wh4bd/QIy85YgTDBtj/7/9ZkpRX41QdZuUL8xKoSzuMCukXvAE1/oJ4p0F15lqQq9W3g2pgcbr2Aa+CNvqABhg==} + /@tanstack/react-query@4.29.15(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-1zDkv95ljuJ623hhbYU8YIprPW2x6774kh3IQNEuZav62+S+Zr26uUOrE2zGRp9I1uO5Liw/0uYB3dWXQP5+3Q==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -1011,7 +1014,7 @@ packages: react-native: optional: true dependencies: - '@tanstack/query-core': 4.29.14 + '@tanstack/query-core': 4.29.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) use-sync-external-store: 1.2.0(react@18.2.0) @@ -1025,7 +1028,7 @@ packages: '@trpc/server': 10.31.0 dev: false - /@trpc/next@10.31.0(@tanstack/react-query@4.29.14)(@trpc/client@10.31.0)(@trpc/react-query@10.31.0)(@trpc/server@10.31.0)(next@13.4.6)(react-dom@18.2.0)(react@18.2.0): + /@trpc/next@10.31.0(@tanstack/react-query@4.29.15)(@trpc/client@10.31.0)(@trpc/react-query@10.31.0)(@trpc/server@10.31.0)(next@13.4.6)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-BZtZr7UKAs0tUTreCsYhy+/HjFNFl5KBwBS+Li6pCv9GwqCDqpoivesQz7LltO4Y4lOLXLm9tXQXtS1gfmF9yg==} peerDependencies: '@tanstack/react-query': ^4.18.0 @@ -1036,9 +1039,9 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - '@tanstack/react-query': 4.29.14(react-dom@18.2.0)(react@18.2.0) + '@tanstack/react-query': 4.29.15(react-dom@18.2.0)(react@18.2.0) '@trpc/client': 10.31.0(@trpc/server@10.31.0) - '@trpc/react-query': 10.31.0(@tanstack/react-query@4.29.14)(@trpc/client@10.31.0)(@trpc/server@10.31.0)(react-dom@18.2.0)(react@18.2.0) + '@trpc/react-query': 10.31.0(@tanstack/react-query@4.29.15)(@trpc/client@10.31.0)(@trpc/server@10.31.0)(react-dom@18.2.0)(react@18.2.0) '@trpc/server': 10.31.0 next: 13.4.6(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 @@ -1046,7 +1049,7 @@ packages: react-ssr-prepass: 1.5.0(react@18.2.0) dev: false - /@trpc/react-query@10.31.0(@tanstack/react-query@4.29.14)(@trpc/client@10.31.0)(@trpc/server@10.31.0)(react-dom@18.2.0)(react@18.2.0): + /@trpc/react-query@10.31.0(@tanstack/react-query@4.29.15)(@trpc/client@10.31.0)(@trpc/server@10.31.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-+M8sIsbf6e4H5XYvHlzDqhaf+ybfUigA/9OL3wXRp2vXhCedEiIERCnwNuHWFDRASl9vjOcM33AuJ4sbOOINEA==} peerDependencies: '@tanstack/react-query': ^4.18.0 @@ -1055,7 +1058,7 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - '@tanstack/react-query': 4.29.14(react-dom@18.2.0)(react@18.2.0) + '@tanstack/react-query': 4.29.15(react-dom@18.2.0)(react@18.2.0) '@trpc/client': 10.31.0(@trpc/server@10.31.0) '@trpc/server': 10.31.0 react: 18.2.0 @@ -1277,6 +1280,23 @@ packages: eslint-visitor-keys: 3.4.1 dev: true + /@upstash/core-analytics@0.0.6: + resolution: {integrity: sha512-cpPSR0XJAJs4Ddz9nq3tINlPS5aLfWVCqhhtHnXt4p7qr5+/Znlt1Es736poB/9rnl1hAHrOsOvVj46NEXcVqA==} + engines: {node: '>=16.0.0'} + dependencies: + '@upstash/redis': 1.21.0 + transitivePeerDependencies: + - encoding + dev: false + + /@upstash/ratelimit@0.4.3: + resolution: {integrity: sha512-Dsp9Mw09Flg28JRklKgFiCXqr3bqv8bbG0kgpUYoHjcgPPolFFyaYOj/I2HExvYLZiogl77NUavBoNvMOK0zUQ==} + dependencies: + '@upstash/core-analytics': 0.0.6 + transitivePeerDependencies: + - encoding + dev: false + /@upstash/redis@1.21.0: resolution: {integrity: sha512-c6M+cl0LOgGK/7Gp6ooMkIZ1IDAJs8zFR+REPkoSkAq38o7CWFX5FYwYEqGZ6wJpUGBuEOr/7hTmippXGgL25A==} dependencies: @@ -1462,7 +1482,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.21.9 - caniuse-lite: 1.0.30001504 + caniuse-lite: 1.0.30001505 fraction.js: 4.2.0 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -1576,8 +1596,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001504 - electron-to-chromium: 1.4.434 + caniuse-lite: 1.0.30001505 + electron-to-chromium: 1.4.435 node-releases: 2.0.12 update-browserslist-db: 1.0.11(browserslist@4.21.9) dev: false @@ -1647,8 +1667,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - /caniuse-lite@1.0.30001504: - resolution: {integrity: sha512-5uo7eoOp2mKbWyfMXnGO9rJWOGU8duvzEiYITW+wivukL7yHH4gX9yuRaobu6El4jPxo6jKZfG+N6fB621GD/Q==} + /caniuse-lite@1.0.30001505: + resolution: {integrity: sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==} dev: false /chainsaw@0.1.0: @@ -2060,8 +2080,8 @@ packages: sigmund: 1.0.1 dev: false - /electron-to-chromium@1.4.434: - resolution: {integrity: sha512-5Gvm09UZTQRaWrimRtWRO5rvaX6Kpk5WHAPKDa7A4Gj6NIPuJ8w8WNpnxCXdd+CJJt6RBU6tUw0KyULoW6XuHw==} + /electron-to-chromium@1.4.435: + resolution: {integrity: sha512-B0CBWVFhvoQCW/XtjRzgrmqcgVWg6RXOEM/dK59+wFV93BFGR6AeNKc4OyhM+T3IhJaOOG8o/V+33Y2mwJWtzw==} dev: false /emoji-regex@9.2.2: @@ -3615,7 +3635,7 @@ packages: '@next/env': 13.4.6 '@swc/helpers': 0.5.1 busboy: 1.6.0 - caniuse-lite: 1.0.30001504 + caniuse-lite: 1.0.30001505 postcss: 8.4.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -3958,8 +3978,8 @@ packages: engines: {node: '>=6'} dev: false - /pirates@4.0.5: - resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} dev: true @@ -4176,13 +4196,13 @@ packages: js-beautify: 1.14.8 dev: false - /prisma@4.15.0: - resolution: {integrity: sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA==} + /prisma@4.16.0: + resolution: {integrity: sha512-kSCwbTm3LCephyGfZMJYqBXpPJXdJStg5xwfzeFmR5C05zfkOURK9pQpJF6uUQvFWm3lI9ZMSNkObmFkAPnB+g==} engines: {node: '>=14.17'} hasBin: true requiresBuild: true dependencies: - '@prisma/engines': 4.15.0 + '@prisma/engines': 4.16.0 /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -4737,7 +4757,7 @@ packages: glob: 7.1.6 lines-and-columns: 1.2.4 mz: 2.7.0 - pirates: 4.0.5 + pirates: 4.0.6 ts-interface-checker: 0.1.13 dev: true diff --git a/src/env.mjs b/src/env.mjs index 5067a1c..d09bbf1 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -8,6 +8,8 @@ const server = z.object({ DATABASE_URL: z.string().url(), UPSTASH_REDIS_REST_URL: z.string().url(), UPSTASH_REDIS_REST_TOKEN: z.string(), + UPSTASH_RATELIMIT_REQUESTS: z.string(), + UPSTASH_RATELIMIT_SECONDS: z.string(), NODE_ENV: z.enum(["development", "test", "production"]), NEXTAUTH_SECRET: process.env.NODE_ENV === "production" @@ -67,6 +69,8 @@ const processEnv = { DATABASE_URL: process.env.DATABASE_URL, UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN, + UPSTASH_RATELIMIT_REQUESTS: process.env.UPSTASH_RATELIMIT_REQUESTS, + UPSTASH_RATELIMIT_SECONDS: process.env.UPSTASH_RATELIMIT_SECONDS, NODE_ENV: process.env.NODE_ENV, NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, NEXTAUTH_URL: process.env.NEXTAUTH_URL, diff --git a/src/server/api/routers/room.ts b/src/server/api/routers/room.ts index 553927c..4c0f69a 100644 --- a/src/server/api/routers/room.ts +++ b/src/server/api/routers/room.ts @@ -1,18 +1,14 @@ import { z } from "zod"; import { publishToChannel } from "~/server/ably"; -import { - createTRPCRouter, - publicProcedure, - protectedProcedure, -} from "~/server/api/trpc"; +import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { env } from "~/env.mjs"; import { redis } from "~/server/redis"; export const roomRouter = createTRPCRouter({ // Create - create: publicProcedure + create: protectedProcedure .input( z.object({ name: z.string(), diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index 03e3d2e..cc1c135 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -20,6 +20,15 @@ import { type Session } from "next-auth"; import { getServerAuthSession } from "~/server/auth"; import { prisma } from "~/server/db"; +const rateLimit = new Ratelimit({ + redis, + limiter: Ratelimit.slidingWindow( + Number(env.UPSTASH_RATELIMIT_REQUESTS), + `${Number(env.UPSTASH_RATELIMIT_SECONDS)}s` + ), + analytics: true, +}); + type CreateContextOptions = { session: Session | null; }; @@ -65,6 +74,9 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => { */ import { initTRPC, TRPCError } from "@trpc/server"; import superjson from "superjson"; +import { Ratelimit } from "@upstash/ratelimit"; +import { redis } from "../redis"; +import { env } from "~/env.mjs"; const t = initTRPC.context().create({ transformer: superjson, @@ -97,13 +109,18 @@ export const createTRPCRouter = t.router; export const publicProcedure = t.procedure; /** Reusable middleware that enforces users are logged in before running the procedure. */ -const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { +const enforceRouteProtection = t.middleware(async ({ ctx, next }) => { + // Auth if (!ctx.session || !ctx.session.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } + const { success } = await rateLimit.limit( + `${env.APP_ENV}_${ctx.session.user.id}` + ); + if (!success) throw new TRPCError({ code: "TOO_MANY_REQUESTS" }); + return next({ ctx: { - // infers the `session` as non-nullable session: { ...ctx.session, user: ctx.session.user }, }, }); @@ -117,4 +134,4 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { * * @see https://trpc.io/docs/procedures */ -export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); +export const protectedProcedure = t.procedure.use(enforceRouteProtection);