Skip to content

Commit 75c3b5d

Browse files
committed
refactor: seperated Logics for report-tag endpoint
1 parent efa28e2 commit 75c3b5d

File tree

5 files changed

+125
-101
lines changed

5 files changed

+125
-101
lines changed

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/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/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+
}

src/lib/utils/rate-limiter.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { redis } from "@/lib/utils/redis";
2+
import { Ratelimit } from "@upstash/ratelimit";
3+
import { CustomError } from "./error";
4+
5+
function getClientIp(req: Request & { ip?: string}): string {
6+
const xff = req.headers.get("x-forwarded-for");
7+
if (typeof xff === "string" && xff.length > 0) {
8+
return xff.split(",")[0]?.trim()??"";
9+
}
10+
const xri = req.headers.get("x-real-ip");
11+
if (typeof xri === "string" && xri.length > 0) {
12+
return xri;
13+
}
14+
return "0.0.0.0";
15+
}
16+
17+
export async function rateLimitCheck(req: Request & { ip?: string }, paperId: string) {
18+
const ratelimit = new Ratelimit({
19+
redis,
20+
limiter: Ratelimit.slidingWindow(3, "1 h"),//per id - 3 request - per hour
21+
analytics: true,
22+
});
23+
24+
const ip = getClientIp(req);
25+
const key = `${ip}::${paperId}`;
26+
const { success } = await ratelimit.limit(key);
27+
28+
if (!success) {
29+
throw new CustomError("Rate limit exceeded for reporting.", 429);
30+
}
31+
}

0 commit comments

Comments
 (0)