Skip to content

Commit fcdc04a

Browse files
server side validation
1 parent 8bb5daf commit fcdc04a

File tree

2 files changed

+58
-78
lines changed

2 files changed

+58
-78
lines changed

src/app/api/mail/route.ts

Lines changed: 44 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -14,58 +14,26 @@ type MailOptions = {
1414
}[];
1515
};
1616

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-
}
17+
// Allowed MIME types for PDF and image files
18+
const ALLOWED_MIME_TYPES = ["application/pdf", "image/jpeg", "image/png", "image/gif"];
19+
const MAX_FILE_SIZE_MB = 5; // Limit file size to 5 MB
4420

4521
export async function POST(request: Request) {
4622
try {
4723
const formData = await request.formData();
4824

4925
const ip =
50-
request.headers.get("x-real-ip") ??
51-
request.headers.get("x-forwarded-for") ??
26+
request.headers.get("x-real-ip") ??
27+
request.headers.get("x-forwarded-for") ??
5228
request.headers.get("remote-addr");
5329

5430
if (!ip) {
5531
return NextResponse.json(
56-
{ message: "IP address not found" },
57-
{ status: 400 },
32+
{ message: "IP address not found" },
33+
{ status: 400 }
5834
);
5935
}
6036

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-
6937
const transporter = nodemailer.createTransport({
7038
host: process.env.EMAIL_HOST,
7139
port: parseInt(process!.env.EMAIL_PORT as string),
@@ -76,11 +44,10 @@ export async function POST(request: Request) {
7644
},
7745
});
7846

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() ?? '';
47+
const slot = formData.get("slot")?.toString() ?? "";
48+
const subject = formData.get("subject")?.toString() ?? "";
49+
const exam = formData.get("exam")?.toString() ?? "";
50+
const year = formData.get("year")?.toString() ?? "";
8451

8552
const htmlContent = `
8653
<div style="font-family: Arial, sans-serif; line-height: 1.5;">
@@ -92,32 +59,52 @@ export async function POST(request: Request) {
9259
</div>
9360
`;
9461

62+
const attachments: { filename: string; content: Buffer }[] = [];
63+
const files = formData.getAll("files");
64+
65+
// Validate files and prepare attachments
66+
for (const file of files) {
67+
if (file instanceof Blob) {
68+
const fileType = file.type;
69+
const fileSizeMB = file.size / (1024 * 1024); // Convert size to MB
70+
71+
if (!ALLOWED_MIME_TYPES.includes(fileType)) {
72+
return NextResponse.json(
73+
{ message: `File type not allowed: ${fileType}` },
74+
{ status: 400 }
75+
);
76+
}
77+
78+
if (fileSizeMB > MAX_FILE_SIZE_MB) {
79+
return NextResponse.json(
80+
{ message: `File ${file.name} exceeds the 5MB size limit` },
81+
{ status: 400 }
82+
);
83+
}
84+
85+
const buffer = await file.arrayBuffer();
86+
attachments.push({
87+
filename: (file as any).name,
88+
content: Buffer.from(buffer),
89+
});
90+
}
91+
}
92+
9593
const mailOptions: MailOptions = {
9694
from: process.env.EMAIL_USER!,
9795
to: process.env.EMAIL_USER!,
9896
subject: subject,
9997
html: htmlContent,
100-
attachments: [],
98+
attachments,
10199
};
102100

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-
113101
await transporter.sendMail(mailOptions);
114102

115103
return NextResponse.json({ message: "Email sent successfully!" }, { status: 200 });
116104
} catch (error) {
117105
return NextResponse.json(
118106
{ message: "Error sending email", error: error },
119-
{ status: 422 },
107+
{ status: 422 }
120108
);
121109
}
122110
}
123-

src/app/upload/page.tsx

Lines changed: 14 additions & 21 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";
@@ -61,22 +60,16 @@ const Page = () => {
6160
return;
6261
}
6362
}
64-
const zip = new JSZip();
63+
6564
const formData = new FormData();
66-
6765
for (const file of files) {
68-
zip.file(file.name, file);
69-
const content = await zip.generateAsync({ type: "blob" });
70-
71-
const arrayBuffer = await new Response(content).arrayBuffer();
72-
const uint8Array = new Uint8Array(arrayBuffer);
73-
74-
formData.append("zipFile", new Blob([uint8Array]), "files.zip");
75-
formData.append("slot", slot);
76-
formData.append("subject", subject);
77-
formData.append("exam", exam);
78-
formData.append("year", year);
66+
formData.append("files", file); // append each file
7967
}
68+
formData.append("slot", slot);
69+
formData.append("subject", subject);
70+
formData.append("exam", exam);
71+
formData.append("year", year);
72+
8073
try {
8174
const result = await toast.promise(
8275
(async () => {
@@ -88,7 +81,7 @@ const Page = () => {
8881
headers: {
8982
"Content-Type": "multipart/form-data",
9083
},
91-
},
84+
}
9285
);
9386
return response.data;
9487
} catch (error) {
@@ -99,16 +92,16 @@ const Page = () => {
9992
loading: "Sending papers",
10093
success: "Papers successfully sent",
10194
error: (err: ApiError) => err.message,
102-
},
95+
}
10396
);
10497
if (result?.message === "Email sent successfully!") {
105-
setTimeout(() => {
106-
router.push("/");
107-
}, 1500);
98+
// setTimeout(() => {
99+
// router.push("/");
100+
// }, 1500);
108101
}
109-
} catch (e) {
110-
}
102+
} catch (e) {}
111103
};
104+
112105

113106
const handleSubjectSelect = (value: string) => {
114107
setSubject(value);

0 commit comments

Comments
 (0)