Skip to content

Commit b753581

Browse files
Merge pull request #443 from Gslmao/Gslmao/seperate-logic
Gslmao/seperate logic
2 parents ac62f6e + aeb3cbf commit b753581

File tree

10 files changed

+177
-142
lines changed

10 files changed

+177
-142
lines changed

src/app/api/course-list/route.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { NextResponse } from "next/server";
2-
import { connectToDatabase } from "@/lib/database/mongoose";
3-
import { Course } from "@/db/course";
2+
import { getCourseList } from "@/lib/services/subject";
43

54
export const dynamic = "force-dynamic";
65

76
export async function GET() {
87
try {
9-
await connectToDatabase();
10-
const courses = await Course.find().lean();
11-
8+
const courses = await getCourseList();
129
return NextResponse.json(courses, { status: 200 });
1310
} catch (error) {
1411
console.error(error);

src/app/api/paper-by-id/[id]/route.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
11
import { NextResponse } from "next/server";
2-
import { connectToDatabase } from "@/lib/database/mongoose";
3-
import Paper from "@/db/papers";
42
import { Types } from "mongoose";
3+
import { getPaperById } from "@/lib/services/paper";
54

65
export async function GET(req: Request, { params }: { params: { id: string } }) {
76
try {
8-
await connectToDatabase();
9-
107
const { id } = params;
11-
8+
129
if (!Types.ObjectId.isValid(id)) {
1310
return NextResponse.json({ message: "Invalid paper ID" }, { status: 400 });
1411
}
1512

16-
const paper = await Paper.findById(id);
17-
18-
if (!paper) {
19-
return NextResponse.json({ message: "Paper not found" }, { status: 404 });
20-
}
13+
const paper = await getPaperById(id);
2114

2215
return NextResponse.json(paper, { status: 200 });
2316
} catch (error) {

src/app/api/papers/count/route.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,13 @@
11
import { NextResponse } from "next/server";
2-
import { connectToDatabase } from "@/lib/database/mongoose";
3-
import CourseCount from "@/db/course";
2+
import { getCourseCounts } from "@/lib/services/paper";
43

54
export const dynamic = "force-dynamic";
65

76
export async function GET(req: Request) {
87
try {
9-
await connectToDatabase();
8+
const courseCount = await getCourseCounts();
109

11-
const count = await CourseCount.find().lean();
12-
13-
const formatted = count.map((item) => ({
14-
name: item.name,
15-
count: item.count,
16-
}));
17-
18-
return NextResponse.json(formatted, { status: 200 });
10+
return NextResponse.json(courseCount, { status: 200 });
1911
} catch (error) {
2012
return NextResponse.json(
2113
{ message: "Failed to fetch course counts", error },

src/app/api/report-tag/route.ts

Lines changed: 6 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,10 @@
11
import { NextResponse } from "next/server";
2-
import { connectToDatabase } from "@/lib/database/mongoose";
3-
import TagReport from "@/db/tagReport";
4-
import { Ratelimit } from "@upstash/ratelimit";
5-
import { redis } from "@/lib/utils/redis";
6-
7-
const exams: string[] = ["CAT-1", "CAT-2", "FAT", "Model CAT-1", "Model CAT-2", "Model FAT"]
8-
9-
interface ReportedFieldInput {
10-
field: string;
11-
value?: string;
12-
}
13-
interface ReportTagBody {
14-
paperId?: string;
15-
reportedFields?: unknown;
16-
comment?: unknown;
17-
reporterEmail?: unknown;
18-
reporterId?: unknown;
19-
}
20-
21-
const ALLOWED_FIELDS = ["subject", "courseCode", "exam", "slot", "year"];
22-
23-
function getRateLimit(){
24-
return new Ratelimit({
25-
redis,
26-
limiter: Ratelimit.slidingWindow(3, "1 h"),//per id - 3 request - per hour
27-
analytics: true,
28-
});
29-
}
30-
function getClientIp(req: Request & { ip?: string}): string {
31-
const xff = req.headers.get("x-forwarded-for");
32-
if (typeof xff === "string" && xff.length > 0) {
33-
return xff.split(",")[0]?.trim()??"";
34-
}
35-
const xri = req.headers.get("x-real-ip");
36-
if (typeof xri === "string" && xri.length > 0) {
37-
return xri;
38-
}
39-
return "0.0.0.0";
40-
}
2+
import { reportTag, ReportTagBody } from "@/lib/services/report";
3+
import { rateLimitCheck } from "@/lib/utils/rate-limiter";
4+
import { customErrorHandler } from "@/lib/utils/error";
415

426
export async function POST(req: Request & { ip?: string }) {
437
try {
44-
await connectToDatabase();
45-
const ratelimit = getRateLimit();
468
const body = (await req.json()) as ReportTagBody;
479
const paperId = typeof body.paperId === "string" ? body.paperId : undefined;
4810

@@ -52,71 +14,15 @@ export async function POST(req: Request & { ip?: string }) {
5214
{ status: 400 }
5315
);
5416
}
55-
const ip = getClientIp(req);
56-
const key = `${ip}::${paperId}`;
57-
const { success } = await ratelimit.limit(key);
58-
59-
if (!success) {
60-
return NextResponse.json(
61-
{ error: "Rate limit exceeded for reporting." },
62-
{ status: 429 }
63-
);
64-
}
65-
const MAX_REPORTS_PER_PAPER = 5;
66-
const count = await TagReport.countDocuments({ paperId });
67-
68-
if (count >= MAX_REPORTS_PER_PAPER) {
69-
return NextResponse.json(
70-
{ error: "Received many reports; we are currently working on it." },
71-
{ status: 429 }
72-
);
73-
}
74-
const reportedFields: ReportedFieldInput[] = Array.isArray(body.reportedFields)
75-
? body.reportedFields
76-
.map((r): ReportedFieldInput | null => {
77-
if (!r || typeof r !== "object") return null;
78-
79-
const field = typeof (r as { field?: unknown }).field === "string" ? (r as { field: string }).field.trim() : "";
80-
const value = typeof (r as { value?: unknown }).value === "string" ? (r as { value: string }).value.trim() : undefined;
81-
return field ? { field, value } : null;
82-
})
83-
.filter((r): r is ReportedFieldInput => r !== null):[];
84-
85-
for (const rf of reportedFields) {
86-
if (!ALLOWED_FIELDS.includes(rf.field)) {
87-
return NextResponse.json(
88-
{ error: `Invalid field: ${rf.field}` },
89-
{ status: 400 }
90-
);
91-
}
92-
if (rf.field === "exam" && rf.value) {
93-
if (!exams.some(e => e.toLowerCase() === rf.value?.toLowerCase())) {
94-
return NextResponse.json(
95-
{ error: `Invalid exam value: ${rf.value}` },
96-
{ status: 400 }
97-
);
98-
}
99-
}
100-
}
101-
102-
const newReport = await TagReport.create({
103-
paperId,
104-
reportedFields,
105-
comment: typeof body.comment === "string" ? body.comment : undefined,
106-
reporterEmail: typeof body.reporterEmail === "string" ? body.reporterEmail : undefined,
107-
reporterId: typeof body.reporterId === "string" ? body.reporterId : undefined,
108-
109-
});
17+
await rateLimitCheck(req, paperId);
18+
const newReport = await reportTag(paperId, body);
11019

11120
return NextResponse.json(
11221
{ message: "Report submitted.", report: newReport },
11322
{ status: 201 }
11423
);
11524
} catch (err) {
11625
console.error(err);
117-
return NextResponse.json(
118-
{ error: "Failed to submit tag report." },
119-
{ status: 500 }
120-
);
26+
return customErrorHandler(err, "Failed to submit tag report.");
12127
}
12228
}

src/components/ReportTagModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ if (reportedFields.length === 0 && comment.trim().length === 0) {
228228
error: (err: unknown)=>{
229229
if (axios.isAxiosError<ReportResponse>(err)) {
230230
return (
231-
err.response?.data?.error ??
231+
err.response?.data?.message ??
232232
err.message ??
233233
"Failed to submit report."
234234
);

src/lib/services/paper.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,48 @@ import { type IPaper } from "@/interface";
33
import { escapeRegExp } from "@/lib/utils/regex";
44
import { extractUniqueValues } from "@/lib/utils/paper-aggregation";
55
import { connectToDatabase } from "../database/mongoose";
6+
import CourseCount from "@/db/course";
67

78
export async function getPapersBySubject(subject: string) {
8-
if (!subject){
9-
throw new Error("Subject query parameter is required");
10-
}
9+
if (!subject){
10+
throw new Error("Subject query parameter is required");
11+
}
12+
await connectToDatabase();
13+
14+
const escapedSubject = escapeRegExp(subject);
15+
const papers: IPaper[] = await Paper.find({
16+
subject: { $regex: new RegExp(`${escapedSubject}`, "i") },
17+
});
1118

12-
await connectToDatabase();
13-
14-
const escapedSubject = escapeRegExp(subject);
15-
const papers: IPaper[] = await Paper.find({
16-
subject: { $regex: new RegExp(`${escapedSubject}`, "i") },
17-
});
19+
const uniqueValues = extractUniqueValues(papers);
1820

19-
const uniqueValues = extractUniqueValues(papers);
21+
return {
22+
papers,
23+
...uniqueValues,
24+
}
2025

21-
return {
22-
papers,
23-
...uniqueValues,
24-
}
26+
}
2527

28+
export async function getPaperById(id: string) {
29+
await connectToDatabase();
30+
const paper = await Paper.findById(id);
31+
32+
if (!paper) {
33+
throw new Error("Paper not found"); // 404
34+
}
35+
36+
return paper;
37+
}
38+
39+
export async function getCourseCounts(){
40+
await connectToDatabase();
41+
42+
const count = await CourseCount.find().lean();
43+
44+
const formatted = count.map((item) => ({
45+
name: item.name,
46+
count: item.count,
47+
}));
48+
49+
return formatted;
2650
}

src/lib/services/report.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { connectToDatabase } from "@/lib/database/mongoose";
2+
import TagReport from "@/db/tagReport";
3+
import { CustomError } from "@/lib/utils/error";
4+
5+
const exams: string[] = ["CAT-1", "CAT-2", "FAT", "Model CAT-1", "Model CAT-2", "Model FAT"]
6+
const ALLOWED_FIELDS = ["subject", "courseCode", "exam", "slot", "year"];
7+
const MAX_REPORTS_PER_PAPER = 5;
8+
9+
export interface ReportTagBody {
10+
paperId?: string;
11+
reportedFields?: unknown;
12+
comment?: unknown;
13+
reporterEmail?: unknown;
14+
reporterId?: unknown;
15+
}
16+
17+
interface ReportedFieldInput {
18+
field: string;
19+
value?: string;
20+
}
21+
22+
export async function reportTag(paperId: string, body: ReportTagBody) {
23+
await connectToDatabase();
24+
const MAX_REPORTS_PER_PAPER = 5;
25+
const count = await TagReport.countDocuments({ paperId });
26+
27+
if (count >= MAX_REPORTS_PER_PAPER) {
28+
throw new CustomError("Received many reports; we are currently working on it.", 429)
29+
}
30+
const reportedFields: ReportedFieldInput[] = Array.isArray(body.reportedFields)
31+
? body.reportedFields
32+
.map((r): ReportedFieldInput | null => {
33+
if (!r || typeof r !== "object") return null;
34+
35+
const field = typeof (r as { field?: unknown }).field === "string" ? (r as { field: string }).field.trim() : "";
36+
const value = typeof (r as { value?: unknown }).value === "string" ? (r as { value: string }).value.trim() : undefined;
37+
return field ? { field, value } : null;
38+
})
39+
.filter((r): r is ReportedFieldInput => r !== null):[];
40+
41+
for (const rf of reportedFields) {
42+
if (!ALLOWED_FIELDS.includes(rf.field)) {
43+
throw new CustomError(`Invalid field: ${rf.field}`, 400);
44+
}
45+
46+
if (rf.field === "exam" && rf.value) {
47+
if (!exams.some(e => e.toLowerCase() === rf.value?.toLowerCase())) {
48+
throw new CustomError(`Invalid exam value: ${rf.value}`, 400);
49+
}
50+
}
51+
}
52+
53+
const newReport = await TagReport.create({
54+
paperId,
55+
reportedFields,
56+
comment: typeof body.comment === "string" ? body.comment : undefined,
57+
reporterEmail: typeof body.reporterEmail === "string" ? body.reporterEmail : undefined,
58+
reporterId: typeof body.reporterId === "string" ? body.reporterId : undefined,
59+
60+
});
61+
return newReport;
62+
}

src/lib/services/subject.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { connectToDatabase } from "@/lib/database/mongoose";
2+
import { Course } from "@/db/course";
23
import { IRelatedSubject } from "@/interface";
34
import { escapeRegExp } from "@/lib/utils/regex";
45
import RelatedSubject from "@/db/relatedSubjects";
56

7+
export async function getCourseList(){
8+
await connectToDatabase();
9+
return await Course.find().lean();
10+
}
611
export async function getRelatedSubjects(subject: string) {
712
await connectToDatabase();
813
const escapedSubject = escapeRegExp(subject);
@@ -11,4 +16,4 @@ export async function getRelatedSubjects(subject: string) {
1116
});
1217

1318
return subjects[0]?.related_subjects ?? [];
14-
}
19+
}

src/lib/utils/error.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { NextResponse } from "next/server";
2+
3+
export class CustomError extends Error {
4+
status: number;
5+
6+
constructor(message: string, status: number) {
7+
super(message);
8+
this.status = status;
9+
Object.setPrototypeOf(this, CustomError.prototype);
10+
}
11+
}
12+
13+
export function customErrorHandler(error: unknown, defaultMessage: string) {
14+
if (error instanceof CustomError) {
15+
return NextResponse.json(
16+
{message: error.message},
17+
{status: error.status}
18+
);
19+
} else {
20+
return NextResponse.json(
21+
{message: defaultMessage},
22+
{status: 500}
23+
);
24+
}
25+
}

0 commit comments

Comments
 (0)