Skip to content

feat: Implement College Marketplace with Authentication, Media Uploads, and Seller Controls#241

Open
Rohank3 wants to merge 4 commits intoiiitl:mainfrom
Rohank3:marketplace
Open

feat: Implement College Marketplace with Authentication, Media Uploads, and Seller Controls#241
Rohank3 wants to merge 4 commits intoiiitl:mainfrom
Rohank3:marketplace

Conversation

@Rohank3
Copy link
Copy Markdown
Contributor

@Rohank3 Rohank3 commented Apr 12, 2026

Resolves #20

Description

What is the purpose of this pull request?
This pull request fully implements the College Marketplace module, replacing the previous dummy UI with a fully functional system aligned with the issue requirements.

Key additions include:

  • Product Listing Schema: Full backend support via Mongoose Product model with fields for title, description, price, Cloudinary image URL, and seller references.
  • Authentication & Authorization: Integrated JWT verification to ensure only authenticated users can create listings, and strictly restricted modification, deletion, and "mark as sold" actions to the original seller (seller === req.user.id).
  • RESTful API Endpoints: Fully implemented endpoints for fetching available products (GET), creating (POST), updating (PUT), marking as sold (PATCH), and deleting (DELETE).
  • Seller Controls: Sellers can manage their inventory, mark items out of stock (with an option to keep a "Sold Out" badge visible or hide it entirely), set bulk discounts, and restore items back to availability.
  • Form Validations: Added complete frontend validation for data integrity (standardized email Regex, required fields constraint, strict price/bulk discount validators).

Live Demo (if any)

image image image image

Note for Maintainer

  • The Product model was strictly aligned with the requested schema.
  • Added localized state management to manage modal UX seamlessly.
  • Tested endpoints to ensure that 403 Forbidden responses properly fire when an unauthorized user attempts to modify someone else's product.

Checkout

  • I have read all the contributor guidelines for the repo.

Summary by CodeRabbit

  • New Features
    • Fully functional Student Marketplace with product listing creation, editing, and deletion
    • Product image upload capability
    • Product search and sorting by creation date or price
    • Pagination for browsing listings
    • Comments and offers system on products
    • Mark products as sold or available again
    • Cloudinary image optimization integration

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 12, 2026

@Rohank3 is attempting to deploy a commit to the mrimmortal09's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

Walkthrough

Implements a complete college marketplace backend with new product CRUD API endpoints, a Mongoose Product schema with nested comments/replies, Cloudinary image integration, JWT authentication/authorization checks, and a full-featured marketplace UI supporting search, sorting, pagination, product creation/editing/deletion, offer/comment submission, and seller-side inventory management.

Changes

Cohort / File(s) Summary
Product API Routes
app/api/products/route.ts, app/api/products/[id]/route.ts, app/api/products/[id]/sold/route.ts, app/api/products/[id]/available/route.ts, app/api/products/[id]/comments/route.ts
New endpoints for listing products, creating/editing/deleting products, marking products sold/available, and posting comments/offers. All include JWT authentication, seller ownership validation, database operations, input validation, and error handling. Image uploads use Cloudinary.
Data Model
model/Product.ts
New Product schema with TypeScript interfaces defining product structure, nested comment/reply subdocuments, seller reference, image validation, and field constraints.
Marketplace UI Implementation
app/marketplace/page.jsx
Complete rewrite from static JSON-backed UI to dynamic client-side marketplace consuming /api/products endpoints. Includes product listing/creation/editing/deletion, search (debounced), sorting, pagination, detailed product view, comments/offers panel, image upload with preview, form validation, toast notifications, and multiple modal overlays.
Configuration & Middleware Updates
next.config.ts, middleware.ts
Added Cloudinary remote image pattern to next.config.ts. Updated middleware.ts to include /api/products in public endpoints for both API access and unauthenticated GET requests.
Minor Formatting & Cleanup
app/api/chat/messages/route.ts, app/layout.jsx, app/quick-reads/page.tsx, app/upload-notes/page.tsx, app/upload-papers/page.tsx, components/chat/ChatWidget.tsx, components/theme-toggler.tsx, hooks/useChatMessages.ts, hooks/useSemesterAutofill.ts, lib/eventEmitter.ts, model/Message.ts
Removed trailing blank lines and reformatted multi-line JSX/function signatures for consistency. No functional changes.

Sequence Diagram

sequenceDiagram
    actor User as Client (Browser)
    participant UI as Marketplace UI
    participant Auth as JWT Auth
    participant API as Product API
    participant DB as MongoDB
    participant Cloud as Cloudinary
    
    User->>UI: Browse/Create/Edit Product
    
    rect rgb(100, 150, 200, 0.5)
    note over UI,Cloud: Product Creation Flow
    UI->>Auth: Authenticate (JWT)
    Auth-->>UI: User ID
    UI->>Cloud: Upload Image
    Cloud-->>UI: Image URL
    UI->>API: POST /api/products (with image URL)
    API->>Auth: Verify JWT & User
    API->>DB: Create Product + Save
    DB-->>API: Product Created
    API-->>UI: 201 + Product
    UI-->>User: Show Success
    end
    
    rect rgb(150, 100, 200, 0.5)
    note over UI,DB: Product Updates Flow
    User->>UI: Edit/Mark Sold/Delete
    UI->>Auth: Verify Ownership
    Auth-->>UI: Authorized
    UI->>API: PUT/PATCH/DELETE /api/products/:id
    API->>DB: Fetch Product & Validate Seller
    alt Seller Match
        DB-->>API: Product Retrieved
        API->>DB: Update/Delete Product
        alt Has Image (Delete)
            API->>Cloud: Delete Old Image
        end
        DB-->>API: Operation Complete
        API-->>UI: 200/204 + Result
    else Seller Mismatch
        API-->>UI: 403 Forbidden
    end
    UI-->>User: Show Result
    end
    
    rect rgb(200, 150, 100, 0.5)
    note over UI,DB: Browse & Comment Flow
    User->>UI: Browse Products / Add Offer
    UI->>API: GET /api/products (with search/sort)
    API->>DB: Query Products + Populate
    DB-->>API: Product List
    API-->>UI: 200 + Products
    UI-->>User: Display Marketplace
    User->>UI: Submit Comment/Offer
    UI->>API: POST /api/products/:id/comments
    API->>Auth: Verify User
    API->>DB: Add Comment + Nested Reply
    DB-->>API: Updated Product
    API-->>UI: 201 + Product
    UI-->>User: Show Comment
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

backend, feature, marketplace

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Multiple formatting-only changes present (layout reformatting, blank line removals) that are tangential to the marketplace feature. Unclear if these whitespace adjustments are intentional refactoring or should be separated. Consider isolating formatting changes into a separate commit or PR to keep marketplace feature changes focused and easier to review.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: implementing a college marketplace with authentication, media uploads, and seller controls.
Linked Issues check ✅ Passed All requirements from issue #20 are met: Product model with required fields, JWT authentication, Cloudinary integration, all specified API endpoints (GET, POST, PUT, PATCH, DELETE), seller authorization checks (403 on mismatch), and comprehensive error handling.

✏️ 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: 8

🧹 Nitpick comments (7)
app/api/products/[id]/comments/route.ts (2)

4-5: Remove unused imports.

IComment and User are imported but never used. The linter has flagged these.

♻️ Remove unused imports
-import Product, { IComment } from '@/model/Product'
-import User from '@/model/User'
+import Product from '@/model/Product'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/products/`[id]/comments/route.ts around lines 4 - 5, The imports
IComment and User are unused in app/api/products/[id]/comments/route.ts; remove
them to satisfy the linter by editing the import lines—keep the default Product
import if it’s used (e.g., Product) and delete the named IComment and the User
import; ensure any references to IComment or User in this file are also removed
or replaced before committing.

73-76: Use proper typing instead of any for parent comment lookup.

The linter flags the use of any here. You can improve type safety by leveraging the IComment interface.

♻️ Suggested type-safe approach
-      const parentComment = product.comments.find(
-        (c: any) => c._id && c._id.toString() === parentCommentId
-      ) as any
+      const parentComment = product.comments.find(
+        (c) => c._id && c._id.toString() === parentCommentId
+      )

Since product.comments is typed as IComment[] from the schema, the callback parameter should infer correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/products/`[id]/comments/route.ts around lines 73 - 76, Replace the
use of `any` in the parent comment lookup with the `IComment` type: change the
find callback to accept (c: IComment) and type the result as `const
parentComment: IComment | undefined = product.comments.find(...)`; remove the
`as any` cast and keep the same equality check (c._id?.toString() ===
parentCommentId) so the linter is satisfied and you preserve correct typings for
`product.comments` and `parentComment`.
model/Product.ts (2)

87-109: Comments array could grow unbounded — consider pagination or size limits.

Embedding comments and replies directly in the product document is convenient but can lead to document size issues (MongoDB 16MB limit) for popular products with many comments. Consider:

  • Adding a maxlength on the comments array
  • Moving to a separate Comments collection with references
  • Implementing pagination on comment retrieval

This is acceptable for an MVP but worth monitoring.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/Product.ts` around lines 87 - 109, The embedded comments array on the
Product schema (field name "comments" in model/Product.ts) can grow unbounded
and risk MongoDB document size limits; either enforce a maximum array length
(add a validator/maxlength on the "comments" array) and limit replies per
comment, or refactor into a separate Comment model (create a Comments collection
with a productId reference, move nested replies to their own sub-docs or a
Replies collection) and update retrieval code to paginate (implement skip/limit
or cursor-based pagination in the functions that load product comments); choose
one approach and update schema definitions (the "comments" field and its nested
"replies") and related retrieval APIs to enforce the limit or perform paginated
queries.

79-86: Consider adding an index on is_sold for query performance.

The GET /api/products handler filters by is_sold: false when fetching available products. Adding an index on this field would improve query performance as the product collection grows.

♻️ Suggested change
   is_sold: {
     type: Boolean,
     default: false,
+    index: true,
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/Product.ts` around lines 79 - 86, Add an index on the Product model's
is_sold field to speed queries that filter for available products; update the
Product schema (the is_sold field definition in the Product model) to declare an
index (either by adding index: true on is_sold or calling schema.index({
is_sold: 1 })) and ensure the index is created (e.g., via mongoose autoIndex or
by running createIndexes) so GET /api/products queries that filter { is_sold:
false } use the index.
app/marketplace/page.jsx (1)

282-284: Phone validation is limited to 10-digit Indian numbers.

The regex /^\d{10}$/ only accepts exactly 10 digits, which works for Indian mobile numbers but excludes international formats or landlines with area codes. This may be intentional for a college marketplace but worth noting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/marketplace/page.jsx` around lines 282 - 284, The phone number validation
in the formData.phone check uses the regex pattern /^\d{10}$/ which only accepts
exactly 10 consecutive digits, limiting support to Indian mobile numbers and
excluding international formats. Either document this intentional limitation if
the marketplace is Indian-specific, or update the regex pattern to support other
phone number formats (such as allowing country codes, hyphens, spaces, or
varying digit lengths) depending on your marketplace requirements.
middleware.ts (1)

154-163: Document that public access to /api/products main endpoint is intentional, but consider explicit paths for protected sub-routes.

The /api/products main GET endpoint is correctly documented as public in its implementation. However, the prefix-based matching in middleware (path.startsWith('/api/products')) broadly exempts all GET requests under /api/products/*, including future sub-routes. While sub-routes like /api/products/[id]/comments currently only implement POST (with explicit auth checks), adding a GET handler would unintentionally become public due to the middleware exemption.

Consider either: (1) explicitly listing protected sub-route paths separately in the middleware, or (2) documenting this design decision so future developers understand that GET handlers on protected sub-routes must include inline auth verification.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middleware.ts` around lines 154 - 163, The middleware currently treats any
GET whose path.startsWith('/api/products') as public via publicGetEndpoints and
isPublicGetRequest, which unintentionally exempts future sub-routes; update the
middleware to either restrict the rule to the exact '/api/products' path (remove
prefix matching) or explicitly enumerate protected sub-route paths under
publicGetEndpoints (e.g., add '/api/products/:id' variants) and add a clear
comment explaining that publicGetEndpoints only intends the main '/api/products'
GET to be public so future GET handlers on product sub-routes must implement
inline auth checks.
app/api/products/route.ts (1)

14-18: Remove the Pages Router bodyParser config from this Route Handler.

config.api.bodyParser is a Pages Router API Route setting. Route Handlers already use the Web Request API, and the official docs explicitly call out that you do not need bodyParser there; they document route-segment exports like dynamic, revalidate, fetchCache, runtime, and preferredRegion instead. Keeping this block in app/api/products/route.ts is dead config and makes the upload path look more customized than it is. (nextjs.org)

♻️ Proposed fix
 export const dynamic = 'force-dynamic'
-
-export const config = {
-  api: {
-    bodyParser: false,
-  },
-}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/products/route.ts` around lines 14 - 18, Remove the Pages
Router-specific bodyParser config from this Route Handler by deleting the export
const config object that sets api.bodyParser (the block exporting config with
api.bodyParser = false) in app/api/products/route.ts; Route Handlers use the Web
Request API and should not export the Pages Router bodyParser setting, so simply
remove that export to avoid dead/misleading config.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/products/`[id]/comments/route.ts:
- Around line 89-92: The current assignment for offerPrice blindly converts any
truthy value to Number(), which can produce NaN; update the logic around
offerPrice (in the route handler that prepares the comment payload) to first
coerce to string and trim, then parse with parseFloat and only use the numeric
value if Number.isFinite(parsed) && parsed > 0; otherwise set offerPrice to
undefined (or return a 400 validation error if you prefer strict validation).
Reference the existing offerPrice variable and the payload construction where
offerPrice is assigned to locate and replace the conversion logic.

In `@app/api/products/route.ts`:
- Around line 30-34: Request query parsing currently allows NaN, zero, negative
values and passes raw sort keys into Mongo; clamp and validate those values
before using them: ensure parseInt results for page and limit are positive
integers (default to 1 and 20 respectively), enforce a min limit of 1 and max of
50, coerce negative or zero page/limit to their defaults, and compute skip using
max(0, (page-1)*limit). Also whitelist allowed sort fields (e.g., an array like
allowedSortFields) and only set sortKey = allowedSortFields.includes(sortBy) ?
sortBy : 'created_at' before calling .sort(), while keeping sortOrder mapped to
1 or -1; update the same logic where pagination/sorting is used (the code around
variables page, limit, sortBy, sortOrder and the later .sort()/skip/limit
usage).
- Around line 40-44: The current branch in app/api/products/route.ts allows a
provided seller param to override the public visibility filter (filter.$or with
is_sold/show_when_sold), letting callers enumerate hidden sold items; restore
the visibility predicate when building filter even if seller is present by
always including the existing $or clause and AND-ing seller into filter, and add
upfront validation of the seller param format (e.g., validate ID shape in the
request handler before using it) so malformed IDs return a 400; move any
owner-only “my listings” behavior out of this public GET route into an
authenticated endpoint that explicitly checks ownership.
- Around line 66-68: The public product listing route currently populates seller
and comment user subdocuments including email; remove email from those Mongoose
projections so PII isn't returned: update the populate calls that reference
'seller', 'comments.user', and 'comments.replies.user' in route.ts to only
select non-PII fields (e.g., name and image) and ensure no other populate or
select elsewhere in the handler returns email or other PII for unauthenticated
requests.
- Around line 202-228: After a successful uploadOnCloudinary (cloudinaryResult)
you must guard against orphaned remote images by wrapping the Product.create
call in a try-catch; on error call await
deleteOnCloudinary(cloudinaryResult.public_id).catch(() => {}) and rethrow or
return the error response. Locate the Product.create invocation and replace it
with this pattern (same as used in app/api/notes/route.ts): try { await
Product.create(...) } catch (err) { await
deleteOnCloudinary(cloudinaryResult.public_id).catch(() => {}); throw err or
return the existing 500 response } so uploaded images are cleaned up when
creation/validation fails.
- Around line 122-160: FormData values may be strings or Blobs, and
parseFloat/parseInt silently accept partial inputs, so update the
extraction/validation around title, description, priceRaw, quantityRaw,
contact_info, and file: when reading from formData (where you currently use
title, description, priceRaw, quantityRaw, contact_info, file) first check types
(typeof value === 'string' for text fields and value instanceof File/Blob for
image) before calling .trim(); for numeric fields do strict format validation
(e.g. test priceRaw against a decimal-regex like /^\d+(\.\d+)?$/ and quantityRaw
against /^\d+$/) and only then parse with Number/parseInt, returning 400 on
mismatch so inputs like "999.99xyz" are rejected; ensure file is validated as a
File (or Blob with acceptable mime/type) rather than just truthy.

In `@app/marketplace/page.jsx`:
- Around line 564-576: The isOwner function contains a misleading fallback
comparing sellerId to session.user.email which can never match; remove the email
comparison so isOwner only checks sellerId against session.user.id and
session.user._id (i.e., delete the sellerId === session.user.email clause) and
ensure the function still returns a boolean based on sellerId ===
session.user.id || sellerId === session.user._id.
- Around line 876-887: The select options send numeric sort orders ("1"/"-1")
while the backend expects "asc"/"desc"; update the option values in the select
(and keep the existing onChange handler that splits on ':') to use "asc" or
"desc" (e.g., "created_at:desc", "created_at:asc", "price:asc", "price:desc") so
setSortBy and setSortOrder receive the backend-compatible strings; ensure any
code reading sortOrder (state variable set by setSortOrder) now contains
"asc"/"desc".

---

Nitpick comments:
In `@app/api/products/`[id]/comments/route.ts:
- Around line 4-5: The imports IComment and User are unused in
app/api/products/[id]/comments/route.ts; remove them to satisfy the linter by
editing the import lines—keep the default Product import if it’s used (e.g.,
Product) and delete the named IComment and the User import; ensure any
references to IComment or User in this file are also removed or replaced before
committing.
- Around line 73-76: Replace the use of `any` in the parent comment lookup with
the `IComment` type: change the find callback to accept (c: IComment) and type
the result as `const parentComment: IComment | undefined =
product.comments.find(...)`; remove the `as any` cast and keep the same equality
check (c._id?.toString() === parentCommentId) so the linter is satisfied and you
preserve correct typings for `product.comments` and `parentComment`.

In `@app/api/products/route.ts`:
- Around line 14-18: Remove the Pages Router-specific bodyParser config from
this Route Handler by deleting the export const config object that sets
api.bodyParser (the block exporting config with api.bodyParser = false) in
app/api/products/route.ts; Route Handlers use the Web Request API and should not
export the Pages Router bodyParser setting, so simply remove that export to
avoid dead/misleading config.

In `@app/marketplace/page.jsx`:
- Around line 282-284: The phone number validation in the formData.phone check
uses the regex pattern /^\d{10}$/ which only accepts exactly 10 consecutive
digits, limiting support to Indian mobile numbers and excluding international
formats. Either document this intentional limitation if the marketplace is
Indian-specific, or update the regex pattern to support other phone number
formats (such as allowing country codes, hyphens, spaces, or varying digit
lengths) depending on your marketplace requirements.

In `@middleware.ts`:
- Around line 154-163: The middleware currently treats any GET whose
path.startsWith('/api/products') as public via publicGetEndpoints and
isPublicGetRequest, which unintentionally exempts future sub-routes; update the
middleware to either restrict the rule to the exact '/api/products' path (remove
prefix matching) or explicitly enumerate protected sub-route paths under
publicGetEndpoints (e.g., add '/api/products/:id' variants) and add a clear
comment explaining that publicGetEndpoints only intends the main '/api/products'
GET to be public so future GET handlers on product sub-routes must implement
inline auth checks.

In `@model/Product.ts`:
- Around line 87-109: The embedded comments array on the Product schema (field
name "comments" in model/Product.ts) can grow unbounded and risk MongoDB
document size limits; either enforce a maximum array length (add a
validator/maxlength on the "comments" array) and limit replies per comment, or
refactor into a separate Comment model (create a Comments collection with a
productId reference, move nested replies to their own sub-docs or a Replies
collection) and update retrieval code to paginate (implement skip/limit or
cursor-based pagination in the functions that load product comments); choose one
approach and update schema definitions (the "comments" field and its nested
"replies") and related retrieval APIs to enforce the limit or perform paginated
queries.
- Around line 79-86: Add an index on the Product model's is_sold field to speed
queries that filter for available products; update the Product schema (the
is_sold field definition in the Product model) to declare an index (either by
adding index: true on is_sold or calling schema.index({ is_sold: 1 })) and
ensure the index is created (e.g., via mongoose autoIndex or by running
createIndexes) so GET /api/products queries that filter { is_sold: false } use
the index.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0426461-2db1-422b-b6bc-962d06212e6e

📥 Commits

Reviewing files that changed from the base of the PR and between 00e129c and 61264a6.

📒 Files selected for processing (20)
  • app/api/chat/messages/route.ts
  • app/api/products/[id]/available/route.ts
  • app/api/products/[id]/comments/route.ts
  • app/api/products/[id]/route.ts
  • app/api/products/[id]/sold/route.ts
  • app/api/products/route.ts
  • app/layout.jsx
  • app/marketplace/page.jsx
  • app/quick-reads/page.tsx
  • app/upload-notes/page.tsx
  • app/upload-papers/page.tsx
  • components/chat/ChatWidget.tsx
  • components/theme-toggler.tsx
  • hooks/useChatMessages.ts
  • hooks/useSemesterAutofill.ts
  • lib/eventEmitter.ts
  • middleware.ts
  • model/Message.ts
  • model/Product.ts
  • next.config.ts
💤 Files with no reviewable changes (5)
  • app/api/chat/messages/route.ts
  • lib/eventEmitter.ts
  • components/chat/ChatWidget.tsx
  • model/Message.ts
  • hooks/useChatMessages.ts

Comment on lines +89 to +92
offerPrice:
offerPrice !== undefined && offerPrice !== null && offerPrice !== ''
? Number(offerPrice)
: undefined,
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 | 🟡 Minor

Validate offerPrice is a valid positive number.

The current check allows any truthy value to be converted to Number(), which could result in NaN being stored if a non-numeric string is passed.

🛡️ Proposed fix
         offerPrice:
-          offerPrice !== undefined && offerPrice !== null && offerPrice !== ''
-            ? Number(offerPrice)
+          offerPrice !== undefined && offerPrice !== null && offerPrice !== '' &&
+          !isNaN(Number(offerPrice)) && Number(offerPrice) >= 0
+            ? Number(offerPrice)
             : undefined,
📝 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
offerPrice:
offerPrice !== undefined && offerPrice !== null && offerPrice !== ''
? Number(offerPrice)
: undefined,
offerPrice:
offerPrice !== undefined && offerPrice !== null && offerPrice !== '' &&
!isNaN(Number(offerPrice)) && Number(offerPrice) >= 0
? Number(offerPrice)
: undefined,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/products/`[id]/comments/route.ts around lines 89 - 92, The current
assignment for offerPrice blindly converts any truthy value to Number(), which
can produce NaN; update the logic around offerPrice (in the route handler that
prepares the comment payload) to first coerce to string and trim, then parse
with parseFloat and only use the numeric value if Number.isFinite(parsed) &&
parsed > 0; otherwise set offerPrice to undefined (or return a 400 validation
error if you prefer strict validation). Reference the existing offerPrice
variable and the payload construction where offerPrice is assigned to locate and
replace the conversion logic.

Comment thread app/api/products/route.ts
Comment on lines +30 to +34
const page = parseInt(searchParams.get('page') || '1', 10)
const limit = Math.min(parseInt(searchParams.get('limit') || '20', 10), 50)
const search = searchParams.get('search') || ''
const sortBy = searchParams.get('sortBy') || 'created_at'
const sortOrder = searchParams.get('sortOrder') === 'asc' ? 1 : -1
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

Validate and whitelist the query params before using them in Mongo.

Lines 30-34 accept NaN, 0, and negative values, and sortBy flows straight into .sort(). That makes pagination metadata unreliable (limit=0 yields Infinity pages, negative pages produce negative skip values) and lets callers request arbitrary sort keys. Clamp page/limit to positive ints and whitelist the sortable fields.

Also applies to: 62-85

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/products/route.ts` around lines 30 - 34, Request query parsing
currently allows NaN, zero, negative values and passes raw sort keys into Mongo;
clamp and validate those values before using them: ensure parseInt results for
page and limit are positive integers (default to 1 and 20 respectively), enforce
a min limit of 1 and max of 50, coerce negative or zero page/limit to their
defaults, and compute skip using max(0, (page-1)*limit). Also whitelist allowed
sort fields (e.g., an array like allowedSortFields) and only set sortKey =
allowedSortFields.includes(sortBy) ? sortBy : 'created_at' before calling
.sort(), while keeping sortOrder mapped to 1 or -1; update the same logic where
pagination/sorting is used (the code around variables page, limit, sortBy,
sortOrder and the later .sort()/skip/limit usage).

Comment thread app/api/products/route.ts
Comment on lines +40 to +44
if (seller) {
filter.seller = seller
} else {
filter.$or = [{ is_sold: false }, { is_sold: true, show_when_sold: true }]
}
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

Do not let seller bypass the hidden-sold filter.

When seller is present, this branch drops the is_sold/show_when_sold predicate entirely. Because the route is public, any caller can request GET /api/products?seller=<otherUserId> and enumerate sold listings that were supposed to stay hidden. Keep the visibility filter for public reads, and use an authenticated owner-only path for “my listings”. If you keep the seller param here, validate its format up front as well so bad ids return 400 instead of bubbling into a cast error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/products/route.ts` around lines 40 - 44, The current branch in
app/api/products/route.ts allows a provided seller param to override the public
visibility filter (filter.$or with is_sold/show_when_sold), letting callers
enumerate hidden sold items; restore the visibility predicate when building
filter even if seller is present by always including the existing $or clause and
AND-ing seller into filter, and add upfront validation of the seller param
format (e.g., validate ID shape in the request handler before using it) so
malformed IDs return a 400; move any owner-only “my listings” behavior out of
this public GET route into an authenticated endpoint that explicitly checks
ownership.

Comment thread app/api/products/route.ts
Comment on lines +66 to +68
.populate('seller', 'name email image')
.populate({ path: 'comments.user', select: 'name email image' })
.populate({ path: 'comments.replies.user', select: 'name email image' })
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

Strip email addresses from the public user projections.

This endpoint is unauthenticated, but it currently returns seller.email, comments.user.email, and comments.replies.user.email for every listing. That leaks account PII even though the listing already has a dedicated contact_info field for marketplace contact.

🛡️ Proposed fix
-        .populate('seller', 'name email image')
-        .populate({ path: 'comments.user', select: 'name email image' })
-        .populate({ path: 'comments.replies.user', select: 'name email image' })
+        .populate('seller', 'name image')
+        .populate({ path: 'comments.user', select: 'name image' })
+        .populate({ path: 'comments.replies.user', select: 'name image' })
📝 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
.populate('seller', 'name email image')
.populate({ path: 'comments.user', select: 'name email image' })
.populate({ path: 'comments.replies.user', select: 'name email image' })
.populate('seller', 'name image')
.populate({ path: 'comments.user', select: 'name image' })
.populate({ path: 'comments.replies.user', select: 'name image' })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/products/route.ts` around lines 66 - 68, The public product listing
route currently populates seller and comment user subdocuments including email;
remove email from those Mongoose projections so PII isn't returned: update the
populate calls that reference 'seller', 'comments.user', and
'comments.replies.user' in route.ts to only select non-PII fields (e.g., name
and image) and ensure no other populate or select elsewhere in the handler
returns email or other PII for unauthenticated requests.

Comment thread app/api/products/route.ts
Comment on lines +122 to +160
const title = (formData.get('title') as string)?.trim()
const description = (formData.get('description') as string)?.trim()
const priceRaw = formData.get('price') as string
const contact_info = (formData.get('contact_info') as string)?.trim()
const quantityRaw = formData.get('quantity') as string
const file = formData.get('image') as File | null

// Validate required fields
const missingFields: string[] = []
if (!title) missingFields.push('title')
if (!description) missingFields.push('description')
if (!priceRaw) missingFields.push('price')
if (!contact_info) missingFields.push('contact_info')
if (!file) missingFields.push('image')

if (missingFields.length > 0) {
return NextResponse.json(
{ message: `Required fields missing: ${missingFields.join(', ')}` },
{ status: 400 }
)
}

const price = parseFloat(priceRaw)
if (isNaN(price) || price < 0) {
return NextResponse.json(
{ message: 'Price must be a valid non-negative number' },
{ status: 400 }
)
}

let quantity = 1
if (quantityRaw) {
quantity = parseInt(quantityRaw, 10)
if (isNaN(quantity) || quantity < 1) {
return NextResponse.json(
{ message: 'Quantity must be at least 1' },
{ status: 400 }
)
}
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

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and examine the code at lines 122-160
if [ -f "app/api/products/route.ts" ]; then
  echo "=== File exists. Reading lines 122-160 ==="
  sed -n '122,160p' app/api/products/route.ts | cat -n
  echo ""
  echo "=== Full context: lines 100-170 ==="
  sed -n '100,170p' app/api/products/route.ts | cat -n
else
  echo "File not found at app/api/products/route.ts"
  # Try to find it
  find . -name "route.ts" -path "*/products/*" 2>/dev/null
fi

Repository: iiitl/student-hub

Length of output: 4280


Harden multipart field parsing before validation.

req.formData() resolves to a FormData object, and FormData values can be strings or blobs, so these as string assertions perform no runtime type validation. Additionally, parseFloat() and parseInt() are permissive parsers that silently stop at invalid characters—a malformed multipart body like price=999.99xyz will have the invalid suffix truncated instead of rejected. The current isNaN() checks only catch complete parsing failures, not partially valid corrupted input.

🛡️ Suggested hardening
-    const title = (formData.get('title') as string)?.trim()
-    const description = (formData.get('description') as string)?.trim()
-    const priceRaw = formData.get('price') as string
-    const contact_info = (formData.get('contact_info') as string)?.trim()
-    const quantityRaw = formData.get('quantity') as string
-    const file = formData.get('image') as File | null
+    const getStringField = (name: string) => {
+      const value = formData.get(name)
+      return typeof value === 'string' ? value.trim() : ''
+    }
+    const title = getStringField('title')
+    const description = getStringField('description')
+    const priceRaw = getStringField('price')
+    const contact_info = getStringField('contact_info')
+    const quantityRaw = getStringField('quantity')
+    const imageEntry = formData.get('image')
+    const file = imageEntry instanceof File ? imageEntry : null
@@
-    const price = parseFloat(priceRaw)
-    if (isNaN(price) || price < 0) {
+    if (!/^\d+(\.\d+)?$/.test(priceRaw)) {
       return NextResponse.json(
         { message: 'Price must be a valid non-negative number' },
         { status: 400 }
       )
     }
+    const price = Number(priceRaw)
@@
-      quantity = parseInt(quantityRaw, 10)
-      if (isNaN(quantity) || quantity < 1) {
+      if (!/^[1-9]\d*$/.test(quantityRaw)) {
         return NextResponse.json(
           { message: 'Quantity must be at least 1' },
           { status: 400 }
         )
       }
+      quantity = Number(quantityRaw)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/products/route.ts` around lines 122 - 160, FormData values may be
strings or Blobs, and parseFloat/parseInt silently accept partial inputs, so
update the extraction/validation around title, description, priceRaw,
quantityRaw, contact_info, and file: when reading from formData (where you
currently use title, description, priceRaw, quantityRaw, contact_info, file)
first check types (typeof value === 'string' for text fields and value
instanceof File/Blob for image) before calling .trim(); for numeric fields do
strict format validation (e.g. test priceRaw against a decimal-regex like
/^\d+(\.\d+)?$/ and quantityRaw against /^\d+$/) and only then parse with
Number/parseInt, returning 400 on mismatch so inputs like "999.99xyz" are
rejected; ensure file is validated as a File (or Blob with acceptable mime/type)
rather than just truthy.

Comment thread app/api/products/route.ts
Comment on lines +202 to +228
let cloudinaryResult: { secure_url: string; public_id: string } | null =
null
try {
cloudinaryResult = (await uploadOnCloudinary(tempFilePath)) as {
secure_url: string
public_id: string
} | null
} finally {
await fs.unlink(tempFilePath).catch(() => {})
}

if (!cloudinaryResult) {
return NextResponse.json(
{ message: 'Failed to upload image to Cloudinary' },
{ status: 500 }
)
}

const product = await Product.create({
title,
description,
price,
image_url: cloudinaryResult.secure_url,
contact_info,
quantity,
seller: userId,
})
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect whether the repository already exposes a Cloudinary cleanup helper
# that this route can reuse.
fd -i 'cloudinary'
rg -n 'uploadOnCloudinary|public_id|destroy|delete.*cloudinary|cloudinary\.uploader'

Repository: iiitl/student-hub

Length of output: 1677


🏁 Script executed:

#!/bin/bash
# Read the deleteOnCloudinary implementation
cat -n helpers/cloudinary.ts

Repository: iiitl/student-hub

Length of output: 2453


🏁 Script executed:

#!/bin/bash
# Read the Product model to check validators at lines 33-72
cat -n model/Product.ts | head -80

Repository: iiitl/student-hub

Length of output: 2447


🏁 Script executed:

#!/bin/bash
# Check how the notes route handles this scenario - see if there's pre-validation or cleanup pattern
cat -n app/api/notes/route.ts | sed -n '90,145p'

Repository: iiitl/student-hub

Length of output: 1961


Add cleanup handler when Product.create() fails.

Once uploadOnCloudinary() succeeds, any later Product.create() failure leaves the remote image orphaned. The Product schema enforces maxlength validators on title (40 chars), description (1000 chars), and contact_info (200 chars) at lines 38, 44, and 71 of model/Product.ts, so validation failures are reachable today. The deleteOnCloudinary() helper already exists in helpers/cloudinary.ts (line 72) and is successfully used in app/api/notes/route.ts (line 138) with the exact pattern needed: wrap Product.create() in try-catch and call await deleteOnCloudinary(cloudinaryResult.public_id).catch(() => {}) on error. Copy this cleanup pattern from the notes route.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/products/route.ts` around lines 202 - 228, After a successful
uploadOnCloudinary (cloudinaryResult) you must guard against orphaned remote
images by wrapping the Product.create call in a try-catch; on error call await
deleteOnCloudinary(cloudinaryResult.public_id).catch(() => {}) and rethrow or
return the error response. Locate the Product.create invocation and replace it
with this pattern (same as used in app/api/notes/route.ts): try { await
Product.create(...) } catch (err) { await
deleteOnCloudinary(cloudinaryResult.public_id).catch(() => {}); throw err or
return the existing 500 response } so uploaded images are cleaned up when
creation/validation fails.

Comment thread app/marketplace/page.jsx
Comment on lines +564 to +576
const isOwner = (product) => {
if (!session?.user) return false
const seller = product.seller
if (!seller) return false
// seller can be populated object or plain id string
const sellerId = typeof seller === 'object' ? seller._id : seller
// session.user may have id or _id
return (
sellerId === session.user.id ||
sellerId === session.user._id ||
sellerId === session.user.email // fallback match
)
}
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 | 🟡 Minor

Remove misleading email fallback in ownership check.

The fallback sellerId === session.user.email compares an ObjectId string to an email address, which will never match. This dead code path is misleading and should be removed.

🧹 Proposed fix
   const isOwner = (product) => {
     if (!session?.user) return false
     const seller = product.seller
     if (!seller) return false
     // seller can be populated object or plain id string
     const sellerId = typeof seller === 'object' ? seller._id : seller
     // session.user may have id or _id
     return (
       sellerId === session.user.id ||
-      sellerId === session.user._id ||
-      sellerId === session.user.email // fallback match
+      sellerId === session.user._id
     )
   }
📝 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
const isOwner = (product) => {
if (!session?.user) return false
const seller = product.seller
if (!seller) return false
// seller can be populated object or plain id string
const sellerId = typeof seller === 'object' ? seller._id : seller
// session.user may have id or _id
return (
sellerId === session.user.id ||
sellerId === session.user._id ||
sellerId === session.user.email // fallback match
)
}
const isOwner = (product) => {
if (!session?.user) return false
const seller = product.seller
if (!seller) return false
// seller can be populated object or plain id string
const sellerId = typeof seller === 'object' ? seller._id : seller
// session.user may have id or _id
return (
sellerId === session.user.id ||
sellerId === session.user._id
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/marketplace/page.jsx` around lines 564 - 576, The isOwner function
contains a misleading fallback comparing sellerId to session.user.email which
can never match; remove the email comparison so isOwner only checks sellerId
against session.user.id and session.user._id (i.e., delete the sellerId ===
session.user.email clause) and ensure the function still returns a boolean based
on sellerId === session.user.id || sellerId === session.user._id.

Comment thread app/marketplace/page.jsx
Comment on lines +876 to +887
onChange={(e) => {
const [sb, so] = e.target.value.split(':')
setSortBy(sb)
setSortOrder(so)
}}
className="appearance-none px-4 py-3 pr-10 rounded-xl bg-white dark:bg-[#0c1630] border border-gray-200 dark:border-blue-900/40 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500/60 text-sm cursor-pointer"
>
<option value="created_at:-1">Newest first</option>
<option value="created_at:1">Oldest first</option>
<option value="price:1">Price: Low to High</option>
<option value="price:-1">Price: High to Low</option>
</select>
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how the backend handles sortOrder parameter
rg -n "sortOrder" app/api/products/route.ts -A5 -B2

Repository: iiitl/student-hub

Length of output: 689


Backend sort order format incompatibility – options sending "1" will sort incorrectly.

The frontend sends -1 or 1 as sort order values, but the backend checks for the string "asc" (line 34 of app/api/products/route.ts):

const sortOrder = searchParams.get('sortOrder') === 'asc' ? 1 : -1

This causes a bug: when the frontend sends "1" for "Oldest first" and "Price: Low to High", the backend receives "1" !== "asc", defaults to -1, and reverses the sort direction.

Update the frontend to match the backend's expected format: send "asc" or "desc" strings instead of "-1"/"1", or update the backend parser to accept the numeric string format.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/marketplace/page.jsx` around lines 876 - 887, The select options send
numeric sort orders ("1"/"-1") while the backend expects "asc"/"desc"; update
the option values in the select (and keep the existing onChange handler that
splits on ':') to use "asc" or "desc" (e.g., "created_at:desc",
"created_at:asc", "price:asc", "price:desc") so setSortBy and setSortOrder
receive the backend-compatible strings; ensure any code reading sortOrder (state
variable set by setSortOrder) now contains "asc"/"desc".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat]: Implement College Marketplace Backend (Similar to OLX)

1 participant