Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 23 additions & 145 deletions backend/src/config/socket.js
Original file line number Diff line number Diff line change
@@ -1,159 +1,37 @@
import { Server } from 'socket.io';
import { createSocketOptions } from './socketOptions.js';
import { socketAuthMiddleware } from '../middleware/socketAuth.js';
import { setupSocketHandlers } from '../services/socketServiceFirebase.js';
import { presenceService } from '../services/presenceService.js';
import socketOptions from './socketOptions.js';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: socketOptions.js does not export a default value, so this default import will fail module loading with an import/export mismatch error. Import the named factory (e.g., createSocketOptions) and pass its returned object to Server instead. [api mismatch]

Severity Level: Critical 🚨
- ❌ Backend server fails to start due to socketOptions import error.
- ❌ All REST and websocket features unavailable in all environments.
Steps of Reproduction ✅
1. Start the backend server so that `backend/src/index.js` is executed; it imports `{
initializeSocket }` from `./config/socket.js` at `backend/src/index.js:48` (verified via
BulkRead).

2. During module linking, Node loads `backend/src/config/socket.js`, which executes the
statement `import socketOptions from './socketOptions.js';` at
`backend/src/config/socket.js:2`.

3. Node then loads `backend/src/config/socketOptions.js`, which only defines named exports
(`SOCKET_TRANSPORTS` at line 15, `getAllowedSocketOrigins` at 23, `isSocketOriginAllowed`
at 38, `createSocketOptions` at 52) and does not contain any `export default` (verified
via BulkRead of that file).

4. Because `socketOptions.js` has no default export, the ESM import in `socket.js:2` fails
with an error like "The requested module './socketOptions.js' does not provide an export
named 'default'", preventing `backend/src/index.js` from completing evaluation and
stopping the server from starting.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** backend/src/config/socket.js
**Line:** 2:2
**Comment:**
	*Api Mismatch: `socketOptions.js` does not export a default value, so this default import will fail module loading with an import/export mismatch error. Import the named factory (e.g., `createSocketOptions`) and pass its returned object to `Server` instead.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


let io = null;
let io;

export const initializeSocket = (server) => {
io = new Server(server, createSocketOptions());
io.engine.on('connection_error', (error) => {
console.error('❌ Socket transport connection error:', {
code: error.code,
message: error.message,
transport: error.req?._query?.transport || 'unknown'
});
});
export const initSocket = (server) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The exported initializer name was changed to initSocket, but the app bootstrap imports initializeSocket; this breaks server startup because the named export no longer matches what callers import. Keep backward-compatible export naming or update all callers consistently. [api mismatch]

Severity Level: Critical 🚨
- ❌ Socket server never initializes; real-time features completely disabled.
- ⚠️ Startup error masks deeper issues in socket handlers implementation.
Steps of Reproduction ✅
1. `backend/src/index.js` imports the initializer as `import { initializeSocket } from
'./config/socket.js';` at line 48 (confirmed via BulkRead).

2. The implementation file `backend/src/config/socket.js` instead defines `export const
initSocket = (server) => { ... }` at line 6 and does not export any symbol named
`initializeSocket` (verified via BulkRead of that file).

3. When Node links `backend/src/index.js`, it attempts to resolve the named export
`initializeSocket` from `./config/socket.js`, but finds only `initSocket`, causing an ESM
import error: "The requested module './config/socket.js' does not provide an export named
'initializeSocket'".

4. This import/export mismatch occurs before `startServer()` (defined at
`backend/src/index.js:58`) can be executed, so the HTTP server is never initialized and no
Socket.IO listeners are registered.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** backend/src/config/socket.js
**Line:** 6:6
**Comment:**
	*Api Mismatch: The exported initializer name was changed to `initSocket`, but the app bootstrap imports `initializeSocket`; this breaks server startup because the named export no longer matches what callers import. Keep backward-compatible export naming or update all callers consistently.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

io = new Server(server, socketOptions);

// Authentication middleware
io.use(socketAuthMiddleware);
io.on('connection', (socket) => {
console.log(`User connected: ${socket.id}`);

// Connection handler
io.on('connection', async (socket) => {
const initialTransport = socket.conn.transport.name;

if (process.env.NODE_ENV !== 'production') {
console.log(
`🔌 Socket connected using ${initialTransport} ` +
`(socketId=${socket.id})`
);

socket.conn.once('upgrade', (transport) => {
console.log(
`⬆️ Socket transport upgraded from ${initialTransport} ` +
`to ${transport.name} (socketId=${socket.id})`
);
});
}

// Track user presence
try {
await presenceService.setOnline(socket.user.uid, socket.user);
} catch (error) {
console.error(`Presence setOnline failed for ${socket.user.uid}:`, error);
}

// Join user's personal room for DMs
socket.join(`user:${socket.user.uid}`);

// Join global presence room (for general presence updates)
socket.join('global');

// Broadcast user online status only to global room
io.to('global').emit('user_online', {
uid: socket.user.uid,
name: socket.user.name,
timestamp: new Date()
});

// Setup all socket event handlers
setupSocketHandlers(io, socket);

// Handle channel presence subscription
socket.on('join_channel', async (channelId) => {
if (channelId) {
socket.join(`channel:${channelId}`);
await presenceService.joinRoom(socket.user.uid, `channel:${channelId}`);
console.log(`${socket.user.name} joined channel presence: ${channelId}`);

// Notify channel members
io.to(`channel:${channelId}`).emit('user_joined_channel', {
uid: socket.user.uid,
name: socket.user.name,
channelId,
timestamp: new Date()
// --- Start Real-time Collaboration Logic ---
socket.on('proposal:join', ({ proposalId }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The join handler destructures { proposalId } directly from the event payload, so a malformed emit (e.g., undefined/null) triggers a runtime TypeError before the handler can recover. Guard the payload shape before destructuring to avoid crashing handler execution on bad client input. [type error]

Severity Level: Critical 🚨
- ❌ Malformed socket payload can crash Node process via uncaughtException.
- ⚠️ Single malicious client can repeatedly trigger backend denial-of-service.
- ⚠️ Reduces robustness against buggy or out-of-date socket clients.
Steps of Reproduction ✅
1. The backend initializes Socket.IO by calling `initializeSocket(httpServer);` from
`startServer()` in `backend/src/index.js:111`, which invokes `initSocket` from
`backend/src/config/socket.js` (once the export name mismatch described in suggestion 2 is
corrected).

2. In `backend/src/config/socket.js`, the connection handler registers
`socket.on('proposal:join', ({ proposalId }) => { ... });` at line 13, destructuring
`proposalId` directly from the first argument without prior validation.

3. From any Socket.IO client (e.g., browser console using the Socket.IO client library),
connect to the server and emit a malformed event such as `socket.emit('proposal:join')` or
`socket.emit('proposal:join', null)`, so the handler receives `undefined`/`null` as its
first argument.

4. When the listener executes, it attempts to evaluate `({ proposalId }) => { ... }` with
an `undefined` argument, causing a `TypeError: Cannot destructure property 'proposalId' of
'undefined' as it is undefined`; this exception is not caught inside the handler and thus
triggers the global `process.on("uncaughtException", ...)` at
`backend/src/index.js:419-5`, which logs the error, closes `httpServer`, shuts down Redis
via `redisManager.shutdown()`, and exits the process, resulting in a full backend outage
from a single malformed socket event.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** backend/src/config/socket.js
**Line:** 13:13
**Comment:**
	*Type Error: The join handler destructures `{ proposalId }` directly from the event payload, so a malformed emit (e.g., `undefined`/`null`) triggers a runtime TypeError before the handler can recover. Guard the payload shape before destructuring to avoid crashing handler execution on bad client input.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

socket.join(proposalId);
console.log(`User ${socket.id} joined cooperative proposal room: ${proposalId}`);
});
}
});

socket.on('leave_channel', async (channelId) => {
if (channelId) {
socket.leave(`channel:${channelId}`);
await presenceService.leaveRoom(socket.user.uid, `channel:${channelId}`);
console.log(`${socket.user.name} left channel presence: ${channelId}`);

// Notify channel members
io.to(`channel:${channelId}`).emit('user_left_channel', {
uid: socket.user.uid,
name: socket.user.name,
channelId,
timestamp: new Date()
socket.on('proposal:op', (data) => {
// Broadcast operation to everyone else in the distinct room channel
socket.to(data.proposalId).emit('proposal:op:broadcast', data);
Comment on lines +13 to +20

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

Validate event payloads before destructuring/access.

Line 13 and Line 20 can throw on undefined/malformed payloads; add shape/type checks to prevent avoidable handler exceptions from bad client packets.

Suggested guard pattern
-        socket.on('proposal:join', ({ proposalId }) => {
+        socket.on('proposal:join', (payload = {}, ack) => {
+            const { proposalId } = payload;
+            if (typeof proposalId !== 'string' || !proposalId.trim()) {
+                ack?.({ ok: false, error: 'invalid_proposal_id' });
+                return;
+            }
             socket.join(proposalId);
         });

-        socket.on('proposal:op', (data) => {
-            socket.to(data.proposalId).emit('proposal:op:broadcast', data);
+        socket.on('proposal:op', (data = {}, ack) => {
+            if (typeof data.proposalId !== 'string' || !data.proposalId.trim()) {
+                ack?.({ ok: false, error: 'invalid_proposal_id' });
+                return;
+            }
+            socket.to(data.proposalId).emit('proposal:op:broadcast', data);
         });
📝 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
socket.on('proposal:join', ({ proposalId }) => {
socket.join(proposalId);
console.log(`User ${socket.id} joined cooperative proposal room: ${proposalId}`);
});
}
});
socket.on('leave_channel', async (channelId) => {
if (channelId) {
socket.leave(`channel:${channelId}`);
await presenceService.leaveRoom(socket.user.uid, `channel:${channelId}`);
console.log(`${socket.user.name} left channel presence: ${channelId}`);
// Notify channel members
io.to(`channel:${channelId}`).emit('user_left_channel', {
uid: socket.user.uid,
name: socket.user.name,
channelId,
timestamp: new Date()
socket.on('proposal:op', (data) => {
// Broadcast operation to everyone else in the distinct room channel
socket.to(data.proposalId).emit('proposal:op:broadcast', data);
socket.on('proposal:join', (payload = {}, ack) => {
const { proposalId } = payload;
if (typeof proposalId !== 'string' || !proposalId.trim()) {
ack?.({ ok: false, error: 'invalid_proposal_id' });
return;
}
socket.join(proposalId);
console.log(`User ${socket.id} joined cooperative proposal room: ${proposalId}`);
});
socket.on('proposal:op', (data = {}, ack) => {
if (typeof data.proposalId !== 'string' || !data.proposalId.trim()) {
ack?.({ ok: false, error: 'invalid_proposal_id' });
return;
}
// Broadcast operation to everyone else in the distinct room channel
socket.to(data.proposalId).emit('proposal:op:broadcast', data);
});
🤖 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 `@backend/src/config/socket.js` around lines 13 - 20, The socket event handlers
for 'proposal:join' and 'proposal:op' are missing payload validation, which can
cause crashes when clients send malformed or undefined data. In the
'proposal:join' handler, add a guard check to validate that the incoming payload
exists and contains the proposalId property before destructuring it. Similarly,
in the 'proposal:op' handler, validate that the data object exists and has a
proposalId property before attempting to emit the broadcast. Use simple guard
conditions or type checks to return early if validation fails, preventing
handler exceptions from bad client packets.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Enforce auth and proposal-level authorization before room join/op.

Line 13 and Line 20 trust client-provided proposalId with no identity/ACL check, so any connected client can join arbitrary proposal rooms and inject operations.

Suggested fix (server-side gate before join/op)
+import Proposal from '../models/Proposal.model.js';
+import { verifySocketAuth } from '../middleware/socketAuth.js';
+
 export const initSocket = (server) => {
     io = new Server(server, socketOptions);
+    io.use(verifySocketAuth);

     io.on('connection', (socket) => {
-        socket.on('proposal:join', ({ proposalId }) => {
+        socket.on('proposal:join', async ({ proposalId } = {}, ack) => {
+            const proposal = await Proposal.findById(proposalId).select('studentId challengeId');
+            if (!proposal || !canAccessProposal(socket.user, proposal)) {
+                ack?.({ ok: false, error: 'forbidden' });
+                return;
+            }
             socket.join(proposalId);
+            ack?.({ ok: true });
         });

-        socket.on('proposal:op', (data) => {
+        socket.on('proposal:op', async (data = {}, ack) => {
+            if (!socket.rooms.has(data.proposalId)) {
+                ack?.({ ok: false, error: 'join required' });
+                return;
+            }
             socket.to(data.proposalId).emit('proposal:op:broadcast', data);
+            ack?.({ ok: true });
         });
🤖 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 `@backend/src/config/socket.js` around lines 13 - 20, The proposal:join and
proposal:op event handlers trust the client-provided proposalId without
verifying the user's authentication or authorization. Before allowing access in
the proposal:join handler, verify that the socket connection is authenticated
(check for a valid user session/token) and confirm the user has permission to
join that specific proposalId. Apply the same authentication and authorization
checks in the proposal:op handler before emitting the operation to other
clients. This ensures only authorized users can join proposal rooms and modify
proposals.

Comment on lines +18 to +20

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 | 🏗️ Heavy lift

Apply server-side version sequencing for OT operations.

Line 20 broadcasts operations without validating an expected/base version, so concurrent edits can be accepted out of order despite the new Proposal.version contract.

Suggested direction (version gate + monotonic broadcast payload)
-        socket.on('proposal:op', (data) => {
-            socket.to(data.proposalId).emit('proposal:op:broadcast', data);
-        });
+        socket.on('proposal:op', async ({ proposalId, op, baseVersion } = {}, ack) => {
+            const updated = await Proposal.findOneAndUpdate(
+                { _id: proposalId, version: baseVersion },
+                { $inc: { version: 1 } },
+                { new: true, projection: { version: 1 } }
+            );
+            if (!updated) {
+                ack?.({ ok: false, error: 'version_conflict' });
+                return;
+            }
+            socket.to(proposalId).emit('proposal:op:broadcast', {
+                proposalId,
+                op,
+                version: updated.version
+            });
+            ack?.({ ok: true, version: updated.version });
+        });
🤖 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 `@backend/src/config/socket.js` around lines 18 - 20, The proposal:op socket
listener must validate version sequencing before broadcasting operations. Before
emitting the proposal:op:broadcast event, add a check to verify that the
incoming data contains the expected/base version that matches the current
proposal version on the server. Only broadcast the operation if the version
validation passes, and include the version information in the broadcast payload
to maintain monotonic ordering of operations across all clients in the room.

});
}
});
// --- End Real-time Collaboration Logic ---

// Handle friends presence subscription
socket.on('subscribe_friends', async (userId) => {
if (userId) {
socket.join(`friends:${userId}`);
await presenceService.joinRoom(socket.user.uid, `friends:${userId}`);
console.log(`${socket.user.name} subscribed to friends presence: ${userId}`);
}
});

socket.on('unsubscribe_friends', async (userId) => {
if (userId) {
socket.leave(`friends:${userId}`);
await presenceService.leaveRoom(socket.user.uid, `friends:${userId}`);
console.log(`${socket.user.name} unsubscribed from friends presence: ${userId}`);
}
});

// Handle disconnect
socket.on('disconnect', async (reason) => {
console.log(`❌ User disconnected: ${socket.user.name} - ${reason}`);

// Get user's rooms before going offline
const rooms = await presenceService.getUserRooms(socket.user.uid);

await presenceService.setOffline(socket.user.uid);

// Broadcast to global room
io.to('global').emit('user_offline', {
uid: socket.user.uid,
name: socket.user.name,
timestamp: new Date()
});

// Notify channel rooms about user leaving
for (const room of rooms) {
if (room.startsWith('channel:')) {
io.to(room).emit('user_left_channel', {
uid: socket.user.uid,
name: socket.user.name,
channelId: room.replace('channel:', ''),
timestamp: new Date()
});
}
}
});

// Handle errors
socket.on('error', (error) => {
console.error(`Socket error for ${socket.user.name}:`, error);
socket.on('disconnect', () => {
console.log(`User disconnected: ${socket.id}`);
});
});
});

console.log('🔌 Socket.IO initialized');
return io;
return io;
};

export const getIO = () => {
if (!io) {
throw new Error('Socket.IO not initialized');
}
return io;
export const getIo = () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The getter export was renamed to getIo, while existing services/controllers import getIO; this will throw named export import errors and break all socket-based notifications and emits. Preserve the original export name or update every importer. [api mismatch]

Severity Level: Critical 🚨
- ❌ Job post scheduler fails, breaking scheduled community feed publishing.
- ❌ Outreach queue worker crashes, outbound outreach progress not emitted.
- ⚠️ Community presence subscribe/unsubscribe endpoints throw when importing socket.
Steps of Reproduction ✅
1. `backend/src/config/socket.js` currently defines the getter as `export const getIo = ()
=> { ... }` at line 32 (verified via BulkRead).

2. `backend/src/services/postScheduler.js` imports `getIO` (different casing) via `import
{ getIO } from '../config/socket.js';` at line 5, and
`backend/src/services/outreachQueue.js` does the same at line 8 (confirmed via BulkRead).

3. `backend/src/routes/community.js` dynamically imports the same name: `const { getIO } =
await import('../config/socket.js');` at lines 18 and 40 for the presence
subscribe/unsubscribe endpoints (confirmed via BulkRead).

4. On server startup, `backend/src/index.js` imports `initializePostScheduler` and
`startOutreachWorker` at lines 51 and 78, which in turn require `postScheduler.js` and
`outreachQueue.js`; ESM linking fails when those modules try to import the nonexistent
`getIO` export from `config/socket.js`, throwing "does not provide an export named
'getIO'" and blocking startup; even if startup somehow succeeded, any call to the
community presence endpoints would similarly fail at the dynamic import.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** backend/src/config/socket.js
**Line:** 32:32
**Comment:**
	*Api Mismatch: The getter export was renamed to `getIo`, while existing services/controllers import `getIO`; this will throw named export import errors and break all socket-based notifications and emits. Preserve the original export name or update every importer.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

if (!io) {
throw new Error('Socket.io not initialized!');
}
return io;
};
Comment on lines +6 to 37

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
# 1) Inspect current socket export surface
sed -n '1,140p' backend/src/config/socket.js

# 2) Find stale imports/usages of removed API names/default imports
rg -n -C2 "\b(initializeSocket|getIO)\b|import\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from\s+['\"][^'\"]*config/socket(\.js)?['\"]" backend || true

# 3) Confirm new API usage is present where expected
rg -n -C2 "\b(initSocket|getIo)\b" backend || true

Repository: anurag3407/career-pilot

Length of output: 8678


Stale imports of removed socket APIs will cause runtime failures.

Multiple files still import the old getIO name (uppercase) which no longer exists in the refactored module:

  • backend/src/controllers/communityFirebaseController.js (line 3 import, lines 137, 513, 700 usages)
  • backend/src/services/outreachQueue.js (line 8 import, line 33 usage)
  • backend/src/services/postScheduler.js (line 5 import, line 76 usage)
  • backend/src/services/jobAlertSocket.js (line 6 import, lines 15, 38, 62, 84, 105 usages)
  • backend/src/routes/community.js (lines 97, 119 dynamic imports, lines 98, 120 usages)

Additionally, backend/src/index.js (line 48) imports the old initializeSocket function (now exported as initSocket) and calls it at line 370, which will fail. Update all imports to use the new export names: initSocket instead of initializeSocket, and getIo instead of getIO.

🤖 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 `@backend/src/config/socket.js` around lines 6 - 37, Update all imports and
function calls throughout the codebase to match the refactored socket module
export names. Replace all occurrences of the old `getIO` function name with the
new `getIo` (note the lowercase 'o'), and replace all imports and calls to
`initializeSocket` with the new `initSocket`. This includes updating the import
statements in communityFirebaseController.js, outreachQueue.js,
postScheduler.js, jobAlertSocket.js, community.js, and index.js, as well as all
the places where these functions are invoked throughout those files.


export default { initializeSocket, getIO };
11 changes: 10 additions & 1 deletion backend/src/models/Proposal.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ const proposalSchema = new mongoose.Schema({
type: String,
default: null
},
collaborators: [{
type: String,
index: true
}],
version: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
Expand All @@ -64,8 +72,9 @@ proposalSchema.index({ challengeId: 1, createdAt: -1 }, { background: true });
proposalSchema.index({ challengeId: 1, status: 1 }, { background: true });
proposalSchema.index({ challengeId: 1, status: 1, createdAt: -1 }, { background: true });

proposalSchema.pre('save', function () {
proposalSchema.pre('save', function (next) {
this.updatedAt = new Date();
next();
});

const Proposal = mongoose.model('Proposal', proposalSchema);
Expand Down