ratelimit issues

This commit is contained in:
Atridad Lahiji 2023-07-12 12:39:56 -06:00 committed by atridadl
parent 5bd15a2fe7
commit ada7ec0133
No known key found for this signature in database
5 changed files with 116 additions and 142 deletions

View file

@ -5,8 +5,6 @@ DATABASE_URL=""
UPSTASH_REDIS_REST_URL="" UPSTASH_REDIS_REST_URL=""
UPSTASH_REDIS_REST_TOKEN="" UPSTASH_REDIS_REST_TOKEN=""
UPSTASH_REDIS_EXPIRY_SECONDS="" UPSTASH_REDIS_EXPIRY_SECONDS=""
UPSTASH_RATELIMIT_REQUESTS=""
UPSTASH_RATELIMIT_SECONDS=""
#Next Auth Core #Next Auth Core
NEXTAUTH_SECRET="" NEXTAUTH_SECRET=""

View file

@ -21,7 +21,6 @@
"@trpc/next": "10.34.0", "@trpc/next": "10.34.0",
"@trpc/react-query": "10.34.0", "@trpc/react-query": "10.34.0",
"@trpc/server": "10.34.0", "@trpc/server": "10.34.0",
"@upstash/ratelimit": "^0.4.3",
"@upstash/redis": "^1.22.0", "@upstash/redis": "^1.22.0",
"ably": "^1.2.41", "ably": "^1.2.41",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
@ -55,4 +54,4 @@
"ct3aMetadata": { "ct3aMetadata": {
"initVersion": "7.5.9" "initVersion": "7.5.9"
} }
} }

View file

@ -9,8 +9,6 @@ const server = z.object({
UPSTASH_REDIS_REST_URL: z.string().url(), UPSTASH_REDIS_REST_URL: z.string().url(),
UPSTASH_REDIS_REST_TOKEN: z.string(), UPSTASH_REDIS_REST_TOKEN: z.string(),
UPSTASH_REDIS_EXPIRY_SECONDS: z.string(), UPSTASH_REDIS_EXPIRY_SECONDS: z.string(),
UPSTASH_RATELIMIT_REQUESTS: z.string(),
UPSTASH_RATELIMIT_SECONDS: z.string(),
NODE_ENV: z.enum(["development", "test", "production"]), NODE_ENV: z.enum(["development", "test", "production"]),
NEXTAUTH_SECRET: NEXTAUTH_SECRET:
process.env.NODE_ENV === "production" process.env.NODE_ENV === "production"
@ -52,8 +50,6 @@ const processEnv = {
UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL,
UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN, UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN,
UPSTASH_REDIS_EXPIRY_SECONDS: process.env.UPSTASH_REDIS_EXPIRY_SECONDS, UPSTASH_REDIS_EXPIRY_SECONDS: process.env.UPSTASH_REDIS_EXPIRY_SECONDS,
UPSTASH_RATELIMIT_REQUESTS: process.env.UPSTASH_RATELIMIT_REQUESTS,
UPSTASH_RATELIMIT_SECONDS: process.env.UPSTASH_RATELIMIT_SECONDS,
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL, NEXTAUTH_URL: process.env.NEXTAUTH_URL,

View file

@ -3,6 +3,7 @@ import Head from "next/head";
import Image from "next/image"; import Image from "next/image";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { import {
IoCheckmarkCircleOutline, IoCheckmarkCircleOutline,
@ -25,7 +26,6 @@ import { RiVipCrownFill } from "react-icons/ri";
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
import { downloadCSV } from "~/utils/helpers"; import { downloadCSV } from "~/utils/helpers";
import type { PresenceItem } from "~/utils/types"; import type { PresenceItem } from "~/utils/types";
import { Session } from "next-auth";
export const getServerSideProps: GetServerSideProps = async (ctx) => { export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx); const session = await getServerAuthSession(ctx);
@ -46,7 +46,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
}; };
}; };
const Room: NextPage<{ session: Session }> = ({ session }) => { const Room: NextPage = () => {
return ( return (
<> <>
<Head> <Head>
@ -55,7 +55,7 @@ const Room: NextPage<{ session: Session }> = ({ session }) => {
<meta http-equiv="Cache-control" content="no-cache" /> <meta http-equiv="Cache-control" content="no-cache" />
</Head> </Head>
<div className="flex flex-col items-center justify-center text-center gap-2"> <div className="flex flex-col items-center justify-center text-center gap-2">
<RoomBody session={session} /> <RoomBody />
</div> </div>
</> </>
); );
@ -63,7 +63,8 @@ const Room: NextPage<{ session: Session }> = ({ session }) => {
export default Room; export default Room;
const RoomBody: React.FC<{ session: Session }> = ({ session }) => { const RoomBody: React.FC = ({}) => {
const { data: sessionData } = useSession();
const { query } = useRouter(); const { query } = useRouter();
const roomId = z.string().parse(query.id); const roomId = z.string().parse(query.id);
@ -82,7 +83,7 @@ const RoomBody: React.FC<{ session: Session }> = ({ session }) => {
configureAbly({ configureAbly({
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY, key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
clientId: session.user.id, clientId: sessionData?.user.id,
recover: (_, cb) => { recover: (_, cb) => {
cb(true); cb(true);
}, },
@ -105,10 +106,10 @@ const RoomBody: React.FC<{ session: Session }> = ({ session }) => {
const [presenceData] = usePresence<PresenceItem>( const [presenceData] = usePresence<PresenceItem>(
`${env.NEXT_PUBLIC_APP_ENV}-${roomId}`, `${env.NEXT_PUBLIC_APP_ENV}-${roomId}`,
{ {
name: session.user.name || "", name: sessionData?.user.name || "",
image: session.user.image || "", image: sessionData?.user.image || "",
client_id: session.user.id || "", client_id: sessionData?.user.id || "",
role: session.user.role || "USER", role: sessionData?.user.role || "USER",
} }
); );
@ -125,18 +126,18 @@ const RoomBody: React.FC<{ session: Session }> = ({ session }) => {
// Init story name // Init story name
useEffect(() => { useEffect(() => {
if (session && roomFromDb) { if (sessionData && roomFromDb) {
setStoryNameText(roomFromDb.storyName || ""); setStoryNameText(roomFromDb.storyName || "");
setRoomScale(roomFromDb.scale || "ERROR"); setRoomScale(roomFromDb.scale || "ERROR");
} }
}, [roomFromDb, roomId, session]); }, [roomFromDb, roomId, sessionData]);
// Helper functions // Helper functions
const getVoteForCurrentUser = () => { const getVoteForCurrentUser = () => {
if (roomFromDb && session) { if (roomFromDb && sessionData) {
return ( return (
votesFromDb && votesFromDb &&
votesFromDb.find((vote) => vote.userId === session.user.id) votesFromDb.find((vote) => vote.userId === sessionData.user.id)
); );
} else { } else {
return null; return null;
@ -353,108 +354,111 @@ const RoomBody: React.FC<{ session: Session }> = ({ session }) => {
</div> </div>
)} )}
{session && !!roomFromDb && roomFromDb.userId === session.user.id && ( {sessionData &&
<> !!roomFromDb &&
<div className="card card-compact bg-neutral shadow-xl mx-auto m-4"> roomFromDb.userId === sessionData.user.id && (
<div className="card-body flex flex-col flex-wrap"> <>
<h2 className="card-title mx-auto">Room Settings</h2> <div className="card card-compact bg-neutral shadow-xl mx-auto m-4">
<div className="card-body flex flex-col flex-wrap">
<h2 className="card-title mx-auto">Room Settings</h2>
<label className="label mx-auto"> <label className="label mx-auto">
{"Vote Scale (Comma Separated):"}{" "} {"Vote Scale (Comma Separated):"}{" "}
</label> </label>
<input <input
type="text" type="text"
placeholder="Scale (Comma Separated)" placeholder="Scale (Comma Separated)"
className="input input-bordered m-auto" className="input input-bordered m-auto"
value={roomScale} value={roomScale}
onChange={(event) => { onChange={(event) => {
setRoomScale(event.target.value); setRoomScale(event.target.value);
}} }}
/> />
<label className="label mx-auto">{"Story Name:"} </label> <label className="label mx-auto">{"Story Name:"} </label>
<input <input
type="text" type="text"
placeholder="Story Name" placeholder="Story Name"
className="input input-bordered m-auto" className="input input-bordered m-auto"
value={storyNameText} value={storyNameText}
onChange={(event) => { onChange={(event) => {
setStoryNameText(event.target.value); setStoryNameText(event.target.value);
}} }}
/> />
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2"> <div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
<div> <div>
<button <button
onClick={() => saveRoom(!roomFromDb.visible, false)} onClick={() => saveRoom(!roomFromDb.visible, false)}
className="btn btn-primary inline-flex" className="btn btn-primary inline-flex"
> >
{roomFromDb.visible ? ( {roomFromDb.visible ? (
<>
<IoEyeOffOutline className="text-xl mr-1" />
Hide
</>
) : (
<>
<IoEyeOutline className="text-xl mr-1" />
Show
</>
)}
</button>
</div>
<div>
<button
onClick={() =>
saveRoom(
false,
true,
roomFromDb.storyName === storyNameText ||
votesFromDb?.length === 0
? false
: true
)
}
className="btn btn-primary inline-flex"
disabled={
[...new Set(roomScale.split(","))].filter(
(item) => item !== ""
).length <= 1
}
>
{roomFromDb.storyName === storyNameText ||
votesFromDb?.length === 0 ? (
<>
<IoReloadOutline className="text-xl mr-1" /> Reset
</>
) : (
<>
<IoSaveOutline className="text-xl mr-1" /> Save
</>
)}
</button>
</div>
{votesFromDb &&
(roomFromDb.logs.length > 0 || votesFromDb.length > 0) && (
<div>
<button
onClick={() => downloadLogs()}
className="btn btn-primary inline-flex hover:animate-pulse"
>
<> <>
<IoDownloadOutline className="text-xl" /> <IoEyeOffOutline className="text-xl mr-1" />
Hide
</> </>
</button> ) : (
</div> <>
)} <IoEyeOutline className="text-xl mr-1" />
Show
</>
)}
</button>
</div>
<div>
<button
onClick={() =>
saveRoom(
false,
true,
roomFromDb.storyName === storyNameText ||
votesFromDb?.length === 0
? false
: true
)
}
className="btn btn-primary inline-flex"
disabled={
[...new Set(roomScale.split(","))].filter(
(item) => item !== ""
).length <= 1
}
>
{roomFromDb.storyName === storyNameText ||
votesFromDb?.length === 0 ? (
<>
<IoReloadOutline className="text-xl mr-1" /> Reset
</>
) : (
<>
<IoSaveOutline className="text-xl mr-1" /> Save
</>
)}
</button>
</div>
{votesFromDb &&
(roomFromDb.logs.length > 0 ||
votesFromDb.length > 0) && (
<div>
<button
onClick={() => downloadLogs()}
className="btn btn-primary inline-flex hover:animate-pulse"
>
<>
<IoDownloadOutline className="text-xl" />
</>
</button>
</div>
)}
</div>
</div> </div>
</div> </div>
</div> </>
</> )}
)}
</span> </span>
); );
// Room does not exist // Room does not exist

View file

@ -65,7 +65,6 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
* This is where the tRPC API is initialized, connecting the context and transformer. * This is where the tRPC API is initialized, connecting the context and transformer.
*/ */
import { initTRPC, TRPCError } from "@trpc/server"; import { initTRPC, TRPCError } from "@trpc/server";
import { Ratelimit } from "@upstash/ratelimit";
import superjson from "superjson"; import superjson from "superjson";
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
@ -106,33 +105,11 @@ const enforceRouteProtection = t.middleware(async ({ ctx, next }) => {
throw new TRPCError({ code: "UNAUTHORIZED" }); throw new TRPCError({ code: "UNAUTHORIZED" });
} }
try { return next({
const rateLimit = new Ratelimit({ ctx: {
redis: Redis.fromEnv(), session: { ...ctx.session, user: ctx.session.user },
limiter: Ratelimit.slidingWindow( },
Number(env.UPSTASH_RATELIMIT_REQUESTS), });
`${Number(env.UPSTASH_RATELIMIT_SECONDS)}s`
),
analytics: true,
});
const { success } = await rateLimit.limit(
`${env.APP_ENV}_${ctx.session.user.id}`
);
if (!success) throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
return next({
ctx: {
session: { ...ctx.session, user: ctx.session.user },
},
});
} catch {
return next({
ctx: {
session: { ...ctx.session, user: ctx.session.user },
},
});
}
}); });
/** /**