feat: Implement College Marketplace with Authentication, Media Uploads, and Seller Controls#241
feat: Implement College Marketplace with Authentication, Media Uploads, and Seller Controls#241Rohank3 wants to merge 4 commits intoiiitl:mainfrom
Conversation
|
@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. |
WalkthroughImplements 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (7)
app/api/products/[id]/comments/route.ts (2)
4-5: Remove unused imports.
ICommentandUserare 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 ofanyfor parent comment lookup.The linter flags the use of
anyhere. You can improve type safety by leveraging theICommentinterface.♻️ 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.commentsis typed asIComment[]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
maxlengthon 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 onis_soldfor query performance.The
GET /api/productshandler filters byis_sold: falsewhen 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/productsmain endpoint is intentional, but consider explicit paths for protected sub-routes.The
/api/productsmain 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]/commentscurrently 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 RouterbodyParserconfig from this Route Handler.
config.api.bodyParseris a Pages Router API Route setting. Route Handlers already use the WebRequestAPI, and the official docs explicitly call out that you do not needbodyParserthere; they document route-segment exports likedynamic,revalidate,fetchCache,runtime, andpreferredRegioninstead. Keeping this block inapp/api/products/route.tsis 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
📒 Files selected for processing (20)
app/api/chat/messages/route.tsapp/api/products/[id]/available/route.tsapp/api/products/[id]/comments/route.tsapp/api/products/[id]/route.tsapp/api/products/[id]/sold/route.tsapp/api/products/route.tsapp/layout.jsxapp/marketplace/page.jsxapp/quick-reads/page.tsxapp/upload-notes/page.tsxapp/upload-papers/page.tsxcomponents/chat/ChatWidget.tsxcomponents/theme-toggler.tsxhooks/useChatMessages.tshooks/useSemesterAutofill.tslib/eventEmitter.tsmiddleware.tsmodel/Message.tsmodel/Product.tsnext.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
| offerPrice: | ||
| offerPrice !== undefined && offerPrice !== null && offerPrice !== '' | ||
| ? Number(offerPrice) | ||
| : undefined, |
There was a problem hiding this comment.
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.
| 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.
| 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 |
There was a problem hiding this comment.
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).
| if (seller) { | ||
| filter.seller = seller | ||
| } else { | ||
| filter.$or = [{ is_sold: false }, { is_sold: true, show_when_sold: true }] | ||
| } |
There was a problem hiding this comment.
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.
| .populate('seller', 'name email image') | ||
| .populate({ path: 'comments.user', select: 'name email image' }) | ||
| .populate({ path: 'comments.replies.user', select: 'name email image' }) |
There was a problem hiding this comment.
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.
| .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.
| 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 } | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧩 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
fiRepository: 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.
| 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, | ||
| }) |
There was a problem hiding this comment.
🧩 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.tsRepository: 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 -80Repository: 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.
| 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 | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how the backend handles sortOrder parameter
rg -n "sortOrder" app/api/products/route.ts -A5 -B2Repository: 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".
Resolves #20
Description
Key additions include:
Productmodel with fields for title, description, price, Cloudinary image URL, and seller references.seller === req.user.id).GET), creating (POST), updating (PUT), marking as sold (PATCH), and deleting (DELETE).Live Demo (if any)
Note for Maintainer
Productmodel was strictly aligned with the requested schema.403 Forbiddenresponses properly fire when an unauthorized user attempts to modify someone else's product.Checkout
Summary by CodeRabbit