diff --git a/matching-service/src/controller/api-controller.ts b/matching-service/src/controller/api-controller.ts new file mode 100644 index 0000000000..1cb93ab4e8 --- /dev/null +++ b/matching-service/src/controller/api-controller.ts @@ -0,0 +1,15 @@ +import { Request, Response } from 'express'; +import { + getCurrentMatchingUsersCount, +} from '../model/matching-model'; + +export async function getMatchingUsersCount(req: Request, res: Response) { + try { + const count = getCurrentMatchingUsersCount(); + return res.status(200).json({ count }); + } catch (err) { + console.error(err); + return res.status(500).json({ message: 'Unknown error when retrieving matching users count' }); + } +} + diff --git a/matching-service/src/controller/matching-controller.ts b/matching-service/src/controller/matching-controller.ts deleted file mode 100644 index eef41ac940..0000000000 --- a/matching-service/src/controller/matching-controller.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Request, Response } from 'express'; -import { - addUserToSearchPool, - getTimeSpentMatching, - getCurrentMatchingUsersCount, - removeUserFromSearchPool, - matchUsers -} from '../model/matching-model'; - -export async function registerForMatching(req: Request, res: Response) { - try { - const { userId, criteria } = req.body; - if (userId && criteria) { - addUserToSearchPool(userId, criteria); - return res.status(200).json({ message: 'User registered for matching successfully' }); - } else { - return res.status(400).json({ message: 'User ID or criteria are missing' }); - } - } catch (err) { - console.error(err); - return res.status(500).json({ message: 'Unknown error when registering user for matching' }); - } -} - -export async function getMatchingTime(req: Request, res: Response) { - try { - const { userId } = req.params; - if (!userId) { - return res.status(400).json({ message: 'User ID is missing' }); - } - - const timeSpent = getTimeSpentMatching(userId); - if (timeSpent !== null) { - return res.status(200).json({ timeSpent }); - } else { - return res.status(404).json({ message: `User ${userId} not found` }); - } - } catch (err) { - console.error(err); - return res.status(500).json({ message: 'Unknown error when retrieving matching time' }); - } -} - -export async function getMatchingUsersCount(req: Request, res: Response) { - try { - const count = getCurrentMatchingUsersCount(); - return res.status(200).json({ count }); - } catch (err) { - console.error(err); - return res.status(500).json({ message: 'Unknown error when retrieving matching users count' }); - } -} - -export async function cancelMatching(req: Request, res: Response) { - try { - const { userId } = req.params; - if (!userId) { - return res.status(400).json({ message: 'User ID is missing' }); - } - - const user = removeUserFromSearchPool(userId); - if (user) { - return res.status(200).json({ message: `Cancelled matching for user ${userId}` }); - } else { - return res.status(404).json({ message: `User ${userId} not found` }); - } - } catch (err) { - console.error(err); - return res.status(500).json({ message: 'Unknown error when cancelling matching' }); - } -} - -export async function findMatches(req: Request, res: Response) { - try { - const matches = matchUsers(); - if (matches) { - return res.status(200).json({ matches }); - } else { - return res.status(404).json({ message: 'No matches found' }); - } - } catch (err) { - console.error(err); - return res.status(500).json({ message: 'Unknown error when finding matches' }); - } -} diff --git a/matching-service/src/controller/socket-controller.ts b/matching-service/src/controller/socket-controller.ts new file mode 100644 index 0000000000..4917c369aa --- /dev/null +++ b/matching-service/src/controller/socket-controller.ts @@ -0,0 +1,65 @@ +import { Server, Socket } from "socket.io"; +import { addUserToSearchPool, addUserToSocketMap, getSocketIdForUser, matchUsers, removeUserFromSearchPool, removeUserFromSocketMap } from "../model/matching-model"; + +export function initaliseData(socket: Socket) { + const { userId } = socket.data; + addUserToSocketMap(userId, socket.id); + console.log(`User ${userId} connected via socket`); +} + + +// Handle user registration for matching +export function handleRegisterForMatching(socket: Socket, io: Server) { + const { userId } = socket.data; + socket.on('registerForMatching', (criteria) => { + if (criteria.difficulty && criteria.topic) { + addUserToSearchPool(userId, criteria); + console.log(`User ${userId} registered with criteria`, criteria); + socket.emit('registrationSuccess', { message: `User ${userId} registered for matching successfully.` }); + + // Check if a match can be made for the new user + const match = matchUsers(); + if (match) { + const { matchedUsers } = match; + const [user1, user2] = matchedUsers; + + // Notify both clients of the match using the mapping + const socketId1 = getSocketIdForUser(user1.userId); + const socketId2 = getSocketIdForUser(user2.userId); + + if (socketId1) { + io.sockets.sockets.get(socketId1)?.emit('matchFound', { matchedWith: user2.userId }); //INSERT SESSION ID HERE + } + if (socketId2) { + io.sockets.sockets.get(socketId2)?.emit('matchFound', { matchedWith: user1.userId }); //INSERT SESSION ID HERE + } + + // Disconnect both users + if (socketId1) { + io.sockets.sockets.get(socketId1)?.disconnect(true); + } + if (socketId2) { + io.sockets.sockets.get(socketId2)?.disconnect(true); + } + + // Remove users from the map + removeUserFromSocketMap(user1.userId); + removeUserFromSocketMap(user2.userId); + } + } else { + socket.emit('error', 'Invalid matching criteria.'); + } + }); +} + +export function handleDisconnect(socket: Socket) { + // Handle disconnection + const { userId } = socket.data; + socket.on('disconnect', () => { + console.log(`User ${userId} disconnected`); + removeUserFromSearchPool(userId); + + // Remove the user from the map + removeUserFromSocketMap(userId); + }); +} \ No newline at end of file diff --git a/matching-service/src/middleware/jwt-validation.ts b/matching-service/src/middleware/jwt-validation.ts index eb2cb655c5..8ee704a824 100644 --- a/matching-service/src/middleware/jwt-validation.ts +++ b/matching-service/src/middleware/jwt-validation.ts @@ -1,10 +1,11 @@ import { Request, Response, NextFunction } from 'express'; import jwt, { JwtPayload } from 'jsonwebtoken'; +import { Socket } from 'socket.io'; const secretKey = 'your_secret_key'; // Use an environment variable for this in production // Function to validate JWT in HTTP requests -export function validateJWT(req: Request, res: Response, next: NextFunction) { +export function validateApiJWT(req: Request, res: Response, next: NextFunction) { const token = req.headers.authorization?.split(' ')[1]; // Extract Bearer token if (!token) { return res.status(401).json({ message: 'Access Denied. No token provided.' }); @@ -19,12 +20,23 @@ export function validateJWT(req: Request, res: Response, next: NextFunction) { } } -// Function to validate JWT for socket connections -export function validateSocketJWT(token: string): JwtPayload { +export function validateSocketJWT(socket: Socket, next: (err?: Error) => void) { + const token = socket.handshake.auth.token; + + if (!token) { + return next(new Error('Authentication error: No token provided.')); + } + try { const decoded = jwt.verify(token, secretKey) as JwtPayload; - return decoded; // Return the decoded payload (userId, etc.) + socket.data.userId = decoded.id; + console.log(`User ${decoded.id} validated via JWT`); + next(); } catch (err) { - throw new Error('Invalid token'); + next(new Error('Authentication error: Invalid token.')); } } + +export function validateJWT (token: string): JwtPayload { + return jwt.verify(token, secretKey) as JwtPayload; +} \ No newline at end of file diff --git a/matching-service/src/model/matching-model.ts b/matching-service/src/model/matching-model.ts index c4b9d6ba42..21c44178b6 100644 --- a/matching-service/src/model/matching-model.ts +++ b/matching-service/src/model/matching-model.ts @@ -9,8 +9,23 @@ interface UserSearch { startTime: Date; } +// Maintain a mapping of userId to socket.id +const userSocketMap = new Map(); + const searchPool: UserSearch[] = []; +export function addUserToSocketMap(userId: string, socketId: string) { + userSocketMap.set(userId, socketId); +} + +export function removeUserFromSocketMap(userId: string) { + userSocketMap.delete(userId); +} + +export function getSocketIdForUser(userId: string): string | undefined { + return userSocketMap.get(userId); +} + // Add user to the search pool export function addUserToSearchPool(userId: string, criteria: SearchCriteria) { const startTime = new Date(); diff --git a/matching-service/src/routes/matching-routes.ts b/matching-service/src/routes/api-routes.ts similarity index 51% rename from matching-service/src/routes/matching-routes.ts rename to matching-service/src/routes/api-routes.ts index 710737d7fe..f8328bdd95 100644 --- a/matching-service/src/routes/matching-routes.ts +++ b/matching-service/src/routes/api-routes.ts @@ -1,13 +1,9 @@ import { Router } from 'express'; -import { registerForMatching, getMatchingTime, getMatchingUsersCount } from '../controller/matching-controller'; -import { validateJWT } from '../middleware/jwt-validation'; +import { getMatchingUsersCount } from '../controller/api-controller'; const router = Router(); router.get('/', (req, res) => {res.send('Hello from matching service!')}); // Test route - - -router.post('/match/register', validateJWT, registerForMatching); // Register for matching router.get('/match/count', getMatchingUsersCount); // Retrieve the count of users matching export const matchingRoutes = router; diff --git a/matching-service/src/routes/socket-routes.ts b/matching-service/src/routes/socket-routes.ts new file mode 100644 index 0000000000..09876d8bbb --- /dev/null +++ b/matching-service/src/routes/socket-routes.ts @@ -0,0 +1,8 @@ +import { Server, Socket } from "socket.io"; +import { handleDisconnect, handleRegisterForMatching } from "../controller/socket-controller"; + +export function registerEventHandlers(socket: Socket, io: Server) { + handleRegisterForMatching(socket, io); + handleDisconnect(socket); +} + diff --git a/matching-service/src/server.ts b/matching-service/src/server.ts index 91ef0ae9fd..52dfd654a6 100644 --- a/matching-service/src/server.ts +++ b/matching-service/src/server.ts @@ -7,6 +7,9 @@ import { removeUserFromSearchPool, matchUsers } from './model/matching-model'; +import { inherits } from 'util'; +import { registerEventHandlers } from './routes/socket-routes'; +import { initaliseData } from './controller/socket-controller'; // Create the express app const app = express(); @@ -22,85 +25,12 @@ const io = new Server(server, { // Middleware for parsing JSON app.use(express.json()); -// Maintain a mapping of userId to socket.id -const userSocketMap = new Map(); - // Socket.io connection handler with JWT validation -io.use((socket, next) => { - const token = socket.handshake.auth.token; - - if (!token) { - return next(new Error('Authentication error: No token provided.')); - } - - try { - // Validate the token and extract the userId - const decoded = validateSocketJWT(token); - socket.data.userId = decoded.userId; - next(); - } catch (err) { - next(new Error('Authentication error: Invalid token.')); - } -}); - +io.use(validateSocketJWT); io.on('connection', (socket) => { - const { userId } = socket.data; - - // Map the userId to the socket.id - userSocketMap.set(userId, socket.id); - - console.log(`User ${userId} connected via socket`); - - // Handle user registration for matching - socket.on('registerForMatching', (criteria) => { - if (criteria.difficulty && criteria.topic) { - addUserToSearchPool(userId, criteria); - console.log(`User ${userId} registered with criteria`, criteria); - socket.emit('registrationSuccess', { message: `User ${userId} registered for matching successfully.` }); - - // Check if a match can be made for the new user - const match = matchUsers(); - if (match) { - const { matchedUsers } = match; - const [user1, user2] = matchedUsers; - - // Notify both clients of the match using the mapping - const socketId1 = userSocketMap.get(user1.userId); - const socketId2 = userSocketMap.get(user2.userId); - - if (socketId1) { - io.sockets.sockets.get(socketId1)?.emit('matchFound', { matchedWith: user2.userId }); - } - if (socketId2) { - io.sockets.sockets.get(socketId2)?.emit('matchFound', { matchedWith: user1.userId }); - } - - // Disconnect both users - if (socketId1) { - io.sockets.sockets.get(socketId1)?.disconnect(true); - } - if (socketId2) { - io.sockets.sockets.get(socketId2)?.disconnect(true); - } - - // Remove users from the map - userSocketMap.delete(user1.userId); - userSocketMap.delete(user2.userId); - } - } else { - socket.emit('error', 'Invalid matching criteria.'); - } - }); - - // Handle disconnection - socket.on('disconnect', () => { - console.log(`User ${userId} disconnected`); - removeUserFromSearchPool(userId); - - // Remove the user from the map - userSocketMap.delete(userId); - }); -}); + initaliseData(socket); + registerEventHandlers(socket, io); +}) export { server }; @@ -110,9 +40,4 @@ if (require.main === module) { server.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); }); -} -// Start the server -// const PORT = 8002; -// server.listen(PORT, () => { -// console.log(`Server is running on port ${PORT}`); -// }); +} \ No newline at end of file diff --git a/matching-service/test/jwt-validation.test.ts b/matching-service/test/jwt-validation.test.ts index 8f0c77bb1a..3eefa18c1b 100644 --- a/matching-service/test/jwt-validation.test.ts +++ b/matching-service/test/jwt-validation.test.ts @@ -1,4 +1,4 @@ -import { validateSocketJWT } from '../src/middleware/jwt-validation'; +import { validateJWT } from '../src/middleware/jwt-validation'; import jwt from 'jsonwebtoken'; describe('Socket Middleware', () => { @@ -7,11 +7,11 @@ describe('Socket Middleware', () => { const userId = 'testUser'; const token = jwt.sign({ id: userId }, secretKey); - const decoded = validateSocketJWT(token); + const decoded = validateJWT(token); expect(decoded.id).toBe(userId); }); it('should throw an error for invalid token', () => { - expect(() => validateSocketJWT('invalid_token')).toThrow(); + expect(() => validateJWT('invalid_token')).toThrow(); }); }); diff --git a/matching-service/test/socket-connection-matching.test.ts b/matching-service/test/socket-connection-matching.test.ts index 936e1a1fe5..c157d8e9e1 100644 --- a/matching-service/test/socket-connection-matching.test.ts +++ b/matching-service/test/socket-connection-matching.test.ts @@ -18,7 +18,7 @@ describe('Socket.IO Server Tests', () => { test('should register a user and get success response', (done) => { const clientSocket = io(`http://localhost:${PORT}`, { - auth: { token: jwt.sign({ userId: 'client-id' }, 'your_secret_key') }, + auth: { token: jwt.sign({ id: 'client-id' }, 'your_secret_key') }, }); clientSocket.emit('registerForMatching', { difficulty: 'easy', topic: 'math' }); clientSocket.on('registrationSuccess', (message) => { @@ -31,11 +31,11 @@ describe('Socket.IO Server Tests', () => { test('should handle matching users and send matchFound event', (done) => { const clientSocket = io(`http://localhost:${PORT}`, { - auth: { token: jwt.sign({ userId: 'client-id' }, 'your_secret_key') }, + auth: { token: jwt.sign({ id: 'client-id' }, 'your_secret_key') }, }); const anotherSocket = io(`http://localhost:${PORT}`, { - auth: { token: jwt.sign({ userId: 'client-id2' }, 'your_secret_key') }, + auth: { token: jwt.sign({ id: 'client-id2' }, 'your_secret_key') }, });