Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions src/app/api/course-list/route.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
13 changes: 3 additions & 10 deletions src/app/api/paper-by-id/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
14 changes: 3 additions & 11 deletions src/app/api/papers/count/route.ts
Original file line number Diff line number Diff line change
@@ -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 },
Expand Down
106 changes: 6 additions & 100 deletions src/app/api/report-tag/route.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -52,71 +14,15 @@ 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 },
{ status: 201 }
);
} catch (err) {
console.error(err);
return NextResponse.json(
{ error: "Failed to submit tag report." },
{ status: 500 }
);
return customErrorHandler(err, "Failed to submit tag report.");
}
}
2 changes: 1 addition & 1 deletion src/components/ReportTagModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ if (reportedFields.length === 0 && comment.trim().length === 0) {
error: (err: unknown)=>{
if (axios.isAxiosError<ReportResponse>(err)) {
return (
err.response?.data?.error ??
err.response?.data?.message ??
err.message ??
"Failed to submit report."
);
Expand Down
52 changes: 38 additions & 14 deletions src/lib/services/paper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
62 changes: 62 additions & 0 deletions src/lib/services/report.ts
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 6 additions & 1 deletion src/lib/services/subject.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { connectToDatabase } from "@/lib/database/mongoose";
import { Course } from "@/db/course";
import { IRelatedSubject } from "@/interface";
import { escapeRegExp } from "@/lib/utils/regex";
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);
Expand All @@ -11,4 +16,4 @@ export async function getRelatedSubjects(subject: string) {
});

return subjects[0]?.related_subjects ?? [];
}
}
25 changes: 25 additions & 0 deletions src/lib/utils/error.ts
Original file line number Diff line number Diff line change
@@ -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}
);
}
}
Loading