Skip to content
Merged
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
39 changes: 39 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"license": "ISC",
"dependencies": {
"@clerk/express": "^1.7.41",
"@stream-io/node-sdk": "^0.7.12",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check latest version and security advisories for `@stream-io/node-sdk`

# Get latest version from npm
echo "Latest version on npm:"
curl -s https://registry.npmjs.org/@stream-io/node-sdk/latest | jq '.version'

# Check for security advisories
echo -e "\nSecurity advisories:"
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: NPM, package: "@stream-io/node-sdk") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}'

Repository: realACO/CodeMeet

Length of output: 274


🏁 Script executed:

# Check npm registry for vulnerability info
curl -s "https://registry.npmjs.org/@stream-io/node-sdk" | jq '.time | keys | .[-5:]'

# Check if there's a security advisory file or known issues
curl -s "https://api.github.com/repos/GetStream/stream-node-sdk/releases" | jq '.[] | select(.tag_name | contains("0.7")) | {tag_name, published_at}' | head -30

Repository: realACO/CodeMeet

Length of output: 208


🌐 Web query:

@stream-io/node-sdk security vulnerabilities 0.7.12 changelog

💡 Result:

@stream-io/node-sdk v0.7.12 changelog (stream-node repo)

Release 0.7.12 (2025-10-16) includes one bug fix:

  • “cutoff time on req should be string” (#189) (commit 55ccb83) [1]

Known / reported security vulnerabilities for v0.7.12

  • No known vulnerabilities were found/reported for @stream-io/node-sdk (including v0.7.12) by Aikido Intel at the time of writing. [2]

Sources: [1] https://github.com/GetStream/stream-node/releases/tag/v0.7.12, [2] https://intel.aikido.dev/packages/npm/%40stream-io/node-sdk


Consider upgrading @stream-io/node-sdk to the latest version.

The dependency is pinned to version ^0.7.12. While this version is stable with no known security vulnerabilities, the latest available version is 0.7.44—32 minor versions ahead. Consider upgrading to benefit from recent bug fixes and improvements unless there's a specific compatibility requirement for 0.7.12.

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

In `@backend/package.json` at line 16, The dependency `@stream-io/node-sdk` is
pinned at ^0.7.12; update package.json to the newer release (e.g., ^0.7.44) for
bug fixes and improvements, then run your package manager (npm/yarn/pnpm) to
refresh node_modules and update the lockfile; afterwards run the test suite and
any integration checks that exercise code paths using the Stream client (look
for usage sites of `@stream-io/node-sdk` in the codebase) to verify there are no
breaking changes and revert or pin if compatibility issues occur.

"codemeet": "file:..",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
Expand Down
155 changes: 155 additions & 0 deletions backend/src/controllers/sessionController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import Session from "../models/Session.js";
import { chatClient, streamClient } from "../lib/stream.js";

export async function createSession(req, res) {
try {
const { problem, difficulty } = req.body;
const userId = req.user._id;
const clerkId = req.user.clerkId;
if (!problem || !difficulty) {
return res
.status(400)
.json({ msg: "Problem and difficulty are required" });
}

//generate a unique call id for stream video
const callId = `session-${Date.now()}_${Math.random().toString(36).substring(7)}`;

//create session in db
const session = await Session.create({
problem,
difficulty,
host: userId,
callId,
});

//create stream video call
await streamClient.video.call("default", callId).getOrCreate({
data: {
created_by_id: clerkId,
custom: { problem, difficulty, sessionId: session._id.toString() },
},
});

//chat messaging
const channel = chatClient.channel("messaging", callId, {
name: `${problem} Session`,
created_by_id: clerkId,
members: [clerkId],
});

await channel.create();

Comment on lines +19 to +42
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Partial failure risk when creating session.

If streamClient.video.call(...).getOrCreate() fails after Session.create() succeeds, an orphaned DB record remains. Similarly, if channel.create() fails, both the DB record and Stream video call are orphaned with no cleanup.

Consider wrapping in a transaction or implementing cleanup on failure:

🛡️ Proposed cleanup on failure
     //create session in db
     const session = await Session.create({
       problem,
       difficulty,
       host: userId,
       callId,
     });

-    //create stream video call
-    await streamClient.video.call("default", callId).getOrCreate({
-      data: {
-        created_by_id: clerkId,
-        custom: { problem, difficulty, sessionId: session._id.toString() },
-      },
-    });
-
-    //chat messaging
-    const channel = chatClient.channel("messaging", callId, {
-      name: `${problem} Session`,
-      created_by_id: clerkId,
-      members: [clerkId],
-    });
-
-    await channel.create();
+    try {
+      //create stream video call
+      await streamClient.video.call("default", callId).getOrCreate({
+        data: {
+          created_by_id: clerkId,
+          custom: { problem, difficulty, sessionId: session._id.toString() },
+        },
+      });
+
+      //chat messaging
+      const channel = chatClient.channel("messaging", callId, {
+        name: `${problem} Session`,
+        created_by_id: clerkId,
+        members: [clerkId],
+      });
+
+      await channel.create();
+    } catch (externalError) {
+      // Cleanup: remove the orphaned session
+      await Session.findByIdAndDelete(session._id);
+      throw externalError;
+    }
📝 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 session = await Session.create({
problem,
difficulty,
host: userId,
callId,
});
//create stream video call
await streamClient.video.call("default", callId).getOrCreate({
data: {
created_by_id: clerkId,
custom: { problem, difficulty, sessionId: session._id.toString() },
},
});
//chat messaging
const channel = chatClient.channel("messaging", callId, {
name: `${problem} Session`,
created_by_id: clerkId,
members: [clerkId],
});
await channel.create();
const session = await Session.create({
problem,
difficulty,
host: userId,
callId,
});
try {
//create stream video call
await streamClient.video.call("default", callId).getOrCreate({
data: {
created_by_id: clerkId,
custom: { problem, difficulty, sessionId: session._id.toString() },
},
});
//chat messaging
const channel = chatClient.channel("messaging", callId, {
name: `${problem} Session`,
created_by_id: clerkId,
members: [clerkId],
});
await channel.create();
} catch (externalError) {
// Cleanup: remove the orphaned session
await Session.findByIdAndDelete(session._id);
throw externalError;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/sessionController.js` around lines 19 - 42, The code
creates a Session via Session.create and then calls
streamClient.video.call(...).getOrCreate and channel.create without rollback,
risking orphaned resources if later calls fail; fix by wrapping the
multi-resource creation in a DB transaction (use Mongoose transaction around
Session.create) or at minimum add a try/catch that performs compensating
deletes: if streamClient.video.call(...).getOrCreate succeeds but channel.create
fails, call the Stream API to delete the created call and delete the Session
document (Session.deleteOne or session.remove), and likewise if stream creation
fails after Session.create delete the Session; ensure cleanup paths reference
the created session._id, callId, and channel to reliably remove partial
resources.

res.status(201).json({ session });
} catch (error) {
console.error("Error createSession controller:", error.message);
res.status(500).json({ msg: "Internal server error" });
}
}

export async function getActiveSessions(_, res) {
try {
const sessions = await Session.find({ status: "active" })
.populate("host", "name profileImage email clerkId")
.sort({ createdAt: -1 })
.limit(20);
res.status(200).json({ sessions });
} catch (error) {
console.error("Error getActiveSessions controller:", error.message);
res.status(500).json({ msg: "Internal server error" });
}
}

export async function getMyRecentSessions(req, res) {
try {
const userId = req.user._id;
//where user is eigther host or pwrticipant
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const sessions = await Session.find({
status: "completed",
$or: [{ host: userId }, { participant: userId }],
})
.sort({ createdAt: -1 })
.limit(20);

res.status(200).json({ sessions });
} catch (error) {
console.error("Error getMyRecentSessions controller:", error.message);
res.status(500).json({ msg: "Internal server error" });
}
}

export async function getSessionById(req, res) {
try {
const { id } = req.params;
const session = await Session.findById(id)
.populate("host", "name email profileImage clerkId")
.populate("participant", "name email profileImage clerkId");

if (!session) return res.status(404).json({ msg: "Session not found" });

res.status(200).json({ session });
} catch (error) {
console.error("Error getSessionById controller:", error.message);
res.status(500).json({ msg: "Internal server error" });
}
}

export async function joinSession(req, res) {
try {
const { id } = req.params;
const userId = req.user._id;
const clerkId = req.user.clerkId;

const session = await Session.findById(id);
if (!session) return res.status(404).json({ msg: "Session not found" });
//check if session is already full
if (session.participant)
return res.status(400).json({ msg: "Session is already full" });
session.participant = userId;
Comment on lines +103 to +108
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing validation: user can join own session or join non-active sessions.

Two validation gaps:

  1. A host can join their own session as a participant
  2. Users can join sessions that are already completed
🛡️ Proposed validation
     const session = await Session.findById(id);
     if (!session) return res.status(404).json({ msg: "Session not found" });
+    
+    //check if session is active
+    if (session.status !== "active")
+      return res.status(400).json({ msg: "Session is not active" });
+    
+    //prevent host from joining as participant
+    if (session.host.toString() === userId.toString())
+      return res.status(400).json({ msg: "Host cannot join as participant" });
+    
     //check if session is already full
     if (session.participant)
       return res.status(400).json({ msg: "Session is already full" });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/sessionController.js` around lines 103 - 108, Add two
validations in the join flow after fetching the Session via Session.findById:
ensure the request userId is not the session host (e.g., compare userId to
session.host or session.hostId) and return a 400/403 with a clear message if
they try to join their own session; also check session.status (or similar field)
and reject joins when status === "completed" (or not "active") with a 400 and
message indicating the session is not joinable. Keep the existing check for
session.participant and only assign session.participant = userId after both new
validations pass.

await session.save();
Comment on lines +103 to +109
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Race condition when joining session.

The check-then-set on session.participant is not atomic. Two concurrent requests could both pass the if (session.participant) check before either saves, resulting in the second participant overwriting the first.

Consider using findOneAndUpdate with the condition in the query to make it atomic:

🔒 Proposed atomic update
-    const session = await Session.findById(id);
-    if (!session) return res.status(404).json({ msg: "Session not found" });
-    //check if session is already full
-    if (session.participant)
-      return res.status(400).json({ msg: "Session is already full" });
-    session.participant = userId;
-    await session.save();
+    const session = await Session.findOneAndUpdate(
+      { _id: id, participant: null },
+      { participant: userId },
+      { new: true }
+    );
+    if (!session) {
+      const exists = await Session.findById(id);
+      if (!exists) return res.status(404).json({ msg: "Session not found" });
+      return res.status(400).json({ msg: "Session is already full" });
+    }
📝 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 session = await Session.findById(id);
if (!session) return res.status(404).json({ msg: "Session not found" });
//check if session is already full
if (session.participant)
return res.status(400).json({ msg: "Session is already full" });
session.participant = userId;
await session.save();
const session = await Session.findOneAndUpdate(
{ _id: id, participant: null },
{ participant: userId },
{ new: true }
);
if (!session) {
const exists = await Session.findById(id);
if (!exists) return res.status(404).json({ msg: "Session not found" });
return res.status(400).json({ msg: "Session is already full" });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/sessionController.js` around lines 103 - 109, The
current join flow uses Session.findById and a separate check on
session.participant which is a race condition; replace the read-then-write with
an atomic update using Session.findOneAndUpdate (or findOneAndUpdate with { _id:
id, participant: { $exists: false } } or participant: null) to set participant
to userId only when no participant exists, check the returned document to
determine success, and respond with 404/400 appropriately; update the logic
around Session.findById/session.participant and session.save to use the atomic
findOneAndUpdate call instead.


const channel = chatClient.channel("messaging", session.callId);
await channel.addMembers([clerkId]);
res.status(200).json({ session });
} catch (error) {
console.error("Error joinSession controller:", error.message);
res.status(500).json({ msg: "Internal server error" });
}
}

export async function endSession(req, res) {
try {
const { id } = req.params;
const userId = req.user._id;

const session = await Session.findById(id);
if (!session) return res.status(404).json({ msg: "Session not found" });

//check if you are the host or not
if (session.host.toString() !== userId.toString()) {
return res.status(403).json({ msg: "Only The host can end the session" });
}

//check if session is already completed
if (session.status === "completed") {
return res.status(400).json({ msg: "Session is already completed" });
}

session.status = "completed";
await session.save();

//delete stream video call
const call = streamClient.video.call("default", session.callId);
await call.delete({ hard: true });

//delete stream chat channel
const channel = chatClient.channel("messaging", session.callId);
await channel.delete();
Comment on lines +138 to +147
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Partial failure risk when ending session.

If session.save() succeeds but call.delete() or channel.delete() fails, the session is marked completed but Stream resources remain orphaned. Consider:

  1. Deleting external resources first, then updating DB
  2. Or implementing a cleanup mechanism for orphaned resources
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/sessionController.js` around lines 138 - 147, The
current flow marks session.status = "completed" and calls await session.save()
before deleting Stream resources, which can leave orphaned resources if
streamClient.video.call(...).delete() or chatClient.channel(...).delete() fail;
change the flow to delete external resources first (use
streamClient.video.call("default", session.callId).delete({ hard: true }) and
chatClient.channel("messaging", session.callId).delete()) inside a try block,
and only on success set session.status = "completed" and await session.save();
if deletion fails, catch the error, log it, and avoid updating the session (or
schedule a retry/cleanup job), or alternatively perform session.save() and on
failure enqueue the callId for a cleanup worker—use the function/method names
call.delete, channel.delete, session.save, session.status to locate and
implement the fix.


res.status(200).json({ msg: "Session ended successfully" });
} catch (error) {
console.error("Error endSession controller:", error.message);
res.status(500).json({ msg: "Internal server error" });
}
}
//idkd
2 changes: 2 additions & 0 deletions backend/src/lib/inngest.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const syncUser = inngest.createFunction(
name: newUser.name,
image: newUser.profileImage,
});

//Send a Welcome email ro the user in future
Comment on lines +30 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo in TODO comment.

The comment has a typo: "ro" should be "to".

-    //Send a Welcome email ro the user in future
+    //Send a Welcome email to the user in future
📝 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
//Send a Welcome email ro the user in future
//Send a Welcome email to the user in future
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/lib/inngest.js` around lines 30 - 31, Fix the typo in the inline
TODO comment inside backend/src/lib/inngest.js: change "Send a Welcome email ro
the user in future" to "Send a welcome email to the user in the future" (or
similar correct wording) so the TODO reads clearly; update the comment near the
existing TODO line in the file (no code changes other than the comment).

},
);

Expand Down
4 changes: 3 additions & 1 deletion backend/src/lib/stream.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { StreamChat } from "stream-chat";
import { StreamClient } from "@stream-io/node-sdk";
import { ENV } from "./env.js";

const apiKey = ENV.STREAM_API_KEY;
Expand All @@ -8,7 +9,8 @@ if (!apiKey || !apiSecret) {
console.log("STREAM_API_KEY or STREAM_API_SECRET is missing");
}

export const chatClient = StreamChat.getInstance(apiKey, apiSecret);
export const chatClient = StreamChat.getInstance(apiKey, apiSecret); //this is for chat features
export const streamClient = new StreamClient(apiKey, apiSecret); //used for video calls

export const upsertStreamUser = async (userData) => {
try {
Expand Down
39 changes: 39 additions & 0 deletions backend/src/models/Session.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import mongoose from "mongoose";

const sessionSchema = new mongoose.Schema(
{
problem: {
type: String,
required: true,
},
difficulty: {
type: String,
enum: ["easy", "medium", "hard"],
required: true,
},
host: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
participant: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
default: null,
},
Comment on lines +19 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Field name mismatch: participant vs participants.

The schema defines participant (singular), but sessionController.js queries and populates participants (plural) at lines 69 and 86. This will cause:

  • getMyRecentSessions to never find sessions where the user was a participant
  • getSessionById populate on participants to return nothing

Either rename the schema field to participants (if supporting multiple participants) or fix the controller to use participant.

🐛 Option 1: Fix schema to match controller (if multiple participants intended)
-    participant: {
+    participants: {
       type: mongoose.Schema.Types.ObjectId,
       ref: "User",
       default: null,
     },
🐛 Option 2: Fix controller to match schema (if single participant intended)

In sessionController.js, update the queries:

// Line 69 in getMyRecentSessions
-      $or: [{ host: userId }, { participants: userId }],
+      $or: [{ host: userId }, { participant: userId }],

// Line 86 in getSessionById
-      .populate("participants", "name email profileImage clerkId");
+      .populate("participant", "name email profileImage clerkId");
📝 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
participant: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
default: null,
},
participants: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
default: null,
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/models/Session.js` around lines 19 - 23, The schema field name
and controller usage are mismatched: the Session schema defines participant
(singular) while sessionController functions getMyRecentSessions and
getSessionById query/populate participants (plural); fix by either (A) if
multiple participants are intended, rename/replace the schema field participant
with participants: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }]
(default to [] or null as appropriate) so populate and queries in
getMyRecentSessions and getSessionById work, or (B) if only a single participant
is intended, update sessionController (in getMyRecentSessions and
getSessionById) to query and populate participant (singular) instead of
participants; ensure the ref stays "User" and adjust any query operators (e.g.,
$in vs equality) accordingly.

status: {
type: String,
enum: ["active", "completed"],
default: "active",
},
callId: {
type: String,
default: "",
},
},
{ timestamps: true },
);

const Session = mongoose.model("Session", sessionSchema);

export default Session;
21 changes: 21 additions & 0 deletions backend/src/routes/sessionRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import express from "express";
import { protectRoute } from "../middleware/protectRoute.js";
import {
createSession,
getActiveSessions,
getMyRecentSessions,
getSessionById,
joinSession,
endSession,
} from "../controllers/sessionController.js";

const router = express.Router();

router.post("/", protectRoute, createSession);
router.get("/active", protectRoute, getActiveSessions);
router.get("/my-recent", protectRoute, getMyRecentSessions);
router.get("/:id", protectRoute, getSessionById);
router.post("/:id/join", protectRoute, joinSession);
router.post("/:id/end", protectRoute, endSession);

export default router;
2 changes: 2 additions & 0 deletions backend/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ENV } from "./lib/env.js";
import { connectDB } from "./lib/db.js";
import { inngest, functions } from "./lib/inngest.js";
import chatRoutes from "./routes/chatRoutes.js";
import sessionRoutes from "./routes/sessionRoute.js";

const app = express();

Expand All @@ -23,6 +24,7 @@ app.use("/api/inngest", serve({ client: inngest, functions }));

app.use(clerkMiddleware()); //this will add auth object to the request if the user is authenticated, which we can use in our routes
app.use("/api/chat", chatRoutes);
app.use("/api/sessions", sessionRoutes);

app.get("/health", (req, res) => {
res.status(200).json({ msg: "success api is running" });
Expand Down