Skip to content

Commit f7770f6

Browse files
Merge pull request #22 from CodeChefVIT/staging
Staging
2 parents 1e9312b + 712a979 commit f7770f6

File tree

3 files changed

+99
-114
lines changed

3 files changed

+99
-114
lines changed

src/app/api/mail/route.ts

Lines changed: 51 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextResponse } from "next/server";
22
import nodemailer from "nodemailer";
3+
import path from "path";
34

45
type MailOptions = {
56
from: string;
@@ -14,58 +15,31 @@ type MailOptions = {
1415
}[];
1516
};
1617

17-
const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000;
18-
const RATE_LIMIT_MAX_REQUESTS = 3;
19-
20-
const rateLimitStore: Record<string, { count: number; lastRequest: number }> =
21-
{};
22-
function isRateLimited(ip: string) {
23-
const currentTime = Date.now();
24-
const record = rateLimitStore[ip];
25-
26-
if (record) {
27-
if (currentTime - record.lastRequest > RATE_LIMIT_WINDOW_MS) {
28-
rateLimitStore[ip] = { count: 1, lastRequest: currentTime };
29-
return false;
30-
} else {
31-
if (record.count >= RATE_LIMIT_MAX_REQUESTS) {
32-
return true;
33-
} else {
34-
record.count++;
35-
record.lastRequest = currentTime;
36-
return false;
37-
}
38-
}
39-
} else {
40-
rateLimitStore[ip] = { count: 1, lastRequest: currentTime };
41-
return false;
42-
}
43-
}
18+
const ALLOWED_MIME_TYPES = [
19+
"application/pdf",
20+
"image/jpeg",
21+
"image/png",
22+
"image/gif",
23+
];
24+
const MAX_FILE_SIZE_MB = 5;
25+
const ALLOWED_EXTENSIONS = [".pdf", ".jpg", ".jpeg", ".png", ".gif"];
4426

4527
export async function POST(request: Request) {
4628
try {
4729
const formData = await request.formData();
4830

4931
const ip =
50-
request.headers.get("x-real-ip") ??
51-
request.headers.get("x-forwarded-for") ??
32+
request.headers.get("x-real-ip") ??
33+
request.headers.get("x-forwarded-for") ??
5234
request.headers.get("remote-addr");
5335

5436
if (!ip) {
5537
return NextResponse.json(
56-
{ message: "IP address not found" },
57-
{ status: 400 },
38+
{ message: "IP address not found" },
39+
{ status: 400 }
5840
);
5941
}
6042

61-
// Uncomment to enable rate-limiter
62-
// if (isRateLimited(ip)) {
63-
// return NextResponse.json(
64-
// { message: "Too many requests. Please try again later." },
65-
// { status: 429 },
66-
// );
67-
// }
68-
6943
const transporter = nodemailer.createTransport({
7044
host: process.env.EMAIL_HOST,
7145
port: parseInt(process!.env.EMAIL_PORT as string),
@@ -76,11 +50,10 @@ export async function POST(request: Request) {
7650
},
7751
});
7852

79-
const zipFile = formData.get("zipFile");
80-
const slot = formData.get("slot")?.toString() ?? '';
81-
const subject = formData.get("subject")?.toString() ?? '';
82-
const exam = formData.get("exam")?.toString() ?? '';
83-
const year = formData.get("year")?.toString() ?? '';
53+
const slot = formData.get("slot")?.toString() ?? "";
54+
const subject = formData.get("subject")?.toString() ?? "";
55+
const exam = formData.get("exam")?.toString() ?? "";
56+
const year = formData.get("year")?.toString() ?? "";
8457

8558
const htmlContent = `
8659
<div style="font-family: Arial, sans-serif; line-height: 1.5;">
@@ -92,32 +65,53 @@ export async function POST(request: Request) {
9265
</div>
9366
`;
9467

68+
const attachments: { filename: string; content: Buffer }[] = [];
69+
const files = formData.getAll("files");
70+
71+
for (const file of files) {
72+
if (file instanceof Blob) {
73+
const fileType = file.type;
74+
const fileName = (file as any).name;
75+
const fileExtension = path.extname(fileName).toLowerCase();
76+
const fileSizeMB = file.size / (1024 * 1024);
77+
78+
if (!ALLOWED_MIME_TYPES.includes(fileType) || !ALLOWED_EXTENSIONS.includes(fileExtension)) {
79+
return NextResponse.json(
80+
{ message: `File type not allowed: ${fileType}` },
81+
{ status: 400 }
82+
);
83+
}
84+
85+
if (fileSizeMB > MAX_FILE_SIZE_MB) {
86+
return NextResponse.json(
87+
{ message: `File ${fileName} exceeds the 5MB size limit` },
88+
{ status: 400 }
89+
);
90+
}
91+
92+
const buffer = await file.arrayBuffer();
93+
attachments.push({
94+
filename: fileName,
95+
content: Buffer.from(buffer),
96+
});
97+
}
98+
}
99+
95100
const mailOptions: MailOptions = {
96101
from: process.env.EMAIL_USER!,
97102
to: process.env.EMAIL_USER!,
98103
subject: subject,
99104
html: htmlContent,
100-
attachments: [],
105+
attachments,
101106
};
102107

103-
if (zipFile instanceof Blob) {
104-
const buffer = await zipFile.arrayBuffer();
105-
const content = Buffer.from(buffer);
106-
107-
mailOptions.attachments!.push({
108-
filename: "files.zip",
109-
content: content,
110-
});
111-
}
112-
113108
await transporter.sendMail(mailOptions);
114109

115110
return NextResponse.json({ message: "Email sent successfully!" }, { status: 200 });
116111
} catch (error) {
117112
return NextResponse.json(
118113
{ message: "Error sending email", error: error },
119-
{ status: 422 },
114+
{ status: 422 }
120115
);
121116
}
122117
}
123-

src/app/upload/page.tsx

Lines changed: 47 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"use client";
22
import React, { useRef, useState } from "react";
3-
import JSZip from "jszip";
43
import axios from "axios";
54
import { slots, courses } from "./select_options";
65
import toast, { Toaster } from "react-hot-toast";
@@ -31,7 +30,6 @@ import Footer from "@/components/Footer";
3130

3231
const Page = () => {
3332
const router = useRouter();
34-
// const [openCamera, setOpenCamera] = useState(false);
3533
const fileInputRef = useRef<HTMLInputElement>(null);
3634

3735
const [slot, setSlot] = useState("");
@@ -40,45 +38,61 @@ const Page = () => {
4038
const [year, setYear] = useState("");
4139
const [files, setFiles] = useState<File[]>([]);
4240
const [inputValue, setInputValue] = useState('')
43-
4441
const [isSubjectCommandOpen, setIsSubjectCommandOpen] = useState(false);
45-
// const toggleOpenCamera = () => {
46-
// setOpenCamera((prev) => !prev);
47-
// };
42+
const [isUploading, setIsUploading] = useState(false);
4843

4944
const handlePrint = async () => {
50-
// file validation
5145
const maxFileSize = 5 * 1024 * 1024;
46+
const allowedFileTypes = ["application/pdf", "image/jpeg", "image/png", "image/gif"];
5247
const files = fileInputRef.current?.files as FileList | null;
53-
if (!files || files.length == 0) {
48+
49+
if (!slot) {
50+
toast.error("Slot is required");
51+
return;
52+
}
53+
if (!subject) {
54+
toast.error("Subject is required");
55+
return;
56+
}
57+
if (!exam) {
58+
toast.error("Exam is required");
59+
return;
60+
}
61+
if (!year) {
62+
toast.error("Year is required");
63+
return;
64+
}
65+
if (!files || files.length === 0) {
5466
toast.error("No files selected");
5567
return;
5668
} else if (files.length > 5) {
5769
toast.error("More than 5 files selected");
5870
return;
5971
}
72+
6073
for (const file of files) {
6174
if (file.size > maxFileSize) {
6275
toast.error(`File ${file.name} is more than 5MB`);
6376
return;
6477
}
78+
79+
if (!allowedFileTypes.includes(file.type)) {
80+
toast.error(`File type of ${file.name} is not allowed. Only PDFs and images are accepted.`);
81+
return;
82+
}
6583
}
66-
const zip = new JSZip();
84+
6785
const formData = new FormData();
68-
6986
for (const file of files) {
70-
zip.file(file.name, file);
71-
const content = await zip.generateAsync({ type: "blob" });
72-
73-
const arrayBuffer = await new Response(content).arrayBuffer();
74-
const uint8Array = new Uint8Array(arrayBuffer);
75-
76-
formData.append("zipFile", new Blob([uint8Array]), "files.zip");
77-
formData.append("slot", slot);
78-
formData.append("subject", subject);
79-
formData.append("exam", exam);
80-
formData.append("year", year);
87+
formData.append("files", file);
8188
}
89+
formData.append("slot", slot);
90+
formData.append("subject", subject);
91+
formData.append("exam", exam);
92+
formData.append("year", year);
93+
94+
setIsUploading(true); // Set uploading to true
95+
8296
try {
8397
const result = await toast.promise(
8498
(async () => {
@@ -90,28 +104,28 @@ const Page = () => {
90104
headers: {
91105
"Content-Type": "multipart/form-data",
92106
},
93-
},
107+
}
94108
);
95109
return response.data;
96110
} catch (error) {
97-
handleAPIError(error);
111+
throw handleAPIError(error);
98112
}
99113
})(),
100114
{
101115
loading: "Sending papers",
102116
success: "Papers successfully sent",
103117
error: (err: ApiError) => err.message,
104-
},
118+
}
105119
);
120+
106121
if (result?.message === "Email sent successfully!") {
107-
setTimeout(() => {
108-
router.push("/");
109-
}, 1500);
110122
}
111123
} catch (e) {
124+
} finally {
125+
setIsUploading(false);
112126
}
113127
};
114-
128+
115129
const handleSubjectSelect = (value: string) => {
116130
setSubject(value);
117131
setInputValue(value)
@@ -169,30 +183,6 @@ const Page = () => {
169183
{/* Subject Selection */}
170184
<div>
171185
<label>Subject:</label>
172-
{/* <div className="relative">
173-
<Button
174-
type="button"
175-
onClick={() => setIsSubjectCommandOpen((prev) => !prev)}
176-
className="m-2 rounded-md border p-2"
177-
>
178-
{subject || "Select subject"}
179-
</Button>
180-
{isSubjectCommandOpen && (
181-
<Command className="absolute z-10 mt-2 w-full rounded-lg border shadow-md">
182-
<CommandInput placeholder="Search subject..." />
183-
<CommandList>
184-
<CommandEmpty>No subjects found.</CommandEmpty>
185-
<CommandGroup heading="Subjects">
186-
{courses.map((course) => (
187-
<CommandItem key={course} onSelect={() => handleSubjectSelect(course)}>
188-
<span>{course}</span>
189-
</CommandItem>
190-
))}
191-
</CommandGroup>
192-
</CommandList>
193-
</Command>
194-
)}
195-
</div> */}
196186
<Command className="rounded-lg border shadow-md md:min-w-[450px]">
197187
<CommandInput
198188
value={inputValue}
@@ -249,7 +239,7 @@ const Page = () => {
249239
<Input
250240
required
251241
type="file"
252-
accept="image/*,.pdf"
242+
// accept="image/*,.pdf"
253243
multiple
254244
ref={fileInputRef}
255245
className="hidden"
@@ -261,7 +251,7 @@ const Page = () => {
261251
<div>
262252
<Button
263253
type="button"
264-
onClick={() => fileInputRef.current?.click()} // Trigger file input on button click
254+
onClick={() => fileInputRef.current?.click()}
265255
className="rounded-md px-4 py-2 transition"
266256
>
267257
Choose files
@@ -277,9 +267,10 @@ const Page = () => {
277267
</fieldset>
278268
<Button
279269
onClick={handlePrint}
280-
className="w-fit rounded-md px-4 py-3"
270+
disabled={isUploading}
271+
className={`w-fit rounded-md px-4 py-3 ${isUploading ? "bg-gray-300" : ""}`}
281272
>
282-
Upload Papers
273+
{isUploading ? "Uploading..." : "Upload Papers"}
283274
</Button>
284275
</div>
285276
<div className="">

src/components/Footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export default function Footer() {
5858
width={24}
5959
/>
6060
</Link>
61-
<Link href="https://x.com/codechefvit">
61+
<Link href="https://x.com/codechefvit" className="pb-1.5">
6262
<Image
6363
src={isDarkMode ? x_twitter_icon_dark : x_twitter_icon}
6464
alt="x_twitter_icon"

0 commit comments

Comments
 (0)