From efa28e24c50dcf6f1dd6821b433f98f6737fc1f3 Mon Sep 17 00:00:00 2001 From: Gslmao Date: Sun, 15 Feb 2026 19:59:46 +0530 Subject: [PATCH 1/3] refactor: refactored 3 api endpoints to seperate logic between controller and services --- src/app/api/course-list/route.ts | 7 ++-- src/app/api/paper-by-id/[id]/route.ts | 13 ++----- src/app/api/papers/count/route.ts | 14 ++------ src/lib/services/paper.ts | 52 +++++++++++++++++++-------- src/lib/services/subject.ts | 7 ++++ 5 files changed, 53 insertions(+), 40 deletions(-) create mode 100644 src/lib/services/subject.ts diff --git a/src/app/api/course-list/route.ts b/src/app/api/course-list/route.ts index 31ef5c0..47bf19d 100644 --- a/src/app/api/course-list/route.ts +++ b/src/app/api/course-list/route.ts @@ -1,14 +1,11 @@ import { NextResponse } from "next/server"; -import { connectToDatabase } from "@/lib/database/mongoose"; -import { Course } from "@/db/course"; +import { getCourseList } from "@/lib/services/subject"; export const dynamic = "force-dynamic"; export async function GET() { try { - await connectToDatabase(); - const courses = await Course.find().lean(); - + const courses = await getCourseList(); return NextResponse.json(courses, { status: 200 }); } catch (error) { console.error(error); diff --git a/src/app/api/paper-by-id/[id]/route.ts b/src/app/api/paper-by-id/[id]/route.ts index aced225..5a69c28 100644 --- a/src/app/api/paper-by-id/[id]/route.ts +++ b/src/app/api/paper-by-id/[id]/route.ts @@ -1,23 +1,16 @@ import { NextResponse } from "next/server"; -import { connectToDatabase } from "@/lib/database/mongoose"; -import Paper from "@/db/papers"; import { Types } from "mongoose"; +import { getPaperById } from "@/lib/services/paper"; export async function GET(req: Request, { params }: { params: { id: string } }) { try { - await connectToDatabase(); - const { id } = params; - + if (!Types.ObjectId.isValid(id)) { return NextResponse.json({ message: "Invalid paper ID" }, { status: 400 }); } - const paper = await Paper.findById(id); - - if (!paper) { - return NextResponse.json({ message: "Paper not found" }, { status: 404 }); - } + const paper = await getPaperById(id); return NextResponse.json(paper, { status: 200 }); } catch (error) { diff --git a/src/app/api/papers/count/route.ts b/src/app/api/papers/count/route.ts index 2225ff3..cc2fbf1 100644 --- a/src/app/api/papers/count/route.ts +++ b/src/app/api/papers/count/route.ts @@ -1,21 +1,13 @@ import { NextResponse } from "next/server"; -import { connectToDatabase } from "@/lib/database/mongoose"; -import CourseCount from "@/db/course"; +import { getCourseCounts } from "@/lib/services/paper"; export const dynamic = "force-dynamic"; export async function GET(req: Request) { try { - await connectToDatabase(); + const courseCount = await getCourseCounts(); - const count = await CourseCount.find().lean(); - - const formatted = count.map((item) => ({ - name: item.name, - count: item.count, - })); - - return NextResponse.json(formatted, { status: 200 }); + return NextResponse.json(courseCount, { status: 200 }); } catch (error) { return NextResponse.json( { message: "Failed to fetch course counts", error }, diff --git a/src/lib/services/paper.ts b/src/lib/services/paper.ts index ab60914..b2fd146 100644 --- a/src/lib/services/paper.ts +++ b/src/lib/services/paper.ts @@ -3,24 +3,48 @@ import { type IPaper } from "@/interface"; import { escapeRegExp } from "@/lib/utils/regex"; import { extractUniqueValues } from "@/lib/utils/paper-aggregation"; import { connectToDatabase } from "../database/mongoose"; +import CourseCount from "@/db/course"; export async function getPapersBySubject(subject: string) { - if (!subject){ - throw new Error("Subject query parameter is required"); - } + if (!subject){ + throw new Error("Subject query parameter is required"); + } + await connectToDatabase(); + + const escapedSubject = escapeRegExp(subject); + const papers: IPaper[] = await Paper.find({ + subject: { $regex: new RegExp(`${escapedSubject}`, "i") }, + }); - await connectToDatabase(); - - const escapedSubject = escapeRegExp(subject); - const papers: IPaper[] = await Paper.find({ - subject: { $regex: new RegExp(`${escapedSubject}`, "i") }, - }); + const uniqueValues = extractUniqueValues(papers); - const uniqueValues = extractUniqueValues(papers); + return { + papers, + ...uniqueValues, + } - return { - papers, - ...uniqueValues, - } +} +export async function getPaperById(id: string) { + await connectToDatabase(); + const paper = await Paper.findById(id); + + if (!paper) { + throw new Error("Paper not found"); // 404 + } + + return paper; +} + +export async function getCourseCounts(){ + await connectToDatabase(); + + const count = await CourseCount.find().lean(); + + const formatted = count.map((item) => ({ + name: item.name, + count: item.count, + })); + + return formatted; } \ No newline at end of file diff --git a/src/lib/services/subject.ts b/src/lib/services/subject.ts new file mode 100644 index 0000000..bdcfea7 --- /dev/null +++ b/src/lib/services/subject.ts @@ -0,0 +1,7 @@ +import { connectToDatabase } from "@/lib/database/mongoose"; +import { Course } from "@/db/course"; + +export async function getCourseList(){ + await connectToDatabase(); + return await Course.find().lean(); +} \ No newline at end of file From 75c3b5def8181eddc455475535dc16066b5c75f4 Mon Sep 17 00:00:00 2001 From: Gslmao Date: Sun, 15 Feb 2026 20:00:54 +0530 Subject: [PATCH 2/3] refactor: seperated Logics for report-tag endpoint --- src/app/api/report-tag/route.ts | 106 ++---------------------------- src/components/ReportTagModal.tsx | 2 +- src/lib/services/report.ts | 62 +++++++++++++++++ src/lib/utils/error.ts | 25 +++++++ src/lib/utils/rate-limiter.ts | 31 +++++++++ 5 files changed, 125 insertions(+), 101 deletions(-) create mode 100644 src/lib/services/report.ts create mode 100644 src/lib/utils/error.ts create mode 100644 src/lib/utils/rate-limiter.ts diff --git a/src/app/api/report-tag/route.ts b/src/app/api/report-tag/route.ts index 3c1dfc6..032cbd1 100644 --- a/src/app/api/report-tag/route.ts +++ b/src/app/api/report-tag/route.ts @@ -1,48 +1,10 @@ import { NextResponse } from "next/server"; -import { connectToDatabase } from "@/lib/database/mongoose"; -import TagReport from "@/db/tagReport"; -import { Ratelimit } from "@upstash/ratelimit"; -import { redis } from "@/lib/utils/redis"; - -const exams: string[] = ["CAT-1", "CAT-2", "FAT", "Model CAT-1", "Model CAT-2", "Model FAT"] - -interface ReportedFieldInput { - field: string; - value?: string; -} -interface ReportTagBody { - paperId?: string; - reportedFields?: unknown; - comment?: unknown; - reporterEmail?: unknown; - reporterId?: unknown; -} - -const ALLOWED_FIELDS = ["subject", "courseCode", "exam", "slot", "year"]; - -function getRateLimit(){ - return new Ratelimit({ - redis, - limiter: Ratelimit.slidingWindow(3, "1 h"),//per id - 3 request - per hour - analytics: true, -}); -} -function getClientIp(req: Request & { ip?: string}): string { - const xff = req.headers.get("x-forwarded-for"); - if (typeof xff === "string" && xff.length > 0) { - return xff.split(",")[0]?.trim()??""; - } - const xri = req.headers.get("x-real-ip"); - if (typeof xri === "string" && xri.length > 0) { - return xri; - } - return "0.0.0.0"; -} +import { reportTag, ReportTagBody } from "@/lib/services/report"; +import { rateLimitCheck } from "@/lib/utils/rate-limiter"; +import { customErrorHandler } from "@/lib/utils/error"; export async function POST(req: Request & { ip?: string }) { try { - await connectToDatabase(); - const ratelimit = getRateLimit(); const body = (await req.json()) as ReportTagBody; const paperId = typeof body.paperId === "string" ? body.paperId : undefined; @@ -52,61 +14,8 @@ export async function POST(req: Request & { ip?: string }) { { status: 400 } ); } - const ip = getClientIp(req); - const key = `${ip}::${paperId}`; - const { success } = await ratelimit.limit(key); - - if (!success) { - return NextResponse.json( - { error: "Rate limit exceeded for reporting." }, - { status: 429 } - ); - } - const MAX_REPORTS_PER_PAPER = 5; - const count = await TagReport.countDocuments({ paperId }); - - if (count >= MAX_REPORTS_PER_PAPER) { - return NextResponse.json( - { error: "Received many reports; we are currently working on it." }, - { status: 429 } - ); - } - const reportedFields: ReportedFieldInput[] = Array.isArray(body.reportedFields) - ? body.reportedFields - .map((r): ReportedFieldInput | null => { - if (!r || typeof r !== "object") return null; - - const field = typeof (r as { field?: unknown }).field === "string" ? (r as { field: string }).field.trim() : ""; - const value = typeof (r as { value?: unknown }).value === "string" ? (r as { value: string }).value.trim() : undefined; - return field ? { field, value } : null; - }) - .filter((r): r is ReportedFieldInput => r !== null):[]; - - for (const rf of reportedFields) { - if (!ALLOWED_FIELDS.includes(rf.field)) { - return NextResponse.json( - { error: `Invalid field: ${rf.field}` }, - { status: 400 } - ); - } - if (rf.field === "exam" && rf.value) { - if (!exams.some(e => e.toLowerCase() === rf.value?.toLowerCase())) { - return NextResponse.json( - { error: `Invalid exam value: ${rf.value}` }, - { status: 400 } - ); - } - } - } - - const newReport = await TagReport.create({ - paperId, - reportedFields, - comment: typeof body.comment === "string" ? body.comment : undefined, - reporterEmail: typeof body.reporterEmail === "string" ? body.reporterEmail : undefined, - reporterId: typeof body.reporterId === "string" ? body.reporterId : undefined, - - }); + await rateLimitCheck(req, paperId); + const newReport = await reportTag(paperId, body); return NextResponse.json( { message: "Report submitted.", report: newReport }, @@ -114,9 +23,6 @@ export async function POST(req: Request & { ip?: string }) { ); } catch (err) { console.error(err); - return NextResponse.json( - { error: "Failed to submit tag report." }, - { status: 500 } - ); + return customErrorHandler(err, "Failed to submit tag report."); } } diff --git a/src/components/ReportTagModal.tsx b/src/components/ReportTagModal.tsx index 3171f2a..91c31e2 100644 --- a/src/components/ReportTagModal.tsx +++ b/src/components/ReportTagModal.tsx @@ -228,7 +228,7 @@ if (reportedFields.length === 0 && comment.trim().length === 0) { error: (err: unknown)=>{ if (axios.isAxiosError(err)) { return ( - err.response?.data?.error ?? + err.response?.data?.message ?? err.message ?? "Failed to submit report." ); diff --git a/src/lib/services/report.ts b/src/lib/services/report.ts new file mode 100644 index 0000000..4f21808 --- /dev/null +++ b/src/lib/services/report.ts @@ -0,0 +1,62 @@ +import { connectToDatabase } from "@/lib/database/mongoose"; +import TagReport from "@/db/tagReport"; +import { CustomError } from "@/lib/utils/error"; + +const exams: string[] = ["CAT-1", "CAT-2", "FAT", "Model CAT-1", "Model CAT-2", "Model FAT"] +const ALLOWED_FIELDS = ["subject", "courseCode", "exam", "slot", "year"]; +const MAX_REPORTS_PER_PAPER = 5; + +export interface ReportTagBody { + paperId?: string; + reportedFields?: unknown; + comment?: unknown; + reporterEmail?: unknown; + reporterId?: unknown; +} + +interface ReportedFieldInput { + field: string; + value?: string; +} + +export async function reportTag(paperId: string, body: ReportTagBody) { + await connectToDatabase(); + const MAX_REPORTS_PER_PAPER = 5; + const count = await TagReport.countDocuments({ paperId }); + + if (count >= MAX_REPORTS_PER_PAPER) { + throw new CustomError("Received many reports; we are currently working on it.", 429) + } + const reportedFields: ReportedFieldInput[] = Array.isArray(body.reportedFields) + ? body.reportedFields + .map((r): ReportedFieldInput | null => { + if (!r || typeof r !== "object") return null; + + const field = typeof (r as { field?: unknown }).field === "string" ? (r as { field: string }).field.trim() : ""; + const value = typeof (r as { value?: unknown }).value === "string" ? (r as { value: string }).value.trim() : undefined; + return field ? { field, value } : null; + }) + .filter((r): r is ReportedFieldInput => r !== null):[]; + + for (const rf of reportedFields) { + if (!ALLOWED_FIELDS.includes(rf.field)) { + throw new CustomError(`Invalid field: ${rf.field}`, 400); + } + + if (rf.field === "exam" && rf.value) { + if (!exams.some(e => e.toLowerCase() === rf.value?.toLowerCase())) { + throw new CustomError(`Invalid exam value: ${rf.value}`, 400); + } + } + } + + const newReport = await TagReport.create({ + paperId, + reportedFields, + comment: typeof body.comment === "string" ? body.comment : undefined, + reporterEmail: typeof body.reporterEmail === "string" ? body.reporterEmail : undefined, + reporterId: typeof body.reporterId === "string" ? body.reporterId : undefined, + + }); + return newReport; +} \ No newline at end of file diff --git a/src/lib/utils/error.ts b/src/lib/utils/error.ts new file mode 100644 index 0000000..f4b1890 --- /dev/null +++ b/src/lib/utils/error.ts @@ -0,0 +1,25 @@ +import { NextResponse } from "next/server"; + +export class CustomError extends Error { + status: number; + + constructor(message: string, status: number) { + super(message); + this.status = status; + Object.setPrototypeOf(this, CustomError.prototype); + } +} + +export function customErrorHandler(error: unknown, defaultMessage: string) { + if (error instanceof CustomError) { + return NextResponse.json( + {message: error.message}, + {status: error.status} + ); + } else { + return NextResponse.json( + {message: defaultMessage}, + {status: 500} + ); + } +} diff --git a/src/lib/utils/rate-limiter.ts b/src/lib/utils/rate-limiter.ts new file mode 100644 index 0000000..bef7723 --- /dev/null +++ b/src/lib/utils/rate-limiter.ts @@ -0,0 +1,31 @@ +import { redis } from "@/lib/utils/redis"; +import { Ratelimit } from "@upstash/ratelimit"; +import { CustomError } from "./error"; + +function getClientIp(req: Request & { ip?: string}): string { + const xff = req.headers.get("x-forwarded-for"); + if (typeof xff === "string" && xff.length > 0) { + return xff.split(",")[0]?.trim()??""; + } + const xri = req.headers.get("x-real-ip"); + if (typeof xri === "string" && xri.length > 0) { + return xri; + } + return "0.0.0.0"; +} + +export async function rateLimitCheck(req: Request & { ip?: string }, paperId: string) { + const ratelimit = new Ratelimit({ + redis, + limiter: Ratelimit.slidingWindow(3, "1 h"),//per id - 3 request - per hour + analytics: true, + }); + + const ip = getClientIp(req); + const key = `${ip}::${paperId}`; + const { success } = await ratelimit.limit(key); + + if (!success) { + throw new CustomError("Rate limit exceeded for reporting.", 429); + } +} \ No newline at end of file From aeb3cbf18741295e9e790af08025f1577b814703 Mon Sep 17 00:00:00 2001 From: Gslmao <96773562+Gslmao@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:12:16 +0530 Subject: [PATCH 3/3] Update subject.ts fixed a syntax error --- src/lib/services/subject.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/services/subject.ts b/src/lib/services/subject.ts index 5c82879..d0291a0 100644 --- a/src/lib/services/subject.ts +++ b/src/lib/services/subject.ts @@ -7,7 +7,7 @@ import RelatedSubject from "@/db/relatedSubjects"; export async function getCourseList(){ await connectToDatabase(); return await Course.find().lean(); - +} export async function getRelatedSubjects(subject: string) { await connectToDatabase(); const escapedSubject = escapeRegExp(subject); @@ -16,4 +16,4 @@ export async function getRelatedSubjects(subject: string) { }); return subjects[0]?.related_subjects ?? []; -} \ No newline at end of file +}