Skip to content

Commit 0fd9080

Browse files
authored
Merge pull request #16 from CS3219-AY2526Sem1/ncduy0303/add-colab-service-auth
feat(collab-service): add authorization & integrate question service
2 parents 1816a28 + e24ba53 commit 0fd9080

File tree

19 files changed

+667
-230
lines changed

19 files changed

+667
-230
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Should match the JWT_SECRET used in user-service
2+
JWT_SECRET=your-jwt-secret-here

backend/collaboration-service/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@y/websocket-server": "^0.1.1",
1818
"cors": "^2.8.5",
1919
"express": "^5.1.0",
20+
"jsonwebtoken": "^9.0.2",
2021
"kafkajs": "^2.2.4",
2122
"mongoose": "^8.19.1",
2223
"uuid": "^13.0.0",

backend/collaboration-service/src/config.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,24 @@ export const config = {
33
WS_PORT: process.env.WS_PORT || 8005,
44
HTTP_PORT: process.env.HTTP_PORT || 8004,
55
// Database
6-
MONGO_URI: process.env.MONGODB_URI || "mongodb://admin:password@localhost:27017/peerprepCollabService?authSource=admin",
6+
MONGO_URI:
7+
process.env.MONGODB_URI ||
8+
"mongodb://admin:password@localhost:27017/peerprepCollabService?authSource=admin",
79
// Room Management
810
ROOM_TIMEOUT_MINUTES: parseInt(process.env.ROOM_TIMEOUT_MINUTES) || 10,
911
// Yjs MongoDB Provider Persistence Settings
1012
PERSISTENCE_CONFIG: {
1113
multipleCollections: true, // each document gets an own collection in the database
1214
},
15+
// JWT Token (Should match the user-service JWT secret)
16+
JWT_SECRET: process.env.JWT_SECRET || "your-jwt-secret-here",
17+
// WebSocket Close Codes
18+
WS_CLOSE_CODES: {
19+
AUTH_FAILED: 4000, // Invalid/missing token
20+
UNAUTHORIZED: 4001, // User not in room
21+
ROOM_NOT_FOUND: 4002, // Room doesn't exist
22+
ROOM_INACTIVE: 4003, // Room is closed
23+
},
1324
};
1425

1526
export default config;

backend/collaboration-service/src/http/httpServer.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ class HttpServer {
1616
* Set up Express middleware
1717
*/
1818
setupMiddleware() {
19-
this.app.use(cors());
19+
this.app.use(
20+
cors({
21+
origin: process.env.WEB_BASE_URL,
22+
credentials: true,
23+
}),
24+
);
2025
this.app.use(express.json());
2126
}
2227

backend/collaboration-service/src/http/roomRoutes.js

Lines changed: 114 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,179 @@
11
import express from "express";
22
import roomController from "../controllers/roomController.js";
3+
import { authenticateHttp } from "../middleware/auth.js";
34

45
const router = express.Router();
56

67
/**
78
* POST /api/v1/rooms
89
* Create a new room
10+
* @deprecated This route is disabled since room creation is now handled by matching service through Kafka
911
*/
10-
router.post("/", async (req, res) => {
11-
try {
12-
const { questionId, userIds, programmingLanguage } = req.body;
13-
14-
if (!userIds || !Array.isArray(userIds) || userIds.length === 0) {
15-
return res.status(400).json({
16-
success: false,
17-
error: "userIds is required and must be a non-empty array",
18-
});
19-
}
20-
21-
const room = await roomController.create(null, questionId, userIds, programmingLanguage);
22-
res.json({
23-
success: true,
24-
room,
25-
});
26-
} catch (error) {
27-
console.error("Failed to create room:", error);
28-
res.status(500).json({
29-
success: false,
30-
error: error.message,
31-
});
32-
}
33-
});
12+
// router.post("/", async (req, res) => {
13+
// try {
14+
// const { questionId, userIds, programmingLanguage } = req.body;
15+
16+
// if (!userIds || !Array.isArray(userIds) || userIds.length === 0) {
17+
// return res.status(400).json({
18+
// success: false,
19+
// error: "userIds is required and must be a non-empty array",
20+
// });
21+
// }
22+
23+
// const room = await roomController.create(null, questionId, userIds, programmingLanguage);
24+
// res.json({
25+
// success: true,
26+
// room,
27+
// });
28+
// } catch (error) {
29+
// console.error("Failed to create room:", error);
30+
// res.status(500).json({
31+
// success: false,
32+
// error: error.message,
33+
// });
34+
// }
35+
// });
3436

3537
/**
3638
* GET /api/v1/rooms/:roomId
3739
* Get room information and document content
40+
* Requires authentication and authorization (user must be in room)
3841
*/
39-
router.get("/:roomId", async (req, res) => {
42+
router.get("/:roomId", authenticateHttp, async (req, res) => {
4043
try {
4144
const { roomId } = req.params;
45+
const userId = req.userId; // Set by authenticateHttp middleware
46+
4247
const room = await roomController.get(roomId);
43-
if (room) {
44-
const documentContent = await roomController.getDocumentContent(roomId);
45-
res.json({
46-
success: true,
47-
room: room,
48-
document: {
49-
content: documentContent,
50-
},
51-
});
52-
} else {
53-
res.status(404).json({
48+
49+
// Check if room exists
50+
if (!room) {
51+
return res.status(404).json({
5452
success: false,
5553
error: "Room not found",
5654
});
5755
}
58-
} catch (error) {
59-
console.error("Failed to get room:", error);
60-
res.status(500).json({
61-
success: false,
62-
error: error.message,
63-
});
64-
}
65-
});
6656

67-
/**
68-
* PATCH /api/v1/rooms/:roomId/close
69-
* Close (stop) a room for collaboration
70-
*/
71-
router.patch("/:roomId/close", async (req, res) => {
72-
try {
73-
const { roomId } = req.params;
74-
const room = await roomController.get(roomId);
75-
if (!room) {
57+
// Check if user is authorized (part of the room)
58+
if (!room.userIds.includes(userId)) {
59+
// Return 404 to maintain privacy (don't reveal room exists)
7660
return res.status(404).json({
7761
success: false,
7862
error: "Room not found",
7963
});
80-
} else if (!room.isActive) {
81-
return res.status(400).json({
82-
success: false,
83-
error: "Room is already closed",
84-
});
8564
}
86-
await roomController.closeRoom(roomId);
65+
66+
const documentContent = await roomController.getDocumentContent(roomId);
8767
res.json({
8868
success: true,
89-
message: "Room closed successfully. All clients have been notified.",
69+
room: room,
70+
document: {
71+
content: documentContent,
72+
},
9073
});
9174
} catch (error) {
92-
console.error("Failed to close room:", error);
75+
console.error("Failed to get room:", error);
9376
res.status(500).json({
9477
success: false,
9578
error: error.message,
9679
});
9780
}
9881
});
9982

83+
/**
84+
* PATCH /api/v1/rooms/:roomId/close
85+
* Close (stop) a room for collaboration
86+
* Requires authentication and authorization (user must be in room)
87+
* @deprecated This route is disabled since room closing is automated by collaboration service
88+
* when no users are left in the room for a certain period
89+
*/
90+
// router.patch("/:roomId/close", authenticateHttp, async (req, res) => {
91+
// try {
92+
// const { roomId } = req.params;
93+
// const userId = req.userId; // Set by authenticateHttp middleware
94+
95+
// const room = await roomController.get(roomId);
96+
97+
// // Check if room exists
98+
// if (!room) {
99+
// return res.status(404).json({
100+
// success: false,
101+
// error: "Room not found",
102+
// });
103+
// }
104+
105+
// // Check if user is authorized (part of the room)
106+
// if (!room.userIds.includes(userId)) {
107+
// return res.status(404).json({
108+
// success: false,
109+
// error: "Room not found",
110+
// });
111+
// }
112+
113+
// if (!room.isActive) {
114+
// return res.status(400).json({
115+
// success: false,
116+
// error: "Room is already closed",
117+
// });
118+
// }
119+
120+
// await roomController.closeRoom(roomId);
121+
// res.json({
122+
// success: true,
123+
// message: "Room closed successfully. All clients have been notified.",
124+
// });
125+
// } catch (error) {
126+
// console.error("Failed to close room:", error);
127+
// res.status(500).json({
128+
// success: false,
129+
// error: error.message,
130+
// });
131+
// }
132+
// });
133+
100134
/**
101135
* PATCH /api/v1/rooms/:roomId/language
102136
* Set programming language for a room
137+
* Requires authentication and authorization (user must be in room)
103138
*/
104-
router.patch("/:roomId/language", async (req, res) => {
139+
router.patch("/:roomId/language", authenticateHttp, async (req, res) => {
105140
try {
106141
const { roomId } = req.params;
107142
const { language } = req.body;
143+
const userId = req.userId; // Set by authenticateHttp middleware
144+
108145
if (!language) {
109146
return res.status(400).json({
110147
success: false,
111148
error: "Programming language is required",
112149
});
113150
}
151+
114152
const room = await roomController.get(roomId);
153+
154+
// Check if room exists
115155
if (!room) {
116156
return res.status(404).json({
117157
success: false,
118158
error: "Room not found",
119159
});
120-
} else if (!room.isActive) {
160+
}
161+
162+
// Check if user is authorized (part of the room)
163+
if (!room.userIds.includes(userId)) {
164+
return res.status(404).json({
165+
success: false,
166+
error: "Room not found",
167+
});
168+
}
169+
170+
if (!room.isActive) {
121171
return res.status(400).json({
122172
success: false,
123173
error: "Cannot set language for a closed room",
124174
});
125175
}
176+
126177
await roomController.setProgrammingLanguage(roomId, language);
127178
res.json({
128179
success: true,
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import jwt from "jsonwebtoken";
2+
import { URL } from "url";
3+
4+
/**
5+
* Middleware to authenticate WebSocket connections using JWT
6+
* Extracts userId from JWT and returns it as part of the result
7+
*/
8+
export const authenticateWebSocket = async (req) => {
9+
try {
10+
// Try to extract token from query parameter
11+
const url = new URL(req.url, `http://${req.headers.host}`);
12+
const token = url.searchParams.get("token");
13+
14+
if (!token) {
15+
return {
16+
success: false,
17+
error: "Missing authentication token",
18+
};
19+
}
20+
21+
// Verify the JWT
22+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
23+
24+
return {
25+
success: true,
26+
userId: decoded.username,
27+
};
28+
} catch (err) {
29+
return {
30+
success: false,
31+
error: err.message,
32+
};
33+
}
34+
};
35+
36+
/**
37+
* Middleware to authenticate HTTP requests using JWT
38+
* Extracts userId from JWT and attaches it to req.userId
39+
*/
40+
export const authenticateHttp = async (req, res, next) => {
41+
try {
42+
const authHeader = req.get("authorization");
43+
44+
if (!authHeader) {
45+
return res.status(401).json({
46+
success: false,
47+
error: "Missing Authorization header",
48+
});
49+
}
50+
51+
// Expected format: "Bearer <token>"
52+
const [scheme, token] = authHeader.split(" ");
53+
54+
if (scheme !== "Bearer" || !token) {
55+
return res.status(401).json({
56+
success: false,
57+
error: "Invalid Authorization header format",
58+
});
59+
}
60+
61+
// Verify the JWT
62+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
63+
64+
// Attach user ID to request
65+
req.userId = decoded.username;
66+
67+
next();
68+
} catch (err) {
69+
return res.status(401).json({
70+
success: false,
71+
error: err.message,
72+
});
73+
}
74+
};
75+
76+
export default { authenticateWebSocket, authenticateHttp };

0 commit comments

Comments
 (0)