ratelimit issues
This commit is contained in:
parent
dfdd82af04
commit
7417ae4023
5 changed files with 116 additions and 142 deletions
|
@ -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=""
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Add table
Reference in a new issue