-
-
Copyright © {new Date().getFullYear()} Noah Kurrack. All rights reserved.
-
-
-
+
+
+
Copyright © {new Date().getFullYear()} Noah Kurrack. All rights reserved.
- >
+
)
}
diff --git a/src/components/question.tsx b/src/components/question.tsx
index 64236eb..d02e8b1 100644
--- a/src/components/question.tsx
+++ b/src/components/question.tsx
@@ -4,13 +4,16 @@ import { useStopwatch } from "react-timer-hook"
import { api } from "~/utils/api"
import { noRefreshOpts, codeEditorExtensions, codeEditorTheme } from "./constants"
import { SubmissionDisplay } from "./submission"
-import { type PersonalStatusData } from "~/server/api/routers/status"
import toast from "react-hot-toast"
+import { useAuth } from "@clerk/nextjs"
+import { GreenSignInButton } from "./layout"
+import { LoadingSpinner } from "./loading"
+import { type PublicQuestionInfo } from "~/server/helpers/filter"
const Question = ({ questionId }: { questionId: number }) => {
const [isSubmitTimeout, setIsSubmitTimeout] = useState(false)
- const { data, isLoading, isError } = api.question.get.useQuery({ id: questionId }, noRefreshOpts)
- const { mutate: submitQuestion } = api.submission.submit.useMutation({
+ const { data, isLoading, isError } = api.question.getPrivate.useQuery({ id: questionId }, noRefreshOpts)
+ const { mutate: submitQuestion, isLoading: isSubmitLoading } = api.submission.submit.useMutation({
onSuccess: (data) => {
if (data.error) {
toast.error(`Execution error: ${data.execResult.errorMessage ?? 'unknown'}`, {
@@ -19,7 +22,8 @@ const Question = ({ questionId }: { questionId: number }) => {
return
}
toast.success('Your submission was recorded!')
- void utils.status.personal.invalidate()
+ void utils.status.question.invalidate()
+ void utils.question.getInfinite.invalidate()
void utils.submission.invalidate(undefined, {
type: 'all' // refresh queries on other pages
})
@@ -39,18 +43,17 @@ const Question = ({ questionId }: { questionId: number }) => {
if (isError) return
Question could not be loaded. Please refresh the page.
- const initialValue = data.funcSignature + ' {\n \n}'
+ const initialValue = data.signature + ' {\n \n}'
if (code == "") setCode(initialValue)
return (
-
Question #{data.id}: {data.title}
-
{data.questionDescription}
+
{data.description}
{
@@ -90,48 +93,77 @@ const ElapsedTimeCounter = ({ startTime }: { startTime: Date }) => {
)
}
-export const QuestionHandler = (props: PersonalStatusData) => {
+export const QuestionHandler = (questionInfo: PublicQuestionInfo) => {
+ const { isSignedIn } = useAuth()
+
+ if (!isSignedIn) {
+ return (
+
+
Question #{questionInfo.questionNum}: {questionInfo.questionTitle}
+
Sign in to get started!
+
+
+ )
+ }
+
+ return
+}
+
+const QuestionSignedIn = (questionInfo: PublicQuestionInfo) => {
+ const {
+ data: questionStatus,
+ isLoading: isQuestionStatusLoading,
+ isError: isQuestionStatusError
+ } = api.status.question.useQuery({ id: questionInfo.questionId }, noRefreshOpts)
const utils = api.useUtils()
const { mutate: startQuestion, isLoading: isQuestionLoading } = api.question.start.useMutation({
onSuccess: () => {
- void utils.status.personal.invalidate()
+ void utils.status.question.invalidate()
},
onError: (error) => {
toast.error(`An error occured: ${error.message}`)
}
})
- if (props.isCompleted) {
+ if (isQuestionStatusError) return Error
+
+ if (isQuestionStatusLoading) return
+
+ if (questionStatus.isCompleted) {
return (
- <>
- You've already completed today's question!
- {props.submissionId
- ?
+
+
Question #{questionInfo.questionNum}: {questionInfo.questionTitle}
+
You've already completed this question!
+ {questionStatus.submissionId
+ ?
:
There was an error retrieving your submission.
}
- >
+
)
}
- if (props.isStarted) {
+ if (questionStatus.isStarted) {
return (
-
Today's Question
-
- {props.startTime &&
Elapsed time:
}
+
Question #{questionInfo.questionNum}: {questionInfo.questionTitle}
+
+ {questionStatus.startTime &&
Elapsed time:
}
)
}
return (
-
-
Click button to start today's question (#{props.questionId})!
-
-
+ <>
+
+
Question #{questionInfo.questionNum}: {questionInfo.questionTitle}
+
Click the button to start this question:
+
+
+ >
)
}
diff --git a/src/components/submission.tsx b/src/components/submission.tsx
index 7bcbb6a..fd69c66 100644
--- a/src/components/submission.tsx
+++ b/src/components/submission.tsx
@@ -1,36 +1,18 @@
import ReactCodeMirror from "@uiw/react-codemirror";
import { api, type RouterOutputs } from "~/utils/api";
-import { codeEditorExtensions, codeEditorTheme, noRefreshOpts } from "./constants";
-import { useCollapse } from "react-collapsed";
-import { ChevronDownIcon, ChevronRightIcon } from "@radix-ui/react-icons";
+import { codeEditorExtensions, codeEditorTheme, noRefreshOpts, statusColors } from "./constants";
import { LoadingSpinner } from "./loading";
type SubmissionItem = RouterOutputs['submission']['getInfinite']['submissions'][number]
-export const Submission = ({ data, solo: isSolo }: { data: SubmissionItem, solo: boolean }) => {
- // eslint-disable-next-line @typescript-eslint/unbound-method
- const { getCollapseProps, getToggleProps, isExpanded } = useCollapse({
- defaultExpanded: isSolo
- })
-
- const statusColors = {
- ERROR: "text-rose-700",
- TIMEOUT: "text-rose-700",
- INCORRECT: "text-yellow-500",
- CORRECT: "text-emerald-600",
- UNKNOWN: "text-slate-500"
- };
+export const Submission = ({ data }: { data: SubmissionItem }) => {
const statusColor = statusColors[data.runResult] || "";
return (
-
-
-
Question #{data.questionId} Submission ({data.runResult})
- {!isSolo &&
-
- {isExpanded ? : }
-
}
+ <>
+
+
Result: {data.runResult}
-
+
{data.errorMessage &&
Error: "{data.errorMessage}"
@@ -73,7 +55,7 @@ export const Submission = ({ data, solo: isSolo }: { data: SubmissionItem, solo:
/>
{/* improvement: add share score button */}
-
+ >
)
}
@@ -84,7 +66,7 @@ export const SubmissionDisplay = ({ id }: { id: number }) => {
if (isError) return There was an error retrieving your submission.
- return
+ return
}
const convertSeconds = (seconds: number) => {
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index ef97603..b8acb00 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,9 +1,6 @@
-import { api } from "~/utils/api";
-import { SignedIn, SignedOut } from "@clerk/nextjs";
-import { LoadingSpinner } from "~/components/loading";
+import { useContext } from "react";
+import { GlobalStatusContext } from "~/components/layout";
import { QuestionHandler } from "~/components/question";
-import { noRefreshOpts } from "~/components/constants";
-import { GreenSignInButton } from "~/components/layout";
const Description = () => {
return (
@@ -22,36 +19,18 @@ const Description = () => {
)
}
-const SignedInHome = () => {
- const {
- data: personalStatusData,
- isLoading: isPersonalStatusLoading,
- isError: isPersonalStatusError
- } = api.status.personal.useQuery(undefined, noRefreshOpts)
-
- if (isPersonalStatusError) return Could not connect to backend service. Please refresh the page.
-
- if (isPersonalStatusLoading) return
-
- // improvement: use suspense boundary (?)
- return
-}
export default function HomePage() {
+ const globalStatus = useContext(GlobalStatusContext)
+
+ if (!globalStatus) return (Could not connect to backend service. Please refresh the page.
)
return (
-
-
-
-
-
-
Sign in to get started!
-
-
-
+
Today's Question
+
- );
+ )
}
diff --git a/src/pages/past-questions.tsx b/src/pages/past-questions.tsx
new file mode 100644
index 0000000..6d4ed0f
--- /dev/null
+++ b/src/pages/past-questions.tsx
@@ -0,0 +1,90 @@
+import { ChevronRightIcon } from "@radix-ui/react-icons";
+import dayjs from "dayjs";
+import Link from "next/link";
+import React from "react";
+import { noRefreshOpts, statusColors } from "~/components/constants";
+import { LoadingSpinner } from "~/components/loading";
+import { api } from "~/utils/api";
+
+const QuestionList = () => {
+ const {
+ data,
+ error,
+ isLoading,
+ fetchNextPage,
+ hasNextPage,
+ isFetching,
+ isFetchingNextPage,
+ } = api.question.getInfinite.useInfiniteQuery({
+ limit: 2
+ }, {
+ getNextPageParam: (lastPage, _pages) => lastPage.nextCursor,
+ ...noRefreshOpts
+ })
+
+ if (error) return Could not load past questions.
+
+ if (isLoading) return
+
+ return (
+ <>
+ {data.pages.map((group, i) => (
+
+ {group.questions.map((question, i) => {
+ const submission = question.submissions[0]
+ const statusColor = submission ? statusColors[submission.runResult] : statusColors.UNKNOWN
+ return (
+
+
+
+
+ Question #{question.num}: {question.title}
+ {dayjs(question.startsAt).format('ddd MMM DD YYYY')}
+
+
+
+ ({submission?.runResult ?? "INCOMPLETE"})
+
+
+
+
+
+
+
+
+ )
+ }
+ )}
+
+ ))}
+
+
+ {isFetchingNextPage || isFetching
+ ?
+ : hasNextPage
+ ?
+
+ : null}
+
+ >
+ )
+}
+
+export default function PastQuestionsPage() {
+ return (
+ <>
+
+
Past Questions
+ {/*
Browse all previous questions.
*/}
+
+
+
+ >
+ )
+}
diff --git a/src/pages/past-submissions.tsx b/src/pages/past-submissions.tsx
deleted file mode 100644
index 6dc2da9..0000000
--- a/src/pages/past-submissions.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import React from "react";
-import { noRefreshOpts } from "~/components/constants";
-import { LoadingSpinner } from "~/components/loading";
-import { Submission } from "~/components/submission";
-import { api } from "~/utils/api";
-
-const SubmissionsList = () => {
- const {
- data,
- error,
- isLoading,
- fetchNextPage,
- hasNextPage,
- isFetching,
- isFetchingNextPage,
- } = api.submission.getInfinite.useInfiniteQuery({
- limit: 2
- }, {
- getNextPageParam: (lastPage, _pages) => lastPage.nextCursor,
- ...noRefreshOpts
- })
-
- if (error) return Could not load past submissions.
-
- if (isLoading) return
-
- return (
- <>
- {data.pages.map((group, i) => (
-
- {group.submissions.map((submission, i) => (
-
-
-
-
- ))}
-
- ))}
-
-
- {isFetchingNextPage || isFetching
- ?
- : hasNextPage
- ?
-
-
- : null}
-
-
- >
- )
-}
-
-export default function PastQuestionsPage() {
- return (
- <>
-
-
Past Submissions
-
All of your previously submitted solutions are listed here.
-
-
-
- >
- )
-}
diff --git a/src/pages/question/[num].tsx b/src/pages/question/[num].tsx
new file mode 100644
index 0000000..1932b46
--- /dev/null
+++ b/src/pages/question/[num].tsx
@@ -0,0 +1,23 @@
+import { useRouter } from "next/router";
+import { LoadingSpinner } from "~/components/loading";
+import { QuestionHandler } from "~/components/question";
+import { api } from "~/utils/api";
+
+
+export default function QuestionPage() {
+ const router = useRouter()
+
+ if (!router.query.num || typeof router.query.num !== "string") {
+ return <>>
+ }
+
+ const questionNum = parseInt(router.query.num)
+ const { data, isLoading, isError } = api.question.getPublic.useQuery({ num: questionNum })
+
+ if (isError) return Error loading question
+ if (isLoading) return
+
+ return (
+
+ )
+}
diff --git a/src/server/api/routers/question.ts b/src/server/api/routers/question.ts
index e7a71f6..aba2e33 100644
--- a/src/server/api/routers/question.ts
+++ b/src/server/api/routers/question.ts
@@ -1,8 +1,8 @@
import { type PrismaClient } from "@prisma/client";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
-import { createTRPCRouter, privateProcedure } from "~/server/api/trpc";
-import { filterQuestionForClient } from "~/server/helpers/filter";
+import { createTRPCRouter, privateProcedure, publicProcedure } from "~/server/api/trpc";
+import { filterQuestionForClientPrivate, filterQuestionForClientPublic } from "~/server/helpers/filter";
export const getQuestionById = async (db: PrismaClient, qid: number) => {
const question = await db.question.findFirst({
@@ -16,21 +16,43 @@ export const getQuestionById = async (db: PrismaClient, qid: number) => {
return question
}
+export const getQuestionByNum = async (db: PrismaClient, num: number) => {
+ const question = await db.question.findFirst({
+ where: {
+ num: num
+ }
+ })
+
+ if (question === null) throw new TRPCError({ code: "NOT_FOUND" })
+
+ return question
+}
+
export const questionRouter = createTRPCRouter({
- get: privateProcedure
+ getPublic: publicProcedure
+ .input(z.object({ num: z.number() }))
+ .query(async ({ ctx, input }) => {
+ const latestQuestion = await getLatestQuestion(ctx.db)
+ const question = await getQuestionByNum(ctx.db, input.num)
+ if (question.num > latestQuestion.num)
+ throw new TRPCError({ code: "BAD_REQUEST" })
+ return filterQuestionForClientPublic(question)
+ }),
+ getPrivate: privateProcedure
.input(z.object({ id: z.number() }))
.query(async ({ ctx, input }) => {
- const currentQuestion = await getCurrentQuestion(ctx.db)
- if (input.id > currentQuestion.id)
- throw new TRPCError({ code: "NOT_FOUND" })
+ const latestQuestion = await getLatestQuestion(ctx.db)
const question = await getQuestionById(ctx.db, input.id)
- return filterQuestionForClient(question)
+ if (question.num > latestQuestion.num)
+ throw new TRPCError({ code: "BAD_REQUEST" })
+ return filterQuestionForClientPrivate(question)
}),
start: privateProcedure
.input(z.object({ questionId: z.number() }))
.mutation(async ({ ctx, input }) => {
- const currentQuestion = await getCurrentQuestion(ctx.db)
- if (input.questionId > currentQuestion.id)
+ const latestQuestion = await getLatestQuestion(ctx.db)
+ const question = await getQuestionById(ctx.db, input.questionId)
+ if (question.num > latestQuestion.num)
throw new TRPCError({ code: "NOT_FOUND" })
if (await isQuestionAlreadyStartedByUser(ctx.db, ctx.userId, input.questionId))
@@ -53,7 +75,48 @@ export const questionRouter = createTRPCRouter({
console.error("submissionRouter/submit", e)
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" })
}
- })
+ }),
+ getInfinite: publicProcedure
+ .input(
+ z.object({
+ limit: z.number().min(1).max(20).nullish(),
+ cursor: z.number().nullish()
+ }),
+ )
+ .query(async ({ ctx, input }) => {
+ const limit = input.limit ?? 10
+ const { cursor } = input
+ const questions = await ctx.db.question.findMany({
+ take: limit + 1, // take an extra at end to use as next cursor
+ select: {
+ id: true,
+ num: true,
+ title: true,
+ startsAt: true,
+ submissions: {
+ where: {
+ authorId: ctx.userId ?? "" // ~should~ be no submissions with empty author
+ },
+ select: {
+ runResult: true
+ }
+ }
+ },
+ cursor: cursor ? { id: cursor } : undefined,
+ orderBy: {
+ num: 'desc',
+ },
+ })
+ let nextCursor: typeof cursor | undefined = undefined
+ if (questions.length > limit) {
+ const nextItem = questions.pop()
+ nextCursor = nextItem!.id
+ }
+ return {
+ questions,
+ nextCursor,
+ };
+ }),
});
const isQuestionAlreadyStartedByUser = async (db: PrismaClient, userId: string, questionId: number) => {
@@ -71,7 +134,7 @@ const isQuestionAlreadyStartedByUser = async (db: PrismaClient, userId: string,
// Otherwise, this function should only be used to validate that future questions are not being accessed/referenced
// All other procs that need a question id must get the id from the client
// This allows the user to start/submit previous questions at any time
-export const getCurrentQuestion = async (db: PrismaClient) => {
+export const getLatestQuestion = async (db: PrismaClient) => {
const question = await db.question.findFirst({
where: {
startsAt: {
diff --git a/src/server/api/routers/statistics.ts b/src/server/api/routers/statistics.ts
index ea16079..d3a3d63 100644
--- a/src/server/api/routers/statistics.ts
+++ b/src/server/api/routers/statistics.ts
@@ -2,20 +2,19 @@ import { type Submission, type PrismaClient, type Prisma } from "@prisma/client"
import { createTRPCRouter, privateProcedure, publicProcedure } from "~/server/api/trpc";
import { withTransaction, type TransactionPrismaClient } from "~/server/helpers/transaction";
import { getUserById } from "./user";
-import { getCurrentQuestion } from "./question";
+import { getLatestQuestion } from "./question";
export const statisticsRouter = createTRPCRouter({
global: publicProcedure
.query(async ({ ctx }) => {
- const currentQuestion = await getCurrentQuestion(ctx.db)
+ const latestQuestion = await getLatestQuestion(ctx.db)
const questionStats = await ctx.db.questionStats.findFirst({
where: {
- questionId: currentQuestion.id
+ questionId: latestQuestion.id
}
})
return {
- questionId: currentQuestion.id,
numAnswered: questionStats?.numSubmissions ?? 0,
topFive: questionStats?.top5Scores as LeaderboardEntry[] ?? [],
lastUpdated: new Date()
diff --git a/src/server/api/routers/status.ts b/src/server/api/routers/status.ts
index 35af3e7..b1c34be 100644
--- a/src/server/api/routers/status.ts
+++ b/src/server/api/routers/status.ts
@@ -1,7 +1,10 @@
import { createTRPCRouter, privateProcedure, publicProcedure } from "~/server/api/trpc";
-import { getCurrentQuestion } from "./question";
+import { getLatestQuestion, getQuestionById } from "./question";
+import { z } from "zod";
+import { TRPCError } from "@trpc/server";
+import { filterQuestionForClientPublic } from "~/server/helpers/filter";
-export type PersonalStatusData = {
+export type QuestionStatusData = {
isStarted: boolean,
isCompleted: boolean,
questionId: number,
@@ -12,19 +15,21 @@ export type PersonalStatusData = {
export const statusRouter = createTRPCRouter({
global: publicProcedure
.query(async ({ ctx }) => {
- const currentQuestion = await getCurrentQuestion(ctx.db)
- return {
- questionExpiration: currentQuestion.endsAt
- }
+ const latestQuestion = await getLatestQuestion(ctx.db)
+ return filterQuestionForClientPublic(latestQuestion)
}),
- personal: privateProcedure
- .query(async ({ ctx }): Promise => {
- const currentQuestion = await getCurrentQuestion(ctx.db)
+ question: privateProcedure
+ .input(z.object({ id: z.number() }))
+ .query(async ({ ctx, input }): Promise => {
+ const latestQuestion = await getLatestQuestion(ctx.db)
+ const question = await getQuestionById(ctx.db, input.id)
+ if (question.num > latestQuestion.num)
+ throw new TRPCError({ code: "BAD_REQUEST" })
const startEvent = await ctx.db.startEvent.findFirst({
where: {
authorId: ctx.userId,
- questionId: currentQuestion.id
+ questionId: question.id
}
})
@@ -32,16 +37,16 @@ export const statusRouter = createTRPCRouter({
return {
isStarted: false,
isCompleted: false,
- questionId: currentQuestion.id,
+ questionId: question.id,
submissionId: null,
startTime: null
- } satisfies PersonalStatusData
+ } satisfies QuestionStatusData
}
const submission = await ctx.db.submission.findFirst({
where: {
authorId: ctx.userId,
- questionId: currentQuestion.id
+ questionId: question.id
},
select: {
id: true
@@ -53,7 +58,7 @@ export const statusRouter = createTRPCRouter({
startTime: startEvent.createdAt,
isCompleted: submission != null,
submissionId: submission?.id ?? null,
- questionId: currentQuestion.id
- } satisfies PersonalStatusData
+ questionId: question.id
+ } satisfies QuestionStatusData
})
});
diff --git a/src/server/api/routers/submission.ts b/src/server/api/routers/submission.ts
index d573ae9..6152594 100644
--- a/src/server/api/routers/submission.ts
+++ b/src/server/api/routers/submission.ts
@@ -5,6 +5,7 @@ import { type CodeExecutionResult, executeCode } from "~/server/executeCode";
import { getQuestionById } from "./question";
import { type Question, type PrismaClient, SubmissionResult } from "@prisma/client";
import { updateQuestionStats, updateUserStats } from "./statistics";
+import { rateLimit } from "~/server/rateLimiter";
export const submissionRouter = createTRPCRouter({
getById: privateProcedure
@@ -25,6 +26,10 @@ export const submissionRouter = createTRPCRouter({
questionId: z.number()
}))
.mutation(async ({ ctx, input }) => {
+ const rateLimitResp = await rateLimit(ctx.userId)
+ if (rateLimitResp.remainingPoints === 0)
+ throw new TRPCError({ code: "TOO_MANY_REQUESTS", message: `Too many requests. Retry in ${Math.round(rateLimitResp.msBeforeNext / 1000)} seconds.` })
+
if (await isQuestionAlreadySubmittedByUser(ctx.db, ctx.userId, input.questionId))
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "You already submitted this question!" })
diff --git a/src/server/db.ts b/src/server/db.ts
index 8854ba4..a58d604 100644
--- a/src/server/db.ts
+++ b/src/server/db.ts
@@ -4,7 +4,7 @@ import { log as axiom } from "next-axiom";
import { env } from "~/env.mjs";
const prismaClientSingleton = () => {
- return new PrismaClient({
+ const client = new PrismaClient({
log: [
{ level: 'query', emit: 'event' },
{ level: 'info', emit: 'event' },
@@ -12,6 +12,40 @@ const prismaClientSingleton = () => {
{ level: 'error', emit: 'event' }
]
});
+
+ const queryBlockList = ["BEGIN", "COMMIT", "SELECT 1"]
+
+ client.$on('query', e => {
+ if (env.NODE_ENV === "production") {
+ if (!queryBlockList.some(blockedQuery => e.query.includes(blockedQuery))) {
+ axiom.info("query", { ...e });
+ }
+ } else {
+ console.log(e)
+ }
+ })
+
+ client.$on('error', e => {
+ if (env.NODE_ENV === "production") {
+ axiom.error("prisma error", { ...e })
+ } else {
+ console.log(e)
+ }
+ })
+
+ client.$on('info', e => {
+ if (env.NODE_ENV !== 'production') {
+ console.log(e)
+ }
+ })
+
+ client.$on('warn', e => {
+ if (env.NODE_ENV !== 'production') {
+ console.log(e)
+ }
+ })
+
+ return client
}
declare const globalThis: {
@@ -21,27 +55,3 @@ declare const globalThis: {
export const db = globalThis.prismaGlobal ?? prismaClientSingleton()
if (env.NODE_ENV === 'development') globalThis.prismaGlobal = db
-
-db.$on('query', e => {
- if (env.NODE_ENV !== 'development') {
- axiom.info("query", { ...e })
- } else {
- console.log(e)
- }
-})
-
-db.$on('info', e => {
- if (env.NODE_ENV !== 'production') {
- console.log(e)
- }
-})
-
-db.$on('warn', e => {
- if (env.NODE_ENV !== 'production') {
- console.log(e)
- }
-})
-
-db.$on('error', e => {
- console.log(e)
-})
diff --git a/src/server/helpers/filter.ts b/src/server/helpers/filter.ts
index d4a63a2..e8bd7cc 100644
--- a/src/server/helpers/filter.ts
+++ b/src/server/helpers/filter.ts
@@ -10,11 +10,25 @@ export const filterUserForClient = (user: User) => {
};
};
-export const filterQuestionForClient = (question: Question) => {
+export type PublicQuestionInfo = {
+ questionId: number
+ questionNum: number
+ questionTitle: string
+ questionExp: Date
+}
+
+export const filterQuestionForClientPublic = (question: Question) => {
+ return {
+ questionId: question.id,
+ questionNum: question.num,
+ questionTitle: question.title,
+ questionExp: question.endsAt
+ } as PublicQuestionInfo
+}
+
+export const filterQuestionForClientPrivate = (question: Question) => {
return {
- id: question.id,
- title: question.title,
- questionDescription: question.description,
- funcSignature: question.funcSig,
+ description: question.description,
+ signature: question.funcSig,
}
}
diff --git a/src/server/rateLimiter.ts b/src/server/rateLimiter.ts
new file mode 100644
index 0000000..c19fe68
--- /dev/null
+++ b/src/server/rateLimiter.ts
@@ -0,0 +1,18 @@
+import { TRPCError } from "@trpc/server";
+import { RateLimiterPrisma } from "rate-limiter-flexible"
+
+import { db } from "~/server/db";
+
+const rateLimiter = new RateLimiterPrisma({
+ storeClient: db,
+ tableName: "RateLimiter",
+ points: 4,
+ duration: 60,
+})
+
+export async function rateLimit(userId: string) {
+ return await rateLimiter.penalty(userId, 1).catch(err => {
+ console.log("Rate limiter error: " + err)
+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Rate limiter error" })
+ })
+}