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
3 changes: 2 additions & 1 deletion backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ app.use(express.json());
app.use(cookieParser()); // <-- Add this middleware HERE
app.use(
cors({
origin: process.env.FRONTEND_URL || "http://localhost:5173",
origin: process.env.FRONTEND_URL || "http://localhost:5174",
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
})
);
//initialize passport
Expand Down
170 changes: 120 additions & 50 deletions backend/src/controllers/roomController.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,144 @@
import type { Request, Response } from "express";
import Room from "../models/roomModel.js";
import type { IRoom } from "../models/roomModel.js";
import * as express from "express";
import mongoose from "mongoose";
import { io } from "../server.js";


declare global {
namespace Express {
interface Request {
userId?: string; // or number, depending on your ID type
}
namespace Express {
interface Request {
userId?: string;
}
}
}

// Create a new room
// ---------------------- Create Room ----------------------
export const createRoom = async (req: Request, res: Response) => {
try {
const { name } = req.body;
if (!name) return res.status(400).json({ message: "Room name is required" });

const existingRoom = await Room.findOne({ name });
if (existingRoom) return res.status(400).json({ message: "Room already exists" });

const room: IRoom = new Room({ name, members: [req.userId] });
await room.save();
res.status(201).json(room);
} catch (error) {
res.status(500).json({ message: "Server error", error });
}
try {
const { name } = req.body;
if (!name) return res.status(400).json({ message: "Room name is required" });
if (!req.userId) return res.status(401).json({ message: "Unauthorized" });

const existingRoom = await Room.findOne({ name });
if (existingRoom)
return res.status(400).json({ message: "Room with this name already exists" });

const room: IRoom = new Room({ name, members: [req.userId] });
await room.save();

io.emit("room-created", { roomId: room._id, name: room.name });
res.status(201).json(room);
} catch (error: any) {
console.error("Create room error:", error.message);
res.status(500).json({ message: "Server error", error: error.message });
}
};

// List all rooms
// ---------------------- List Rooms ----------------------
export const listRooms = async (_req: Request, res: Response) => {
try {
const rooms = await Room.find().populate("members", "username email");
res.json(rooms);
} catch (error) {
res.status(500).json({ message: "Server error", error });
}
try {
const rooms = await Room.find().populate("members", "username email");
res.json(rooms);
} catch (error: any) {
console.error("List rooms error:", error.message);
res.status(500).json({ message: "Server error", error: error.message });
}
};

// Join a room
// ---------------------- Join Room ----------------------
export const joinRoom = async (req: Request, res: Response) => {
try {
const { roomId } = req.params;
const room = await Room.findById(roomId);
if (!room) return res.status(404).json({ message: "Room not found" });
const userId = new mongoose.Types.ObjectId(req.userId!);
if (!room.members.includes(userId)) {
room.members.push(userId);
await room.save();
}

res.json(room);
} catch (error) {
res.status(500).json({ message: "Server error", error });
try {
const roomIdOrName = req.params.roomIdOrName;
const userId = req.userId;

if (!roomIdOrName) return res.status(400).json({ message: "Room name or ID is required" });
if (!userId) return res.status(401).json({ message: "Unauthorized - Missing userId" });

let room = await Room.findOne({ name: roomIdOrName });
if (!room && mongoose.Types.ObjectId.isValid(roomIdOrName)) {
room = await Room.findById(roomIdOrName);
}

if (!room) {
console.error("Join room error: Room not found for", roomIdOrName);
return res.status(404).json({ message: "Room not found" });
}

const userObjId = new mongoose.Types.ObjectId(userId);
if (!room.members.some(m => m.equals(userObjId))) {
room.members.push(userObjId);
await room.save();
}

const updatedRoom = await Room.findById(room._id).populate("members", "username email");
io.to(room._id.toString()).emit("user-joined", { userId, roomId: room._id });
io.to(room._id.toString()).emit("update-members", updatedRoom?.members || []);

res.json(updatedRoom);
} catch (error: any) {
console.error("Join room error:", error.message, error.stack);
res.status(500).json({ message: "Server error", error: error.message });
}
};

// Leave a room
// ---------------------- Leave Room ----------------------
export const leaveRoom = async (req: Request, res: Response) => {
try {
const { roomId } = req.params;
const room = await Room.findById(roomId);
if (!room) return res.status(404).json({ message: "Room not found" });
try {
const { roomId } = req.params;
const userId = req.userId;
if (!userId) return res.status(401).json({ message: "Unauthorized" });

const room = await Room.findById(roomId);
if (!room) return res.status(404).json({ message: "Room not found" });

const wasMember = room.members.some(m => m.toString() === userId);
if (!wasMember)
return res.status(400).json({ message: "You are not a member of this room" });

room.members = room.members.filter(member => member.toString() !== req.userId);
await room.save();
room.members = room.members.filter(m => m.toString() !== userId);
await room.save();

res.json({ message: "Left room successfully", room });
} catch (error) {
res.status(500).json({ message: "Server error", error });
io.to(roomId).emit("user-left", { userId, roomId });
const updatedRoom = await Room.findById(roomId).populate("members", "username email");
io.to(roomId).emit("update-members", updatedRoom?.members || []);

if (room.members.length === 0) {
await Room.findByIdAndDelete(roomId);
io.to(roomId).emit("room-ended", { roomId, reason: "empty" });
io.socketsLeave(roomId);
}

res.json({ message: "Left room successfully" });
} catch (error: any) {
console.error("Leave room error:", error.message);
res.status(500).json({ message: "Server error", error: error.message });
}
};

// ---------------------- End Room (Host Only) ----------------------
export const endRoom = async (req: Request, res: Response) => {
try {
const { roomId } = req.params;
const userId = req.userId;

if (!roomId) return res.status(400).json({ message: "Missing room ID" });
if (!userId) return res.status(401).json({ message: "Unauthorized" });

const room = await Room.findById(roomId);
if (!room) return res.status(404).json({ message: "Room not found" });

const hostId = room.members[0]?.toString();
if (hostId !== userId)
return res.status(403).json({ message: "Only the host can end the room" });

await Room.findByIdAndDelete(roomId);
io.to(roomId).emit("room-ended", { roomId, endedBy: userId });
io.socketsLeave(roomId);

res.json({ message: "Room ended successfully" });
} catch (error: any) {
console.error("End room error:", error.message);
res.status(500).json({ message: "Server error", error: error.message });
}
};
28 changes: 22 additions & 6 deletions backend/src/models/roomModel.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
import mongoose, { Schema, Document } from "mongoose";

export interface IRoom extends Document {
name: string;
members: mongoose.Types.ObjectId[];
createdAt: Date;
name: string;
members: mongoose.Types.ObjectId[];
host: mongoose.Types.ObjectId; // 👈 identifies who created/owns the room
isActive: boolean; // 👈 track whether the room is still active
createdAt: Date;
}

const roomSchema: Schema = new Schema({
name: { type: String, required: true, unique: true },
members: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }],
createdAt: { type: Date, default: Date.now },
name: { type: String, required: true, unique: true },
members: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }],
host: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, // host reference
isActive: { type: Boolean, default: true }, // true until ended or empty
createdAt: { type: Date, default: Date.now },
});

// ✅ Optional cleanup or logic
// When all members leave, mark as inactive automatically (not delete immediately)
roomSchema.methods.deactivateIfEmpty = async function () {
if (this.members.length === 0) {
this.isActive = false;
await this.save();
}
};

// Optional: auto-remove inactive rooms after certain time
// roomSchema.index({ createdAt: 1 }, { expireAfterSeconds: 7 * 24 * 60 * 60 }); // expires in 7 days

export default mongoose.model<IRoom>("Room", roomSchema);
17 changes: 12 additions & 5 deletions backend/src/routes/roomRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { Router } from "express";
import { createRoom, listRooms, joinRoom, leaveRoom } from "../controllers/roomController.js";
import {
createRoom,
listRooms,
joinRoom,
leaveRoom,
endRoom,
} from "../controllers/roomController.js";
import { protect } from "../middleware/authMiddleware.js";

const router = Router();

// All routes require authentication
router.use(protect);

router.post("/createRoom", createRoom); // Create room
router.get("/listRooms", listRooms); // List all rooms
router.post("/:roomId/join", joinRoom); // Join a room
router.post("/:roomId/leave", leaveRoom); // Leave a room
router.post("/createRoom", createRoom);
router.get("/listRooms", listRooms);
router.post("/:roomIdOrName/join", joinRoom);
router.post("/:roomId/leave", leaveRoom);
router.post("/:roomId/end", endRoom);

export default router;
Loading
Loading