Skip to content

Optimization/Architecture] Transition to AWS S3 Pre-signed URLs for Direct Client-Side File Uploads#310

Merged
Sachinchaurasiya360 merged 4 commits into
Sachinchaurasiya360:mainfrom
AdityaKumarBharadwaj:main
May 21, 2026
Merged

Optimization/Architecture] Transition to AWS S3 Pre-signed URLs for Direct Client-Side File Uploads#310
Sachinchaurasiya360 merged 4 commits into
Sachinchaurasiya360:mainfrom
AdityaKumarBharadwaj:main

Conversation

@AdityaKumarBharadwaj
Copy link
Copy Markdown

@AdityaKumarBharadwaj AdityaKumarBharadwaj commented May 17, 2026

This PR implements the architectural optimization outlined in #206. It entirely removes the server-side file buffering bottleneck by transitioning all file uploads (resumes, profile pictures, cover images, and attachments) to a direct client-to-S3 flow using pre-signed URLs.

By removing multer and preventing large files from occupying the Node.js heap, this update significantly improves the scalability and performance of the platform.

Key Benefits:

  • Zero Server Memory Pressure: Large PDFs and high-res images no longer pass through the Express server's memory.
  • 50% Bandwidth Reduction: Files are sent directly to AWS, avoiding the double-hop (Client -> Server -> S3).
  • Reduced Latency: Uploads execute in a single hop.
  • Enhanced Security: Removed the risky local-disk fallback (saveLocally()) that could lead to data loss or storage bloating on the production server.

Closes #206


Technical Changes

Backend:

  • Created generatePresignedUploadUrl leveraging the existing @aws-sdk/s3-request-presigner dependency in server/src/utils/s3.utils.ts.
  • Exposed a new secured endpoint: POST /api/upload/presigned-url.
  • Completely removed server/src/middleware/upload.middleware.ts (Multer disk storage).
  • Refactored upload.controller.ts to remove readAndCleanup() and uploadWithFallback(). Profile and resume endpoints now simply accept a JSON payload containing the final S3 URL to update the database.

Frontend:

  • Created a reusable uploadDirectToS3 utility to handle the 3-step upload flow (Get URL -> PUT to S3 -> Save to DB).
  • The utility seamlessly integrates with the existing HttpOnly cookie authentication (credentials: 'include').
  • Refactored legacy FormData uploads across the Student Profile, Recruiter Profile, ATS Score page, and Apply page to use the new direct S3 utility.

CRITICAL: Maintainer Action Required (AWS CORS)

Because the browser is now attempting to execute a PUT request directly to the S3 bucket from internhack.com (and localhost), you must update the CORS policy in the AWS Console.

If this is not added, the browser will block all user uploads. Please navigate to the S3 Bucket Permissions tab and append the following JSON to the Cross-origin resource sharing (CORS) configuration:

[
  {
    "AllowedHeaders": [
      "Content-Type"
    ],
    "AllowedMethods": [
      "PUT"
    ],
    "AllowedOrigins": [
      "[https://internhack.com](https://internhack.com)",
      "http://localhost:5173"
    ],
    "ExposeHeaders": []
  }
]

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Improvements**
  * Direct cloud uploads for profile pictures, cover images, resumes, and attachments for faster, more reliable transfers
  * Unified upload behavior across recruiter and student flows, applications, company logos, and ATS resume scoring
  * More robust handling of returned file URLs and automatic normalization for consistent display

* **Backend**
  * New presigned-url endpoint and server-side utilities to support secure client-to-cloud uploads
  * Upload endpoints now accept persisted file URLs and enforce resume limits and signed access

<!-- review_stack_entry_start -->

[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/Sachinchaurasiya360/InternHack/pull/310?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

- Removed multer and server-side file buffering
- Added /presigned-url endpoint
- Created frontend utility for direct S3 PUT requests with cookie support
- Refactored student, recruiter, apply, and ATS components
- Closes Sachinchaurasiya360#206
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

📝 Walkthrough

Walkthrough

This PR migrates uploads to a presigned-URL flow: clients request an S3 PUT URL, upload files directly to S3, then notify server endpoints to persist file metadata. Client pages use uploadDirectToS3; server adds presign helpers and refactors upload controllers/routes to accept and validate fileUrl.

Changes

Pre-signed URL Upload Architecture

Layer / File(s) Summary
Client upload utility foundation
client/src/utils/upload.ts, client/tsconfig.app.json
Adds uploadDirectToS3 which requests a presigned upload URL, PUTs the file to S3 with correct Content-Type, optionally POSTs metadata to backend endpoints, and re-throws logged errors; also adjusts TS path alias.
Client integration across pages
client/src/module/recruiter/profile/RecruiterProfilePage.tsx, client/src/module/student/profile/StudentProfilePage.tsx, client/src/module/student/applications/ApplyPage.tsx, client/src/module/student/ats/AtsScorePage.tsx, client/src/module/student/companies/AddCompanyPage.tsx
Pages now import and call uploadDirectToS3 for profile/cover images, resumes, custom-field attachments, and company logos; response parsing now normalizes multiple possible URL fields (profilePic, fileUrl, url) and updates local state/auth store accordingly.
Server S3 key & presign helpers
server/src/utils/s3.utils.ts
Adds createUniqueS3Key (sanitizes filenames, appends UUID under folder/userId/) and generatePresignedUploadUrl (returns a presigned PUT URL with a 300s TTL).
Server controller endpoint refactoring
server/src/module/upload/upload.controller.ts
Adds getPresignedUrl and rewrites profile-pic/cover-image/profile-resume handlers to accept validated fileUrl/metadata in JSON bodies, persist URLs to the user's Prisma record, delete previous objects when needed, enforce MAX_RESUMES, and return signed resume URLs.
Server routing updates
server/src/module/upload/upload.routes.ts
Applies authMiddleware to upload routes, adds POST /presigned-url, and replaces multer-based handlers with JSON-based controller calls (multer middleware removed elsewhere).

Sequence Diagram

sequenceDiagram
  participant Browser as Client Browser
  participant Server as InternHack Server
  participant S3 as AWS S3
  
  Browser->>Server: POST /api/upload/presigned-url {fileName,fileType,folder}
  Server->>Server: createUniqueS3Key, generatePresignedUploadUrl
  Server-->>Browser: {uploadUrl, fileUrl, fileKey}
  Browser->>S3: PUT uploadUrl (Content-Type: mime, file bytes)
  S3-->>Browser: 200 OK
  Browser->>Server: POST /api/upload{endpoint} {fileUrl, originalName, size, mimeType}
  Server->>Server: validate fileUrl origin, persist to Prisma, delete previous object if any
  Server-->>Browser: {user, file metadata}
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Suggested Labels

enhancement, level:intermediate, gssoc: intermediate

Poem

🐰 From temp files in a server den,
To presigned doors for files to sprint then,
A friendly PUT across the net,
The server saves just one small set—
Hooray, uploads now hop to S3 again!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main architectural change: transitioning from server-side file buffering to direct client-to-S3 uploads via pre-signed URLs.
Linked Issues check ✅ Passed The PR implements all core requirements from #206: eliminates server-side buffering, removes multer and fallback paths, generates pre-signed URLs, refactors endpoints to accept JSON fileUrls, and provides reusable uploadDirectToS3 utility.
Out of Scope Changes check ✅ Passed All changes align with the stated objective. The tsconfig.json path alias update and addition of uploadDirectToS3 imports across components are direct supports for the main feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
client/src/module/student/profile/StudentProfilePage.tsx (1)

483-508: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing client-side validation for resume uploads.

handleResumeUpload calls uploadDirectToS3 without validating file size or type, unlike the image upload handlers which properly validate. The UI states "PDF only, max 10 MB" but this isn't enforced in code.

🛡️ Proposed fix to add validation
+  const MAX_RESUME_SIZE = 5 * 1024 * 1024; // 5 MB per guidelines
+
   const handleResumeUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
     const file = e.target.files?.[0];
     if (!file) return;
+
+    if (file.size > MAX_RESUME_SIZE) {
+      toast.error("Resume must be under 5 MB");
+      if (resumeInputRef.current) resumeInputRef.current.value = "";
+      return;
+    }
+    if (file.type !== "application/pdf") {
+      toast.error("Only PDF files are allowed");
+      if (resumeInputRef.current) resumeInputRef.current.value = "";
+      return;
+    }
+
     setUploadingResume(true);

Also update line 1254 to reflect the correct limit: <p className="text-[10px] font-mono text-stone-500">PDF only, max 5 MB each.</p>

As per coding guidelines: "File upload validation must validate client-side with size ≤ 5 MB and allowed types"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/module/student/profile/StudentProfilePage.tsx` around lines 483 -
508, The resume upload handler handleResumeUpload lacks client-side validation:
before calling uploadDirectToS3, validate the selected file (from
e.target.files[0] / resumeInputRef) for allowed type (PDF MIME and/or .pdf
extension) and enforce size ≤ 5 MB; if invalid, setUploadingResume(false), clear
resumeInputRef.current.value, and surface a descriptive toast.error (do not call
uploadDirectToS3). After a successful upload keep the existing setForm/syncUser
flow and toast.success; also update the UI text near the resume input in
StudentProfilePage to state "PDF only, max 5 MB each." to match the enforced
limit.
server/src/module/upload/upload.controller.ts (2)

16-24: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Reject client-supplied asset URLs and derive them server-side.

These handlers persist any fileUrl the caller sends, and previously stored values later flow into deleteFile(). That lets a user point their profile data at another S3 object and trigger deletion on the next update, or store a crafted /uploads/... path and hit the local unlink branch. Persist a canonical URL derived from a validated server-issued fileKey instead, and only delete objects under the current user's allowed prefix.

Also applies to: 58-70, 84-96, 110-127

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/module/upload/upload.controller.ts` around lines 16 - 24, Reject
using any client-supplied fileUrl and instead persist and operate only on a
server-derived canonical URL computed from a validated server-issued fileKey;
update the handlers that currently accept fileUrl (the handlers covering lines
58-70, 84-96, 110-127) to validate the incoming fileKey, construct the canonical
URL/server-side key, save that canonical URL to the user record, and pass it to
deleteFile. In deleteFile (and helper getS3KeyFromUrl) enforce strict deletion
rules: only allow S3 deletions for keys under the app's tenant/user prefix and
only allow local deletions inside the UPLOADS_DIR canonical path (resolve and
verify the path is within UPLOADS_DIR); reject or log and skip any
client-provided or out-of-scope paths. Ensure all places that previously
persisted fileUrl now store the server-derived canonical URL derived from
fileKey.

117-129: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Evict the old resume only after the DB write succeeds.

deleteFile(oldest) runs before prisma.user.update(...). If the update fails, the user can lose the object while their profile still points at it. Capture the evicted URL first, update Prisma, then delete the old object after persistence succeeds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/module/upload/upload.controller.ts` around lines 117 - 129, The
code currently calls deleteFile(oldest) before persisting the new resumes array,
risking data loss if prisma.user.update(...) fails; instead, determine the
evicted URL (oldest) from updatedResumes (using MAX_RESUMES, fileUrl logic),
perform prisma.user.update(...) with the new resumes array first, and only after
the update resolves successfully call deleteFile(evictedUrl) to remove the old
object; ensure you handle the case when there is no evictedUrl (i.e.,
updatedResumes length < MAX_RESUMES) and surface/log any deletion errors without
rolling back the successful DB update.
🧹 Nitpick comments (3)
client/src/utils/upload.ts (1)

29-30: ⚡ Quick win

Extract error details from failed responses.

Generic error messages make debugging difficult. The server may return useful error information (e.g., file too large, invalid type, auth failure) that gets lost.

♻️ Proposed improvement for better error messages
-    if (!presignRes.ok) throw new Error('Failed to get upload URL');
+    if (!presignRes.ok) {
+      const errBody = await presignRes.json().catch(() => ({}));
+      throw new Error(errBody.message || `Failed to get upload URL (${presignRes.status})`);
+    }
     const { uploadUrl, fileUrl } = await presignRes.json();

Apply similar pattern to Lines 41 and 60.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/utils/upload.ts` around lines 29 - 30, When a fetch response like
presignRes.ok is false, capture and include the server's error body in the
thrown Error instead of a generic message: read the response body (attempt JSON
parse, fallback to text), extract a helpful message or fields, and include it in
the Error thrown for the presign step (where const { uploadUrl, fileUrl } is
used) and apply the same pattern to the other fetch responses in this file (the
upload/finalize responses—e.g., uploadRes and finalizeRes or similarly named
variables at the later checks) so failures surface server-provided details
(status code + body) in the thrown error.
client/src/module/student/ats/AtsScorePage.tsx (1)

273-273: 💤 Low value

File size limit exceeds coding guidelines.

MAX_SIZE is set to 10 MB, but coding guidelines specify file uploads should be ≤ 5 MB. Consider aligning with the guideline or documenting the exception if 10 MB is intentional for ATS analysis.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/module/student/ats/AtsScorePage.tsx` at line 273, The constant
MAX_SIZE in AtsScorePage.tsx is set to 10 MB which violates the ≤5 MB upload
guideline; change MAX_SIZE to 5 * 1024 * 1024 (5 MB) to comply, or if 10 MB is
intentional for ATS analysis add a clear comment above MAX_SIZE explaining the
exception and reference the guideline ID/approval so reviewers know it's
deliberate.
server/src/module/upload/upload.routes.ts (1)

13-20: ⚡ Quick win

Add request validation middleware to the new JSON upload routes.

After removing multer, these endpoints only run authMiddleware before controller logic. Add route-level validation for fileName, fileType, folder, and fileUrl so malformed or cross-namespace payloads are rejected before they reach the controller.

As per coding guidelines, Routes layer must contain Express router with middleware chain (auth, role, usage-limit, validation).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/module/upload/upload.routes.ts` around lines 13 - 20, The new
JSON-only routes (uploadRouter.post("/profile-pic",
uploadController.uploadProfilePic), uploadRouter.post("/cover-image",
uploadController.uploadCoverImage), uploadRouter.post("/profile-resume",
uploadController.uploadProfileResume), uploadRouter.delete("/profile-resume",
uploadController.deleteProfileResume)) lack route-level request validation and
only run authMiddleware; add a validation middleware into the router chain that
verifies required fields (fileName: non-empty string, fileType: allowed
mime-types/string, folder: non-empty string and matches the authenticated user’s
namespace/owner from authMiddleware, fileUrl: well-formed URL) and rejects
malformed or cross-namespace payloads before calling the controller; update the
routes to use authMiddleware, then the new file-upload request validator (and
existing role/usage-limit middleware if applicable) so controllers only receive
validated input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/utils/upload.ts`:
- Around line 8-10: The uploadDirectToS3 function currently accepts any file
without client-side validation; add validation inside uploadDirectToS3 (which
receives UploadProfileParams) to enforce file.size ≤ 5 * 1024 * 1024 bytes and
restrict mime types to the same allowedTypes list/pattern used in
DynamicFieldRenderer.tsx, returning/rejecting with a clear validation error
before any network call if checks fail; ensure the function documents/throws a
ValidationError (or rejects the promise) so callers can handle it consistently.

In `@server/src/module/upload/upload.controller.ts`:
- Around line 34-45: Currently folder and fileType are accepted directly from
the client (variables folder, fileType) and used to build fileKey and call
generatePresignedUploadUrl, which lets clients control S3 prefixes and MIME
types; change the handler in upload.controller.ts to enforce a server-side
allowlist for folder (map allowed flow names to fixed prefixes and override the
client-supplied folder) and validate fileType against a per-flow whitelist of
acceptable MIME types (reject with res.status(400) if not allowed). Continue
sanitizing fileName and using req.user.id and crypto.randomUUID() for
uniqueness, but do not include client-supplied folder or arbitrary fileType in
the generated fileKey or presigned URL call; instead pass the canonical prefix
and a validated MIME type to generatePresignedUploadUrl.

---

Outside diff comments:
In `@client/src/module/student/profile/StudentProfilePage.tsx`:
- Around line 483-508: The resume upload handler handleResumeUpload lacks
client-side validation: before calling uploadDirectToS3, validate the selected
file (from e.target.files[0] / resumeInputRef) for allowed type (PDF MIME and/or
.pdf extension) and enforce size ≤ 5 MB; if invalid, setUploadingResume(false),
clear resumeInputRef.current.value, and surface a descriptive toast.error (do
not call uploadDirectToS3). After a successful upload keep the existing
setForm/syncUser flow and toast.success; also update the UI text near the resume
input in StudentProfilePage to state "PDF only, max 5 MB each." to match the
enforced limit.

In `@server/src/module/upload/upload.controller.ts`:
- Around line 16-24: Reject using any client-supplied fileUrl and instead
persist and operate only on a server-derived canonical URL computed from a
validated server-issued fileKey; update the handlers that currently accept
fileUrl (the handlers covering lines 58-70, 84-96, 110-127) to validate the
incoming fileKey, construct the canonical URL/server-side key, save that
canonical URL to the user record, and pass it to deleteFile. In deleteFile (and
helper getS3KeyFromUrl) enforce strict deletion rules: only allow S3 deletions
for keys under the app's tenant/user prefix and only allow local deletions
inside the UPLOADS_DIR canonical path (resolve and verify the path is within
UPLOADS_DIR); reject or log and skip any client-provided or out-of-scope paths.
Ensure all places that previously persisted fileUrl now store the server-derived
canonical URL derived from fileKey.
- Around line 117-129: The code currently calls deleteFile(oldest) before
persisting the new resumes array, risking data loss if prisma.user.update(...)
fails; instead, determine the evicted URL (oldest) from updatedResumes (using
MAX_RESUMES, fileUrl logic), perform prisma.user.update(...) with the new
resumes array first, and only after the update resolves successfully call
deleteFile(evictedUrl) to remove the old object; ensure you handle the case when
there is no evictedUrl (i.e., updatedResumes length < MAX_RESUMES) and
surface/log any deletion errors without rolling back the successful DB update.

---

Nitpick comments:
In `@client/src/module/student/ats/AtsScorePage.tsx`:
- Line 273: The constant MAX_SIZE in AtsScorePage.tsx is set to 10 MB which
violates the ≤5 MB upload guideline; change MAX_SIZE to 5 * 1024 * 1024 (5 MB)
to comply, or if 10 MB is intentional for ATS analysis add a clear comment above
MAX_SIZE explaining the exception and reference the guideline ID/approval so
reviewers know it's deliberate.

In `@client/src/utils/upload.ts`:
- Around line 29-30: When a fetch response like presignRes.ok is false, capture
and include the server's error body in the thrown Error instead of a generic
message: read the response body (attempt JSON parse, fallback to text), extract
a helpful message or fields, and include it in the Error thrown for the presign
step (where const { uploadUrl, fileUrl } is used) and apply the same pattern to
the other fetch responses in this file (the upload/finalize responses—e.g.,
uploadRes and finalizeRes or similarly named variables at the later checks) so
failures surface server-provided details (status code + body) in the thrown
error.

In `@server/src/module/upload/upload.routes.ts`:
- Around line 13-20: The new JSON-only routes (uploadRouter.post("/profile-pic",
uploadController.uploadProfilePic), uploadRouter.post("/cover-image",
uploadController.uploadCoverImage), uploadRouter.post("/profile-resume",
uploadController.uploadProfileResume), uploadRouter.delete("/profile-resume",
uploadController.deleteProfileResume)) lack route-level request validation and
only run authMiddleware; add a validation middleware into the router chain that
verifies required fields (fileName: non-empty string, fileType: allowed
mime-types/string, folder: non-empty string and matches the authenticated user’s
namespace/owner from authMiddleware, fileUrl: well-formed URL) and rejects
malformed or cross-namespace payloads before calling the controller; update the
routes to use authMiddleware, then the new file-upload request validator (and
existing role/usage-limit middleware if applicable) so controllers only receive
validated input.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da01f91b-709f-4bfe-994c-17058b7a92d5

📥 Commits

Reviewing files that changed from the base of the PR and between 3552efa and 3a99af9.

📒 Files selected for processing (9)
  • client/src/module/recruiter/profile/RecruiterProfilePage.tsx
  • client/src/module/student/applications/ApplyPage.tsx
  • client/src/module/student/ats/AtsScorePage.tsx
  • client/src/module/student/profile/StudentProfilePage.tsx
  • client/src/utils/upload.ts
  • server/src/middleware/upload.middleware.ts
  • server/src/module/upload/upload.controller.ts
  • server/src/module/upload/upload.routes.ts
  • server/src/utils/s3.utils.ts
💤 Files with no reviewable changes (1)
  • server/src/middleware/upload.middleware.ts

Comment thread client/src/utils/upload.ts Outdated
Comment on lines +8 to +10
export const uploadDirectToS3 = async ({ file, folder, endpoint, authToken }: UploadProfileParams) => {
try {
const apiUrl = (import.meta.env.VITE_API_URL as string | undefined) ?? "http://localhost:3000";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing client-side file validation before upload.

Per coding guidelines, file uploads must validate client-side with size ≤ 5 MB and allowed types. This utility accepts any file without validation, which could lead to:

  1. Wasted bandwidth uploading files that the server will reject
  2. Inconsistent validation across callers
🛡️ Proposed fix to add validation
+const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
+const ALLOWED_TYPES: Record<string, string[]> = {
+  resumes: ['application/pdf'],
+  'profile-pics': ['image/jpeg', 'image/jpg', 'image/png'],
+  'cover-images': ['image/jpeg', 'image/jpg', 'image/png'],
+};
+
 export const uploadDirectToS3 = async ({ file, folder, endpoint, authToken }: UploadProfileParams) => {
   try {
+    if (file.size > MAX_FILE_SIZE) {
+      throw new Error(`File exceeds ${MAX_FILE_SIZE / (1024 * 1024)} MB limit`);
+    }
+    const allowed = ALLOWED_TYPES[folder];
+    if (allowed && !allowed.includes(file.type)) {
+      throw new Error(`Invalid file type. Allowed: ${allowed.join(', ')}`);
+    }
+
     const apiUrl = (import.meta.env.VITE_API_URL as string | undefined) ?? "http://localhost:3000";

As per coding guidelines: "File upload validation must validate client-side with size ≤ 5 MB and allowed types following the pattern in DynamicFieldRenderer.tsx"

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const uploadDirectToS3 = async ({ file, folder, endpoint, authToken }: UploadProfileParams) => {
try {
const apiUrl = (import.meta.env.VITE_API_URL as string | undefined) ?? "http://localhost:3000";
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
const ALLOWED_TYPES: Record<string, string[]> = {
resumes: ['application/pdf'],
'profile-pics': ['image/jpeg', 'image/jpg', 'image/png'],
'cover-images': ['image/jpeg', 'image/jpg', 'image/png'],
};
export const uploadDirectToS3 = async ({ file, folder, endpoint, authToken }: UploadProfileParams) => {
try {
if (file.size > MAX_FILE_SIZE) {
throw new Error(`File exceeds ${MAX_FILE_SIZE / (1024 * 1024)} MB limit`);
}
const allowed = ALLOWED_TYPES[folder];
if (allowed && !allowed.includes(file.type)) {
throw new Error(`Invalid file type. Allowed: ${allowed.join(', ')}`);
}
const apiUrl = (import.meta.env.VITE_API_URL as string | undefined) ?? "http://localhost:3000";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/utils/upload.ts` around lines 8 - 10, The uploadDirectToS3
function currently accepts any file without client-side validation; add
validation inside uploadDirectToS3 (which receives UploadProfileParams) to
enforce file.size ≤ 5 * 1024 * 1024 bytes and restrict mime types to the same
allowedTypes list/pattern used in DynamicFieldRenderer.tsx, returning/rejecting
with a clear validation error before any network call if checks fail; ensure the
function documents/throws a ValidationError (or rejects the promise) so callers
can handle it consistently.

Comment on lines +34 to 45
const { fileName, fileType, folder = "uploads" } = req.body;
if (!fileName || !fileType) {
return res.status(400).json({ message: "fileName and fileType are required" });
}

// Validate actual file content matches claimed MIME type
await validateOrReject(
req.file.path,
["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "image/jpeg", "image/png", "image/webp", "text/plain"],
"File content does not match allowed file types",
);
const cleanFileName = fileName.replace(/[^a-zA-Z0-9.-]/g, "_");
const uniqueId = crypto.randomUUID();
const fileKey = `${folder}/${req.user.id}/${uniqueId}-${cleanFileName}`;

const buffer = readAndCleanup(req.file.path);
const url = await uploadWithFallback(buffer, "uploads", userId, req.file.originalname, req.file.mimetype);
const uploadUrl = await generatePresignedUploadUrl(fileKey, fileType);
const fileUrl = `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${fileKey}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not let the client choose arbitrary S3 prefixes and content types.

folder and fileType are fully client-controlled here, so any authenticated user can mint upload URLs for arbitrary bucket namespaces and MIME types. Restrict folder to a small server-side allowlist per use case and validate fileType against the formats each flow actually supports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/module/upload/upload.controller.ts` around lines 34 - 45,
Currently folder and fileType are accepted directly from the client (variables
folder, fileType) and used to build fileKey and call generatePresignedUploadUrl,
which lets clients control S3 prefixes and MIME types; change the handler in
upload.controller.ts to enforce a server-side allowlist for folder (map allowed
flow names to fixed prefixes and override the client-supplied folder) and
validate fileType against a per-flow whitelist of acceptable MIME types (reject
with res.status(400) if not allowed). Continue sanitizing fileName and using
req.user.id and crypto.randomUUID() for uniqueness, but do not include
client-supplied folder or arbitrary fileType in the generated fileKey or
presigned URL call; instead pass the canonical prefix and a validated MIME type
to generatePresignedUploadUrl.

…ogo sync

- Structural rewrite of uploadDirectToS3 utility to provide clean conditional contracts
- Linked resolved standalone S3 fileUrl records into company payload submissions
- Eliminated dead authorization parameters from type files
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
client/src/utils/upload.ts (1)

7-23: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add client-side file validation before presign/upload calls.

This helper still uploads without enforcing size/type constraints, so invalid files go over the network before rejection.

Suggested fix
 interface UploadProfileParams {
   file: File;
   folder: 'resumes' | 'profile-pics' | 'cover-images' | 'company-logos';
   endpoint?: '/profile-resume' | '/profile-pic' | '/cover-image';
 }
 
+const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
+const ALLOWED_TYPES: Record<UploadProfileParams["folder"], string[]> = {
+  resumes: ["application/pdf"],
+  "profile-pics": ["image/jpeg", "image/jpg", "image/png", "image/webp"],
+  "cover-images": ["image/jpeg", "image/jpg", "image/png", "image/webp"],
+  "company-logos": ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/svg+xml"],
+};
+
 export const uploadDirectToS3 = async ({ file, folder, endpoint }: UploadProfileParams) => {
   try {
+    if (file.size > MAX_FILE_SIZE) {
+      throw new Error("File size must be 5 MB or less");
+    }
+    const allowed = ALLOWED_TYPES[folder];
+    if (!allowed.includes(file.type)) {
+      throw new Error(`Invalid file type for ${folder}`);
+    }
+
     const apiUrl = (import.meta.env.VITE_API_URL as string | undefined) ?? "http://localhost:3000";
As per coding guidelines: "File upload validation must validate client-side with size ≤ 5 MB and allowed types following the pattern in `DynamicFieldRenderer.tsx`."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/utils/upload.ts` around lines 7 - 23, The uploadDirectToS3 helper
currently calls the backend presign endpoint without client-side validation; add
a pre-check inside uploadDirectToS3 (before calling fetch) that enforces
file.size ≤ 5 * 1024 * 1024 and that file.type matches the same allowed
MIME/type pattern used in DynamicFieldRenderer.tsx, and if validation fails
return/reject with a clear error (or throw) to prevent the presign/upload flow
from proceeding; reference the uploadDirectToS3 function and UploadProfileParams
to locate where to insert these checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/src/utils/s3.utils.ts`:
- Around line 92-100: The generatePresignedUploadUrl function currently passes
process.env.AWS_S3_BUCKET directly into PutObjectCommand which can be undefined;
add a guard at the start of generatePresignedUploadUrl that checks
process.env.AWS_S3_BUCKET (or a resolved constant) and throws a clear, early
Error if missing; then use that validated bucket value when constructing the
PutObjectCommand (referencing generatePresignedUploadUrl, PutObjectCommand,
s3Client, and getSignedUrl) so signing never proceeds with an undefined Bucket.

---

Duplicate comments:
In `@client/src/utils/upload.ts`:
- Around line 7-23: The uploadDirectToS3 helper currently calls the backend
presign endpoint without client-side validation; add a pre-check inside
uploadDirectToS3 (before calling fetch) that enforces file.size ≤ 5 * 1024 *
1024 and that file.type matches the same allowed MIME/type pattern used in
DynamicFieldRenderer.tsx, and if validation fails return/reject with a clear
error (or throw) to prevent the presign/upload flow from proceeding; reference
the uploadDirectToS3 function and UploadProfileParams to locate where to insert
these checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b6ee44cd-5d4b-4e1e-bda9-b14f3abd9a96

📥 Commits

Reviewing files that changed from the base of the PR and between 3a99af9 and 2a1a93d.

📒 Files selected for processing (5)
  • client/src/module/student/companies/AddCompanyPage.tsx
  • client/src/utils/upload.ts
  • client/tsconfig.app.json
  • server/src/module/upload/upload.controller.ts
  • server/src/utils/s3.utils.ts
✅ Files skipped from review due to trivial changes (1)
  • client/tsconfig.app.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/src/module/upload/upload.controller.ts

Comment on lines +92 to +100
export const generatePresignedUploadUrl = async (fileKey: string, fileType: string) => {
const command = new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET,
Key: fileKey,
ContentType: fileType, // Enforces that the client uploads the correct file type
});

// URL expires in 5 minutes (300 seconds)
const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 300 });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard missing S3 bucket config before signing upload URLs.

Bucket can be undefined here, which turns misconfiguration into opaque runtime failures on the upload path. Fail fast with a clear error.

Suggested fix
 export const generatePresignedUploadUrl = async (fileKey: string, fileType: string) => {
+  if (!BUCKET) {
+    throw new Error("AWS_S3_BUCKET is not configured");
+  }
+
   const command = new PutObjectCommand({
-    Bucket: process.env.AWS_S3_BUCKET,
+    Bucket: BUCKET,
     Key: fileKey,
     ContentType: fileType, // Enforces that the client uploads the correct file type
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/utils/s3.utils.ts` around lines 92 - 100, The
generatePresignedUploadUrl function currently passes process.env.AWS_S3_BUCKET
directly into PutObjectCommand which can be undefined; add a guard at the start
of generatePresignedUploadUrl that checks process.env.AWS_S3_BUCKET (or a
resolved constant) and throws a clear, early Error if missing; then use that
validated bucket value when constructing the PutObjectCommand (referencing
generatePresignedUploadUrl, PutObjectCommand, s3Client, and getSignedUrl) so
signing never proceeds with an undefined Bucket.

@Abfa41 Abfa41 added level:advanced Complex implementation or logic type:performance Performance optimization changes type:security Security-related fixes or improvements gssoc:approved Approved for GSSoC scoring labels May 18, 2026
@Sachinchaurasiya360 Sachinchaurasiya360 merged commit 75c1305 into Sachinchaurasiya360:main May 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc:approved Approved for GSSoC scoring level:advanced Complex implementation or logic type:performance Performance optimization changes type:security Security-related fixes or improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Optimization/Architecture] Transition to AWS S3 Pre-signed URLs for Direct Client-Side File Uploads

3 participants