Rate limits!

This commit is contained in:
Atridad Lahiji 2023-06-20 12:54:48 -06:00 committed by atridadl
parent 43e36f3ceb
commit d3eff4f3e8
No known key found for this signature in database
5 changed files with 93 additions and 55 deletions

View file

@ -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"
},

106
pnpm-lock.yaml generated
View file

@ -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

View file

@ -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,

View file

@ -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(),

View file

@ -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<typeof createTRPCContext>().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);