Skip to content

Commit ad392cb

Browse files
Merge pull request #431 from YOGESH-08/staging
fix: build and ip_ratelimit
2 parents ec7ed0c + fd6a3f1 commit ad392cb

7 files changed

Lines changed: 101 additions & 59 deletions

File tree

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@ KV_URL="" # The URL of your Vercel KV instance
2727
KV_REST_API_URL="" # The REST API URL of your Vercel KV instance
2828
KV_REST_API_TOKEN="" # The REST API token for your Vercel KV instance
2929
KV_REST_API_READ_ONLY_TOKEN="" # The read-only REST API token for your Vercel KV instance
30+
31+
# Upstash_Redis
32+
UPSTASH_REDIS_REST_URL="" # REST URL of your Upstash Redis database
33+
UPSTASH_REDIS_REST_TOKEN="" # REST API token for Upstash Redis

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

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,48 @@ import { connectToDatabase } from "@/lib/database/mongoose";
33
import TagReport from "@/db/tagReport";
44
import { Ratelimit } from "@upstash/ratelimit";
55
import { redis } from "@/lib/utils/redis";
6-
import { exams } from "@/components/select_options";
6+
7+
const exams: string[] = ["CAT-1", "CAT-2", "FAT", "Model CAT-1", "Model CAT-2", "Model FAT"]
78

89
interface ReportedFieldInput {
910
field: string;
1011
value?: string;
1112
}
13+
interface ReportTagBody {
14+
paperId?: string;
15+
reportedFields?: unknown;
16+
comment?: unknown;
17+
reporterEmail?: unknown;
18+
reporterId?: unknown;
19+
}
20+
1221
const ALLOWED_FIELDS = ["subject", "courseCode", "exam", "slot", "year"];
1322

14-
const ratelimit = new Ratelimit({
15-
redis,
16-
limiter: Ratelimit.slidingWindow(3, "1 h"),//per id - 3 request - per hour
17-
analytics: true,
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,
1828
});
19-
29+
}
2030
function getClientIp(req: Request & { ip?: string}): string {
21-
return req.ip || "127.0.0.1";
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";
2240
}
2341

2442
export async function POST(req: Request & { ip?: string }) {
2543
try {
2644
await connectToDatabase();
27-
28-
const body = await req.json();
29-
const { paperId } = body;
45+
const ratelimit = getRateLimit();
46+
const body = (await req.json()) as ReportTagBody;
47+
const paperId = typeof body.paperId === "string" ? body.paperId : undefined;
3048

3149
if (!paperId) {
3250
return NextResponse.json(
@@ -55,12 +73,14 @@ export async function POST(req: Request & { ip?: string }) {
5573
}
5674
const reportedFields: ReportedFieldInput[] = Array.isArray(body.reportedFields)
5775
? body.reportedFields
58-
.map((r:Partial<ReportedFieldInput>) => ({
59-
field: typeof r.field === "string" ? r.field.trim() : "",
60-
value: typeof r.value === "string" ? r.value.trim() : undefined,
61-
}))
62-
.filter((r:Partial<ReportedFieldInput>) => r.field)
63-
: [];
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):[];
6484

6585
for (const rf of reportedFields) {
6686
if (!ALLOWED_FIELDS.includes(rf.field)) {
@@ -82,9 +102,10 @@ export async function POST(req: Request & { ip?: string }) {
82102
const newReport = await TagReport.create({
83103
paperId,
84104
reportedFields,
85-
comment: body.comment,
86-
reporterEmail: body.reporterEmail,
87-
reporterId: body.reporterId,
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+
88109
});
89110

90111
return NextResponse.json(

src/components/Footer.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import {
1515
} from "react-icons/fa6";
1616
import { Bold, Mail } from "lucide-react";
1717
import toast from "react-hot-toast";
18+
19+
type SubscribeResponse = {
20+
success?: boolean;
21+
error?: string;
22+
};
23+
1824
export default function Footer() {
1925
const { theme } = useTheme();
2026
const [isDarkMode, setIsDarkMode] = useState<boolean | null>(true);
@@ -37,14 +43,14 @@ export default function Footer() {
3743
body: JSON.stringify({ email }),
3844
})
3945
.then(async (res) => {
40-
const data = await res.json();
41-
if (!res.ok) return Promise.reject(data.error || "Something went wrong.");
46+
const data = (await res.json()) as SubscribeResponse;
47+
if (!res.ok) throw new Error(data.error ?? "Something went wrong.");
4248
return data;
4349
}),
4450
{
4551
loading: "Subscribing...",
4652
success: "You've Successfully Subscribed!",
47-
error: (err: any) => err,
53+
error: (err: Error) => err.message,
4854
},
4955
);
5056

src/components/ReportTagModal.tsx

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import { Button } from "@/components/ui/button";
1414
import { MultiSelect } from "@/components/multi-select";
1515
import LabeledInput from "@/components/ui/LabeledInput";
1616
import LabeledSelect from "@/components/ui/LabeledSelect";
17-
import axios from "axios";
17+
import axios, { AxiosError } from "axios";
1818
import toast from "react-hot-toast";
1919

20+
type ReportResponse = { error?: string; message?: string };
21+
2022
interface ReportTagModalProps {
2123
paperId: string;
2224
subject?: string;
@@ -40,8 +42,8 @@ const ReportTagModal = ({
4042
const [internalOpen, setInternalOpen] = useState(false);
4143
const isControlled = open !== undefined && setOpen !== undefined;
4244

43-
const modalOpen = isControlled ? open! : internalOpen;
44-
const modalSetOpen = isControlled ? setOpen! : setInternalOpen;
45+
const modalOpen = isControlled ? open : internalOpen;
46+
const modalSetOpen = isControlled ? setOpen : setInternalOpen;
4547
const [comment, setComment] = useState("");
4648
const [email, setEmail] = useState("");
4749
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
@@ -64,14 +66,14 @@ const ReportTagModal = ({
6466
const isDirty = useMemo(() => {
6567
if (selectedCategories.length === 0) return false;
6668
for (const c of selectedCategories) {
67-
const curr = (categoryValues[c] || "").trim();
68-
const orig = (originalCategoryValues[c] || "").trim();
69+
const curr = (categoryValues[c] ?? "").trim();
70+
const orig = (originalCategoryValues[c] ?? "").trim();
6971
if (curr !== orig) return true;
7072
}
7173

7274
if (selectedCategories.includes("subject")) {
73-
const currCode = (categoryValues["courseCode"] || "").trim();
74-
const origCode = (originalCategoryValues["courseCode"] || "").trim();
75+
const currCode = (categoryValues.courseCode ?? "").trim();
76+
const origCode = (originalCategoryValues.courseCode ?? "").trim();
7577
if (currCode !== origCode) return true;
7678
}
7779

@@ -83,7 +85,8 @@ const ReportTagModal = ({
8385
for (const c of selectedCategories) {
8486
if (categoryValues[c]) continue;
8587
if (c === "subject" && subject) {
86-
const m = subject.match(/^(.*)\s*\[([^\]]+)\]\s*$/);
88+
const subjectRegex = /^(.*)\s*\[([^\]]+)\]\s*$/;
89+
const m = subjectRegex.exec(subject);
8790
if (m?.[1] && m?.[2]) {
8891
const name = m[1].trim();
8992
const code = m[2].trim();
@@ -104,17 +107,18 @@ const ReportTagModal = ({
104107
if (open) {
105108
const base: Record<string, string> = {};
106109
if (subject) {
107-
const m = subject.match(/^(.*)\s*\[([^\]]+)\]\s*$/);
110+
const subjectRegex = /^(.*)\s*\[([^\]]+)\]\s*$/;
111+
const m = subjectRegex.exec(subject);
108112
if (m?.[1] && m?.[2]) {
109-
base["subject"] = m[1].trim();
110-
base["courseCode"] = m[2].trim();
113+
base.subject = m[1].trim();
114+
base.courseCode = m[2].trim();
111115
} else {
112-
base["subject"] = subject;
116+
base.subject = subject;
113117
}
114118
}
115-
if (exam) base["exam"] = exam;
116-
if (slot) base["slot"] = slot;
117-
if (year) base["year"] = year;
119+
if (exam) base.exam = exam;
120+
if (slot) base.slot = slot;
121+
if (year) base.year = year;
118122
setOriginalCategoryValues(base);
119123
setOriginalEmail("");
120124
} else {
@@ -134,23 +138,23 @@ const ReportTagModal = ({
134138
}
135139

136140
if (selectedCategories.includes("subject")) {
137-
const sub = (categoryValues.subject || "").trim();
141+
const sub = (categoryValues.subject ?? "").trim();
138142
if (!sub) {
139143
toast.error("Subject name cannot be empty.");
140144
return;
141145
}
142146
}
143147

144148
if (selectedCategories.includes("slot")) {
145-
const v = (categoryValues.slot || "").trim();
149+
const v = (categoryValues.slot ?? "").trim();
146150
const slotRegex = /^[A-G][1-2]$/;
147151
if (!slotRegex.test(v)) {
148152
toast.error("Slot must be from A1 to G2 (e.g., D1, B2).");
149153
return;
150154
}
151155
}
152156
if (selectedCategories.includes("year")) {
153-
const y = (categoryValues.year || "").trim();
157+
const y = (categoryValues.year ?? "").trim();
154158
const yearRegex = /^\d{4}(-\d{4})?$/;
155159
if (!yearRegex.test(y)) {
156160
toast.error("Year must be a valid format (e.g., 2024 or 2024-2025).");
@@ -180,15 +184,15 @@ const ReportTagModal = ({
180184

181185
for (const c of selectedCategories) {
182186
if (c === "subject") {
183-
const newSub = (categoryValues.subject || "").trim();
184-
const oldSub = (originalCategoryValues.subject || "").trim();
187+
const newSub = (categoryValues.subject ?? "").trim();
188+
const oldSub = (originalCategoryValues.subject ?? "").trim();
185189

186190
if (newSub !== oldSub) {
187191
reportedFields.push({ field: "subject", value: newSub });
188192
}
189193

190-
const newCode = (categoryValues.courseCode || "").trim();
191-
const oldCode = (originalCategoryValues.courseCode || "").trim();
194+
const newCode = (categoryValues.courseCode ?? "").trim();
195+
const oldCode = (originalCategoryValues.courseCode ?? "").trim();
192196

193197
if (newCode !== oldCode) {
194198
reportedFields.push({ field: "courseCode", value: newCode });
@@ -197,8 +201,8 @@ const ReportTagModal = ({
197201
continue;
198202
}
199203

200-
const newVal = (categoryValues[c] || "").trim();
201-
const oldVal = (originalCategoryValues[c] || "").trim();
204+
const newVal = (categoryValues[c] ?? "").trim();
205+
const oldVal = (originalCategoryValues[c] ?? "").trim();
202206

203207
if (newVal !== oldVal) {
204208
reportedFields.push({ field: c, value: newVal });
@@ -213,20 +217,23 @@ if (reportedFields.length === 0 && comment.trim().length === 0) {
213217
paperId,
214218
reportedFields,
215219
comment,
216-
reporterEmail: email || undefined,
220+
reporterEmail: email ?? undefined,
217221
};
218222
setLoading(true);
219223
await toast.promise(
220-
axios.post("/api/report-tag", payload),
224+
axios.post<ReportResponse>("/api/report-tag", payload),
221225
{
222226
loading: "Submitting your report...",
223227
success: "Reported successfully. Thank you, We will work on that",
224-
error: (err)=>{
225-
return (
226-
err?.response?.data?.error ||
227-
err?.message ||
228-
"Failed to submit report."
229-
)
228+
error: (err: unknown)=>{
229+
if (axios.isAxiosError<ReportResponse>(err)) {
230+
return (
231+
err.response?.data?.error ??
232+
err.message ??
233+
"Failed to submit report."
234+
);
235+
}
236+
return err instanceof Error ? err.message : "Failed to submit report.";
230237
},
231238
}
232239
)

src/components/ui/LabeledSelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const LabeledSelect = ({ label, value, onChange, options, placeholder }: Labeled
1717
<Field label={label}>
1818
<Select value={value} onValueChange={onChange}>
1919
<SelectTrigger className="w-full">
20-
<SelectValue placeholder={placeholder || "Select"} />
20+
<SelectValue placeholder={placeholder ?? "Select"} />
2121
</SelectTrigger>
2222
<SelectContent>
2323
{options.map((opt) => (

src/components/ui/field.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function Field({ label, children, className }: {
66
className?: string;
77
}) {
88
return (
9-
<div className={`space-y-1 w-full ${className || ""}`}>
9+
<div className={`space-y-1 w-full ${className ?? ""}`}>
1010
<Label>{label}</Label>
1111
{children}
1212
</div>

src/context/filterContext.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import React, {
77
useCallback,
88
type ReactNode,
99
} from "react";
10-
import { useRouter, useSearchParams } from "next/navigation";
10+
import { ReadonlyURLSearchParams, useRouter, useSearchParams } from "next/navigation";
1111
import { type IPaper, type Filters } from "@/interface";
1212
import JSZip from "jszip";
1313
import { toast } from "react-hot-toast";
@@ -154,12 +154,16 @@ export const FilterProvider: React.FC<FilterProviderProps> = ({
154154
console.error(`Failed to fetch ${paper.file_url}`, err);
155155
}
156156
}
157-
157+
function getDownloadName(params:ReadonlyURLSearchParams, key: string, fallback = "download"): string {
158+
const value = params.get(key);
159+
if (!value) return fallback;
160+
return value.split(" [")[0]?.trim() ?? fallback;
161+
}
158162
const zipBlob = await zip.generateAsync({ type: "blob" });
159163
const url = URL.createObjectURL(zipBlob);
160164
const a = document.createElement("a");
161165
a.href = url;
162-
a.download = searchParams.get("subject")?.split(" [")[0];
166+
a.download = getDownloadName(searchParams,"subject");
163167
document.body.appendChild(a);
164168
a.click();
165169
a.remove();

0 commit comments

Comments
 (0)