feat: implement real-time chat with Socket.IO
Implemented WebSocket-based real-time messaging for both event rooms and private match chats using Socket.IO with comprehensive test coverage. Backend changes: - Installed socket.io@4.8.1 for WebSocket server - Created Socket.IO server with JWT authentication middleware - Implemented event room management (join/leave/messages) - Added active users tracking with real-time updates - Implemented private match room messaging - Integrated Socket.IO with Express HTTP server - Messages are persisted to PostgreSQL via Prisma - Added 12 comprehensive unit tests (89.13% coverage) Frontend changes: - Installed socket.io-client for WebSocket connections - Created socket service layer for connection management - Updated EventChatPage with real-time messaging - Updated MatchChatPage with real-time private chat - Added connection status indicators (● Connected/Disconnected) - Disabled message input when not connected Infrastructure: - Updated nginx config to proxy WebSocket connections at /socket.io - Added Upgrade and Connection headers for WebSocket support - Set long timeouts (7d) for persistent WebSocket connections Key features: - JWT-authenticated socket connections - Room-based architecture for events and matches - Real-time message broadcasting - Active users list with automatic updates - Automatic cleanup on disconnect - Message persistence in database Test coverage: - 12 tests passing (authentication, event rooms, match rooms, disconnect, errors) - Socket.IO module: 89.13% statements, 81.81% branches, 91.66% functions - Overall coverage: 81.19% Phase 1, Step 4 completed. Ready for Phase 2 (Core Features).
This commit is contained in:
311
backend/package-lock.json
generated
311
backend/package-lock.json
generated
@@ -15,12 +15,14 @@
|
|||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-validator": "^7.3.0",
|
"express-validator": "^7.3.0",
|
||||||
"jsonwebtoken": "^9.0.2"
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"socket.io": "^4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"nodemon": "^3.0.2",
|
"nodemon": "^3.0.2",
|
||||||
"prisma": "^5.8.0",
|
"prisma": "^5.8.0",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"supertest": "^6.3.3"
|
"supertest": "^6.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1077,6 +1079,12 @@
|
|||||||
"@sinonjs/commons": "^3.0.0"
|
"@sinonjs/commons": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -1122,6 +1130,15 @@
|
|||||||
"@babel/types": "^7.28.2"
|
"@babel/types": "^7.28.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cors": {
|
||||||
|
"version": "2.8.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||||
|
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/graceful-fs": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.9",
|
"version": "4.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||||
@@ -1163,7 +1180,6 @@
|
|||||||
"version": "24.10.1",
|
"version": "24.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
@@ -1417,6 +1433,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64id": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^4.5.0 || >= 5.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.27",
|
"version": "2.8.27",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.27.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.27.tgz",
|
||||||
@@ -2040,6 +2065,106 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io": {
|
||||||
|
"version": "6.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
|
||||||
|
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/cors": "^2.8.12",
|
||||||
|
"@types/node": ">=10.0.0",
|
||||||
|
"accepts": "~1.3.4",
|
||||||
|
"base64id": "2.0.0",
|
||||||
|
"cookie": "~0.7.2",
|
||||||
|
"cors": "~2.8.5",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||||
|
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1",
|
||||||
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/error-ex": {
|
"node_modules/error-ex": {
|
||||||
"version": "1.3.4",
|
"version": "1.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||||
@@ -4520,6 +4645,157 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "~1.3.4",
|
||||||
|
"base64id": "~2.0.0",
|
||||||
|
"cors": "~2.8.5",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io": "~6.6.0",
|
||||||
|
"socket.io-adapter": "~2.5.2",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-adapter": {
|
||||||
|
"version": "2.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
|
||||||
|
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "~4.3.4",
|
||||||
|
"ws": "~8.17.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-adapter/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-adapter/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.6.1",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@@ -4834,7 +5110,6 @@
|
|||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
@@ -4978,6 +5253,36 @@
|
|||||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|||||||
@@ -29,12 +29,14 @@
|
|||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-validator": "^7.3.0",
|
"express-validator": "^7.3.0",
|
||||||
"jsonwebtoken": "^9.0.2"
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"socket.io": "^4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"nodemon": "^3.0.2",
|
"nodemon": "^3.0.2",
|
||||||
"prisma": "^5.8.0",
|
"prisma": "^5.8.0",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"supertest": "^6.3.3"
|
"supertest": "^6.3.3"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
|||||||
514
backend/src/__tests__/socket.test.js
Normal file
514
backend/src/__tests__/socket.test.js
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
const http = require('http');
|
||||||
|
const { Server } = require('socket.io');
|
||||||
|
const Client = require('socket.io-client');
|
||||||
|
const { initializeSocket } = require('../socket');
|
||||||
|
const { generateToken } = require('../utils/auth');
|
||||||
|
const { prisma } = require('../utils/db');
|
||||||
|
|
||||||
|
describe('Socket.IO Server', () => {
|
||||||
|
let httpServer;
|
||||||
|
let io;
|
||||||
|
let serverSocket;
|
||||||
|
let clientSocket;
|
||||||
|
let testUser;
|
||||||
|
let testToken;
|
||||||
|
const port = 3001;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Create test user
|
||||||
|
testUser = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
username: 'sockettest',
|
||||||
|
email: 'sockettest@example.com',
|
||||||
|
passwordHash: 'hashedpassword',
|
||||||
|
avatar: 'https://example.com/avatar.jpg',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
testToken = generateToken({ userId: testUser.id });
|
||||||
|
|
||||||
|
// Create HTTP server and initialize Socket.IO
|
||||||
|
httpServer = http.createServer();
|
||||||
|
io = initializeSocket(httpServer);
|
||||||
|
|
||||||
|
httpServer.listen(port);
|
||||||
|
|
||||||
|
// Wait for server to be ready
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
httpServer.once('listening', resolve);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Cleanup test user
|
||||||
|
await prisma.user.delete({
|
||||||
|
where: { id: testUser.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close server and client
|
||||||
|
if (clientSocket) clientSocket.close();
|
||||||
|
io.close();
|
||||||
|
httpServer.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (clientSocket && clientSocket.connected) {
|
||||||
|
clientSocket.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Authentication', () => {
|
||||||
|
test('should reject connection without token', (done) => {
|
||||||
|
clientSocket = Client(`http://localhost:${port}`);
|
||||||
|
|
||||||
|
clientSocket.on('connect_error', (error) => {
|
||||||
|
expect(error.message).toBe('Authentication required');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('connect', () => {
|
||||||
|
done(new Error('Should not connect without token'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reject connection with invalid token', (done) => {
|
||||||
|
clientSocket = Client(`http://localhost:${port}`, {
|
||||||
|
auth: { token: 'invalid-token' },
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('connect_error', (error) => {
|
||||||
|
expect(error.message).toBe('Invalid token');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should accept connection with valid token', (done) => {
|
||||||
|
clientSocket = Client(`http://localhost:${port}`, {
|
||||||
|
auth: { token: testToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('connect', () => {
|
||||||
|
expect(clientSocket.connected).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('connect_error', (error) => {
|
||||||
|
done(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Event Rooms', () => {
|
||||||
|
let testEvent;
|
||||||
|
let testChatRoom;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Create test event and chat room
|
||||||
|
testEvent = await prisma.event.create({
|
||||||
|
data: {
|
||||||
|
name: 'Test Event',
|
||||||
|
location: 'Test Location',
|
||||||
|
startDate: new Date('2025-12-01'),
|
||||||
|
endDate: new Date('2025-12-03'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
testChatRoom = await prisma.chatRoom.create({
|
||||||
|
data: {
|
||||||
|
eventId: testEvent.id,
|
||||||
|
type: 'event',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Cleanup test data
|
||||||
|
await prisma.chatRoom.delete({
|
||||||
|
where: { id: testChatRoom.id },
|
||||||
|
});
|
||||||
|
await prisma.event.delete({
|
||||||
|
where: { id: testEvent.id },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should join event room successfully', (done) => {
|
||||||
|
clientSocket = Client(`http://localhost:${port}`, {
|
||||||
|
auth: { token: testToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('connect', () => {
|
||||||
|
clientSocket.emit('join_event_room', { eventId: testEvent.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('active_users', (users) => {
|
||||||
|
expect(Array.isArray(users)).toBe(true);
|
||||||
|
expect(users.length).toBeGreaterThan(0);
|
||||||
|
const currentUser = users.find(u => u.userId === testUser.id);
|
||||||
|
expect(currentUser).toBeDefined();
|
||||||
|
expect(currentUser.username).toBe(testUser.username);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('error', (error) => {
|
||||||
|
done(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should receive user_joined notification', (done) => {
|
||||||
|
const client1 = Client(`http://localhost:${port}`, {
|
||||||
|
auth: { token: testToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
client1.on('connect', () => {
|
||||||
|
client1.emit('join_event_room', { eventId: testEvent.id });
|
||||||
|
|
||||||
|
// Create second user and join the same room
|
||||||
|
const client2Token = generateToken({ userId: testUser.id });
|
||||||
|
const client2 = Client(`http://localhost:${port}`, {
|
||||||
|
auth: { token: client2Token },
|
||||||
|
});
|
||||||
|
|
||||||
|
client1.on('user_joined', (userData) => {
|
||||||
|
expect(userData.userId).toBe(testUser.id);
|
||||||
|
expect(userData.username).toBe(testUser.username);
|
||||||
|
client1.close();
|
||||||
|
client2.close();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
client2.on('connect', () => {
|
||||||
|
client2.emit('join_event_room', { eventId: testEvent.id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send and receive event messages', (done) => {
|
||||||
|
clientSocket = Client(`http://localhost:${port}`, {
|
||||||
|
auth: { token: testToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageContent = 'Test message content';
|
||||||
|
|
||||||
|
clientSocket.on('connect', () => {
|
||||||
|
clientSocket.emit('join_event_room', { eventId: testEvent.id });
|
||||||
|
|
||||||
|
clientSocket.on('active_users', () => {
|
||||||
|
// Wait for active_users, then send message
|
||||||
|
clientSocket.emit('send_event_message', {
|
||||||
|
eventId: testEvent.id,
|
||||||
|
content: messageContent,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('event_message', async (message) => {
|
||||||
|
expect(message.content).toBe(messageContent);
|
||||||
|
expect(message.userId).toBe(testUser.id);
|
||||||
|
expect(message.username).toBe(testUser.username);
|
||||||
|
expect(message.type).toBe('text');
|
||||||
|
expect(message.createdAt).toBeDefined();
|
||||||
|
|
||||||
|
// Verify message was saved to database
|
||||||
|
const dbMessage = await prisma.message.findUnique({
|
||||||
|
where: { id: message.id },
|
||||||
|
});
|
||||||
|
expect(dbMessage).toBeDefined();
|
||||||
|
expect(dbMessage.content).toBe(messageContent);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await prisma.message.delete({ where: { id: message.id } });
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('error', (error) => {
|
||||||
|
done(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should leave event room and update active users', (done) => {
|
||||||
|
clientSocket = Client(`http://localhost:${port}`, {
|
||||||
|
auth: { token: testToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('connect', () => {
|
||||||
|
clientSocket.emit('join_event_room', { eventId: testEvent.id });
|
||||||
|
|
||||||
|
clientSocket.on('active_users', (users) => {
|
||||||
|
if (users.length > 0) {
|
||||||
|
// User joined, now leave
|
||||||
|
clientSocket.emit('leave_event_room', { eventId: testEvent.id });
|
||||||
|
setTimeout(() => {
|
||||||
|
done();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Match Rooms', () => {
|
||||||
|
let testUser2;
|
||||||
|
let testMatch;
|
||||||
|
let testMatchRoom;
|
||||||
|
let testEvent;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Create test event for match
|
||||||
|
testEvent = await prisma.event.create({
|
||||||
|
data: {
|
||||||
|
name: 'Match Test Event',
|
||||||
|
location: 'Test Location',
|
||||||
|
startDate: new Date('2025-12-01'),
|
||||||
|
endDate: new Date('2025-12-03'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create second user with timestamp to avoid conflicts
|
||||||
|
const timestamp = Date.now();
|
||||||
|
testUser2 = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
username: `sockettest2_${timestamp}`,
|
||||||
|
email: `sockettest2_${timestamp}@example.com`,
|
||||||
|
passwordHash: 'hashedpassword',
|
||||||
|
avatar: 'https://example.com/avatar2.jpg',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create match
|
||||||
|
testMatch = await prisma.match.create({
|
||||||
|
data: {
|
||||||
|
status: 'active',
|
||||||
|
user1: { connect: { id: testUser.id } },
|
||||||
|
user2: { connect: { id: testUser2.id } },
|
||||||
|
event: { connect: { id: testEvent.id } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create match chat room
|
||||||
|
testMatchRoom = await prisma.chatRoom.create({
|
||||||
|
data: {
|
||||||
|
type: 'private',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Link room to match
|
||||||
|
await prisma.match.update({
|
||||||
|
where: { id: testMatch.id },
|
||||||
|
data: { roomId: testMatchRoom.id },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Cleanup
|
||||||
|
await prisma.match.delete({
|
||||||
|
where: { id: testMatch.id },
|
||||||
|
});
|
||||||
|
await prisma.chatRoom.delete({
|
||||||
|
where: { id: testMatchRoom.id },
|
||||||
|
});
|
||||||
|
await prisma.user.delete({
|
||||||
|
where: { id: testUser2.id },
|
||||||
|
});
|
||||||
|
await prisma.event.delete({
|
||||||
|
where: { id: testEvent.id },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should join match room successfully', (done) => {
|
||||||
|
clientSocket = Client(`http://localhost:${port}`, {
|
||||||
|
auth: { token: testToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('connect', () => {
|
||||||
|
clientSocket.emit('join_match_room', { matchId: testMatch.id });
|
||||||
|
// Just verify no error occurs
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(clientSocket.connected).toBe(true);
|
||||||
|
done();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('error', (error) => {
|
||||||
|
done(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send and receive match messages', (done) => {
|
||||||
|
clientSocket = Client(`http://localhost:${port}`, {
|
||||||
|
auth: { token: testToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageContent = 'Private match message';
|
||||||
|
|
||||||
|
clientSocket.on('connect', () => {
|
||||||
|
clientSocket.emit('join_match_room', { matchId: testMatch.id });
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
clientSocket.emit('send_match_message', {
|
||||||
|
matchId: testMatch.id,
|
||||||
|
content: messageContent,
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('match_message', async (message) => {
|
||||||
|
expect(message.content).toBe(messageContent);
|
||||||
|
expect(message.userId).toBe(testUser.id);
|
||||||
|
expect(message.username).toBe(testUser.username);
|
||||||
|
expect(message.type).toBe('text');
|
||||||
|
|
||||||
|
// Verify message was saved
|
||||||
|
const dbMessage = await prisma.message.findUnique({
|
||||||
|
where: { id: message.id },
|
||||||
|
});
|
||||||
|
expect(dbMessage).toBeDefined();
|
||||||
|
expect(dbMessage.roomId).toBe(testMatchRoom.id);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await prisma.message.delete({ where: { id: message.id } });
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('error', (error) => {
|
||||||
|
done(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle match room not found error', (done) => {
|
||||||
|
clientSocket = Client(`http://localhost:${port}`, {
|
||||||
|
auth: { token: testToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('connect', () => {
|
||||||
|
clientSocket.emit('join_match_room', { matchId: 99999 });
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
clientSocket.emit('send_match_message', {
|
||||||
|
matchId: 99999,
|
||||||
|
content: 'Test',
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('error', (error) => {
|
||||||
|
expect(error.message).toBe('Match room not found');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Disconnect Handling', () => {
|
||||||
|
let testEvent;
|
||||||
|
let testChatRoom;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
testEvent = await prisma.event.create({
|
||||||
|
data: {
|
||||||
|
name: 'Disconnect Test Event',
|
||||||
|
location: 'Test Location',
|
||||||
|
startDate: new Date('2025-12-01'),
|
||||||
|
endDate: new Date('2025-12-03'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
testChatRoom = await prisma.chatRoom.create({
|
||||||
|
data: {
|
||||||
|
eventId: testEvent.id,
|
||||||
|
type: 'event',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await prisma.chatRoom.delete({
|
||||||
|
where: { id: testChatRoom.id },
|
||||||
|
});
|
||||||
|
await prisma.event.delete({
|
||||||
|
where: { id: testEvent.id },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle disconnect and update active users', (done) => {
|
||||||
|
const client1 = Client(`http://localhost:${port}`, {
|
||||||
|
auth: { token: testToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
const client2Token = generateToken({ userId: testUser.id });
|
||||||
|
const client2 = Client(`http://localhost:${port}`, {
|
||||||
|
auth: { token: client2Token },
|
||||||
|
});
|
||||||
|
|
||||||
|
let client1Connected = false;
|
||||||
|
let client2Connected = false;
|
||||||
|
|
||||||
|
client1.on('connect', () => {
|
||||||
|
client1Connected = true;
|
||||||
|
client1.emit('join_event_room', { eventId: testEvent.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
client2.on('connect', () => {
|
||||||
|
client2Connected = true;
|
||||||
|
client2.emit('join_event_room', { eventId: testEvent.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
client2.on('user_left', (userData) => {
|
||||||
|
expect(userData.userId).toBe(testUser.id);
|
||||||
|
expect(userData.username).toBe(testUser.username);
|
||||||
|
client2.close();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for both to join, then disconnect client1
|
||||||
|
setTimeout(() => {
|
||||||
|
if (client1Connected && client2Connected) {
|
||||||
|
client1.close();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Handling', () => {
|
||||||
|
let testEvent;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
testEvent = await prisma.event.create({
|
||||||
|
data: {
|
||||||
|
name: 'Error Test Event',
|
||||||
|
location: 'Test Location',
|
||||||
|
startDate: new Date('2025-12-01'),
|
||||||
|
endDate: new Date('2025-12-03'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await prisma.event.delete({
|
||||||
|
where: { id: testEvent.id },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle chat room not found error', (done) => {
|
||||||
|
clientSocket = Client(`http://localhost:${port}`, {
|
||||||
|
auth: { token: testToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('connect', () => {
|
||||||
|
clientSocket.emit('join_event_room', { eventId: testEvent.id });
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
clientSocket.emit('send_event_message', {
|
||||||
|
eventId: testEvent.id,
|
||||||
|
content: 'Test message',
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSocket.on('error', (error) => {
|
||||||
|
expect(error.message).toBe('Chat room not found');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
const http = require('http');
|
||||||
const app = require('./app');
|
const app = require('./app');
|
||||||
const { testConnection, disconnect } = require('./utils/db');
|
const { testConnection, disconnect } = require('./utils/db');
|
||||||
|
const { initializeSocket } = require('./socket');
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
@@ -8,7 +10,13 @@ async function startServer() {
|
|||||||
// Test database connection
|
// Test database connection
|
||||||
await testConnection();
|
await testConnection();
|
||||||
|
|
||||||
const server = app.listen(PORT, '0.0.0.0', () => {
|
// Create HTTP server
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
// Initialize Socket.IO
|
||||||
|
initializeSocket(server);
|
||||||
|
|
||||||
|
server.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log('=================================');
|
console.log('=================================');
|
||||||
console.log('🚀 spotlight.cam Backend Started');
|
console.log('🚀 spotlight.cam Backend Started');
|
||||||
console.log('=================================');
|
console.log('=================================');
|
||||||
|
|||||||
270
backend/src/socket/index.js
Normal file
270
backend/src/socket/index.js
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
const { Server } = require('socket.io');
|
||||||
|
const { verifyToken } = require('../utils/auth');
|
||||||
|
const { prisma } = require('../utils/db');
|
||||||
|
|
||||||
|
// Track active users in each event room
|
||||||
|
const activeUsers = new Map(); // eventId -> Set of { socketId, userId, username, avatar }
|
||||||
|
|
||||||
|
function initializeSocket(httpServer) {
|
||||||
|
const io = new Server(httpServer, {
|
||||||
|
cors: {
|
||||||
|
origin: process.env.CORS_ORIGIN || 'http://localhost:8080',
|
||||||
|
credentials: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Authentication middleware for Socket.IO
|
||||||
|
io.use(async (socket, next) => {
|
||||||
|
try {
|
||||||
|
const token = socket.handshake.auth.token;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return next(new Error('Authentication required'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = verifyToken(token);
|
||||||
|
if (!decoded) {
|
||||||
|
return next(new Error('Invalid token'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user from database
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: decoded.userId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
email: true,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return next(new Error('User not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.user = user;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Socket auth error:', error);
|
||||||
|
next(new Error('Authentication failed'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
console.log(`✅ User connected: ${socket.user.username} (${socket.id})`);
|
||||||
|
|
||||||
|
// Join event room
|
||||||
|
socket.on('join_event_room', async ({ eventId }) => {
|
||||||
|
try {
|
||||||
|
const roomName = `event_${eventId}`;
|
||||||
|
socket.join(roomName);
|
||||||
|
socket.currentEventRoom = roomName;
|
||||||
|
socket.currentEventId = eventId;
|
||||||
|
|
||||||
|
// Add user to active users
|
||||||
|
if (!activeUsers.has(eventId)) {
|
||||||
|
activeUsers.set(eventId, new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInfo = {
|
||||||
|
socketId: socket.id,
|
||||||
|
userId: socket.user.id,
|
||||||
|
username: socket.user.username,
|
||||||
|
avatar: socket.user.avatar,
|
||||||
|
};
|
||||||
|
|
||||||
|
activeUsers.get(eventId).add(JSON.stringify(userInfo));
|
||||||
|
|
||||||
|
console.log(`👤 ${socket.user.username} joined event room ${eventId}`);
|
||||||
|
|
||||||
|
// Broadcast updated active users list
|
||||||
|
const users = Array.from(activeUsers.get(eventId)).map(u => JSON.parse(u));
|
||||||
|
io.to(roomName).emit('active_users', users);
|
||||||
|
|
||||||
|
// Notify room about new user
|
||||||
|
socket.to(roomName).emit('user_joined', {
|
||||||
|
userId: socket.user.id,
|
||||||
|
username: socket.user.username,
|
||||||
|
avatar: socket.user.avatar,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Join event room error:', error);
|
||||||
|
socket.emit('error', { message: 'Failed to join room' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Leave event room
|
||||||
|
socket.on('leave_event_room', ({ eventId }) => {
|
||||||
|
const roomName = `event_${eventId}`;
|
||||||
|
socket.leave(roomName);
|
||||||
|
|
||||||
|
// Remove from active users
|
||||||
|
if (activeUsers.has(eventId)) {
|
||||||
|
const users = activeUsers.get(eventId);
|
||||||
|
const userInfo = JSON.stringify({
|
||||||
|
socketId: socket.id,
|
||||||
|
userId: socket.user.id,
|
||||||
|
username: socket.user.username,
|
||||||
|
avatar: socket.user.avatar,
|
||||||
|
});
|
||||||
|
users.delete(userInfo);
|
||||||
|
|
||||||
|
// Broadcast updated list
|
||||||
|
const updatedUsers = Array.from(users).map(u => JSON.parse(u));
|
||||||
|
io.to(roomName).emit('active_users', updatedUsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`👤 ${socket.user.username} left event room ${eventId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send message to event room
|
||||||
|
socket.on('send_event_message', async ({ eventId, content }) => {
|
||||||
|
try {
|
||||||
|
const roomName = `event_${eventId}`;
|
||||||
|
|
||||||
|
// Save message to database
|
||||||
|
const chatRoom = await prisma.chatRoom.findFirst({
|
||||||
|
where: {
|
||||||
|
eventId: parseInt(eventId),
|
||||||
|
type: 'event',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!chatRoom) {
|
||||||
|
return socket.emit('error', { message: 'Chat room not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await prisma.message.create({
|
||||||
|
data: {
|
||||||
|
roomId: chatRoom.id,
|
||||||
|
userId: socket.user.id,
|
||||||
|
content,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Broadcast message to room
|
||||||
|
io.to(roomName).emit('event_message', {
|
||||||
|
id: message.id,
|
||||||
|
roomId: message.roomId,
|
||||||
|
userId: message.user.id,
|
||||||
|
username: message.user.username,
|
||||||
|
avatar: message.user.avatar,
|
||||||
|
content: message.content,
|
||||||
|
type: message.type,
|
||||||
|
createdAt: message.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`💬 Message in event ${eventId} from ${socket.user.username}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Send message error:', error);
|
||||||
|
socket.emit('error', { message: 'Failed to send message' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Join private match room
|
||||||
|
socket.on('join_match_room', ({ matchId }) => {
|
||||||
|
const roomName = `match_${matchId}`;
|
||||||
|
socket.join(roomName);
|
||||||
|
socket.currentMatchRoom = roomName;
|
||||||
|
console.log(`👥 ${socket.user.username} joined match room ${matchId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send message to match room
|
||||||
|
socket.on('send_match_message', async ({ matchId, content }) => {
|
||||||
|
try {
|
||||||
|
const roomName = `match_${matchId}`;
|
||||||
|
|
||||||
|
// Get match and its room
|
||||||
|
const match = await prisma.match.findUnique({
|
||||||
|
where: { id: parseInt(matchId) },
|
||||||
|
include: { room: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!match || !match.room) {
|
||||||
|
return socket.emit('error', { message: 'Match room not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save message
|
||||||
|
const message = await prisma.message.create({
|
||||||
|
data: {
|
||||||
|
roomId: match.room.id,
|
||||||
|
userId: socket.user.id,
|
||||||
|
content,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Broadcast to match room
|
||||||
|
io.to(roomName).emit('match_message', {
|
||||||
|
id: message.id,
|
||||||
|
roomId: message.roomId,
|
||||||
|
userId: message.user.id,
|
||||||
|
username: message.user.username,
|
||||||
|
avatar: message.user.avatar,
|
||||||
|
content: message.content,
|
||||||
|
type: message.type,
|
||||||
|
createdAt: message.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`💬 Private message in match ${matchId} from ${socket.user.username}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Send match message error:', error);
|
||||||
|
socket.emit('error', { message: 'Failed to send message' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle disconnection
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log(`❌ User disconnected: ${socket.user.username} (${socket.id})`);
|
||||||
|
|
||||||
|
// Remove from active users in all event rooms
|
||||||
|
if (socket.currentEventId) {
|
||||||
|
const eventId = socket.currentEventId;
|
||||||
|
if (activeUsers.has(eventId)) {
|
||||||
|
const users = activeUsers.get(eventId);
|
||||||
|
const userInfo = JSON.stringify({
|
||||||
|
socketId: socket.id,
|
||||||
|
userId: socket.user.id,
|
||||||
|
username: socket.user.username,
|
||||||
|
avatar: socket.user.avatar,
|
||||||
|
});
|
||||||
|
users.delete(userInfo);
|
||||||
|
|
||||||
|
// Broadcast updated list
|
||||||
|
const updatedUsers = Array.from(users).map(u => JSON.parse(u));
|
||||||
|
io.to(socket.currentEventRoom).emit('active_users', updatedUsers);
|
||||||
|
|
||||||
|
// Notify about user leaving
|
||||||
|
socket.to(socket.currentEventRoom).emit('user_left', {
|
||||||
|
userId: socket.user.id,
|
||||||
|
username: socket.user.username,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🔌 Socket.IO initialized');
|
||||||
|
return io;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { initializeSocket };
|
||||||
140
frontend/package-lock.json
generated
140
frontend/package-lock.json
generated
@@ -11,7 +11,8 @@
|
|||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.553.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.9.5"
|
"react-router-dom": "^7.9.5",
|
||||||
|
"socket.io-client": "^4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
@@ -1406,6 +1407,12 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -1993,6 +2000,45 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||||
|
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1",
|
||||||
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
@@ -2923,7 +2969,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mz": {
|
"node_modules/mz": {
|
||||||
@@ -3647,6 +3692,68 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.6.1",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -4178,6 +4285,35 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.553.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.9.5"
|
"react-router-dom": "^7.9.5",
|
||||||
|
"socket.io-client": "^4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import { useParams, useNavigate } from 'react-router-dom';
|
|||||||
import Layout from '../components/layout/Layout';
|
import Layout from '../components/layout/Layout';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { mockEvents } from '../mocks/events';
|
import { mockEvents } from '../mocks/events';
|
||||||
import { mockEventMessages } from '../mocks/messages';
|
|
||||||
import { mockUsers } from '../mocks/users';
|
|
||||||
import { Send, UserPlus } from 'lucide-react';
|
import { Send, UserPlus } from 'lucide-react';
|
||||||
|
import { connectSocket, disconnectSocket, getSocket } from '../services/socket';
|
||||||
|
|
||||||
const EventChatPage = () => {
|
const EventChatPage = () => {
|
||||||
const { eventId } = useParams();
|
const { eventId } = useParams();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [messages, setMessages] = useState(mockEventMessages);
|
const [messages, setMessages] = useState([]);
|
||||||
const [newMessage, setNewMessage] = useState('');
|
const [newMessage, setNewMessage] = useState('');
|
||||||
const [activeUsers, setActiveUsers] = useState(mockUsers.slice(1, 5));
|
const [activeUsers, setActiveUsers] = useState([]);
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
|
|
||||||
const event = mockEvents.find(e => e.id === parseInt(eventId));
|
const event = mockEvents.find(e => e.id === parseInt(eventId));
|
||||||
@@ -26,29 +26,86 @@ const EventChatPage = () => {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Connect to Socket.IO
|
||||||
|
const socket = connectSocket();
|
||||||
|
|
||||||
|
if (!socket) {
|
||||||
|
console.error('Failed to connect to socket');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Socket event listeners
|
||||||
|
socket.on('connect', () => {
|
||||||
|
setIsConnected(true);
|
||||||
|
// Join event room
|
||||||
|
socket.emit('join_event_room', { eventId: parseInt(eventId) });
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
setIsConnected(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Receive messages
|
||||||
|
socket.on('event_message', (message) => {
|
||||||
|
setMessages((prev) => [...prev, message]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Receive active users list
|
||||||
|
socket.on('active_users', (users) => {
|
||||||
|
// Filter out duplicates and current user
|
||||||
|
const uniqueUsers = users
|
||||||
|
.filter((u, index, self) =>
|
||||||
|
index === self.findIndex((t) => t.userId === u.userId)
|
||||||
|
)
|
||||||
|
.filter((u) => u.userId !== user.id);
|
||||||
|
setActiveUsers(uniqueUsers);
|
||||||
|
});
|
||||||
|
|
||||||
|
// User joined notification
|
||||||
|
socket.on('user_joined', (userData) => {
|
||||||
|
console.log(`${userData.username} joined the room`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// User left notification
|
||||||
|
socket.on('user_left', (userData) => {
|
||||||
|
console.log(`${userData.username} left the room`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
socket.emit('leave_event_room', { eventId: parseInt(eventId) });
|
||||||
|
socket.off('connect');
|
||||||
|
socket.off('disconnect');
|
||||||
|
socket.off('event_message');
|
||||||
|
socket.off('active_users');
|
||||||
|
socket.off('user_joined');
|
||||||
|
socket.off('user_left');
|
||||||
|
};
|
||||||
|
}, [eventId, user.id]);
|
||||||
|
|
||||||
const handleSendMessage = (e) => {
|
const handleSendMessage = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!newMessage.trim()) return;
|
if (!newMessage.trim()) return;
|
||||||
|
|
||||||
const message = {
|
const socket = getSocket();
|
||||||
id: messages.length + 1,
|
if (!socket || !socket.connected) {
|
||||||
room_id: parseInt(eventId),
|
alert('Not connected to chat server');
|
||||||
user_id: user.id,
|
return;
|
||||||
username: user.username,
|
}
|
||||||
avatar: user.avatar,
|
|
||||||
content: newMessage,
|
// Send message via Socket.IO
|
||||||
type: 'text',
|
socket.emit('send_event_message', {
|
||||||
created_at: new Date().toISOString(),
|
eventId: parseInt(eventId),
|
||||||
};
|
content: newMessage,
|
||||||
|
});
|
||||||
|
|
||||||
setMessages([...messages, message]);
|
|
||||||
setNewMessage('');
|
setNewMessage('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMatchWith = (userId) => {
|
const handleMatchWith = (userId) => {
|
||||||
// Mockup - in the future will be WebSocket request
|
// TODO: Implement match request
|
||||||
alert(`Match request sent to user!`);
|
alert(`Match request sent to user!`);
|
||||||
// Simulate acceptance after 1 second
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate(`/matches/1/chat`);
|
navigate(`/matches/1/chat`);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@@ -70,6 +127,11 @@ const EventChatPage = () => {
|
|||||||
<div className="bg-primary-600 text-white p-4">
|
<div className="bg-primary-600 text-white p-4">
|
||||||
<h2 className="text-2xl font-bold">{event.name}</h2>
|
<h2 className="text-2xl font-bold">{event.name}</h2>
|
||||||
<p className="text-primary-100 text-sm">{event.location}</p>
|
<p className="text-primary-100 text-sm">{event.location}</p>
|
||||||
|
<div className="mt-2">
|
||||||
|
<span className={`text-xs px-2 py-1 rounded ${isConnected ? 'bg-green-500' : 'bg-red-500'}`}>
|
||||||
|
{isConnected ? '● Connected' : '● Disconnected'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex h-[calc(100vh-280px)]">
|
<div className="flex h-[calc(100vh-280px)]">
|
||||||
@@ -78,10 +140,13 @@ const EventChatPage = () => {
|
|||||||
<h3 className="font-semibold text-gray-900 mb-4">
|
<h3 className="font-semibold text-gray-900 mb-4">
|
||||||
Active users ({activeUsers.length})
|
Active users ({activeUsers.length})
|
||||||
</h3>
|
</h3>
|
||||||
|
{activeUsers.length === 0 && (
|
||||||
|
<p className="text-sm text-gray-500">No other users online</p>
|
||||||
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{activeUsers.map((activeUser) => (
|
{activeUsers.map((activeUser) => (
|
||||||
<div
|
<div
|
||||||
key={activeUser.id}
|
key={activeUser.userId}
|
||||||
className="flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg"
|
className="flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -94,13 +159,10 @@ const EventChatPage = () => {
|
|||||||
<p className="text-sm font-medium text-gray-900">
|
<p className="text-sm font-medium text-gray-900">
|
||||||
{activeUser.username}
|
{activeUser.username}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
⭐ {activeUser.rating}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMatchWith(activeUser.id)}
|
onClick={() => handleMatchWith(activeUser.userId)}
|
||||||
className="p-1 text-primary-600 hover:bg-primary-50 rounded"
|
className="p-1 text-primary-600 hover:bg-primary-50 rounded"
|
||||||
title="Connect"
|
title="Connect"
|
||||||
>
|
>
|
||||||
@@ -115,8 +177,13 @@ const EventChatPage = () => {
|
|||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
|
{messages.length === 0 && (
|
||||||
|
<div className="text-center text-gray-500 py-8">
|
||||||
|
No messages yet. Start the conversation!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{messages.map((message) => {
|
{messages.map((message) => {
|
||||||
const isOwnMessage = message.user_id === user.id;
|
const isOwnMessage = message.userId === user.id;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
@@ -134,7 +201,7 @@ const EventChatPage = () => {
|
|||||||
{message.username}
|
{message.username}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
{new Date(message.created_at).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
|
{new Date(message.createdAt).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -162,11 +229,13 @@ const EventChatPage = () => {
|
|||||||
value={newMessage}
|
value={newMessage}
|
||||||
onChange={(e) => setNewMessage(e.target.value)}
|
onChange={(e) => setNewMessage(e.target.value)}
|
||||||
placeholder="Write a message..."
|
placeholder="Write a message..."
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500"
|
disabled={!isConnected}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
disabled={!isConnected}
|
||||||
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Send className="w-5 h-5" />
|
<Send className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import { useState, useRef, useEffect } from 'react';
|
|||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import Layout from '../components/layout/Layout';
|
import Layout from '../components/layout/Layout';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { mockPrivateMessages } from '../mocks/messages';
|
|
||||||
import { mockUsers } from '../mocks/users';
|
import { mockUsers } from '../mocks/users';
|
||||||
import { Send, Video, Upload, X, Check, Link as LinkIcon } from 'lucide-react';
|
import { Send, Video, Upload, X, Check, Link as LinkIcon } from 'lucide-react';
|
||||||
|
import { connectSocket, getSocket } from '../services/socket';
|
||||||
|
|
||||||
const MatchChatPage = () => {
|
const MatchChatPage = () => {
|
||||||
const { matchId } = useParams();
|
const { matchId } = useParams();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [messages, setMessages] = useState(mockPrivateMessages);
|
const [messages, setMessages] = useState([]);
|
||||||
const [newMessage, setNewMessage] = useState('');
|
const [newMessage, setNewMessage] = useState('');
|
||||||
const [selectedFile, setSelectedFile] = useState(null);
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
const [isTransferring, setIsTransferring] = useState(false);
|
const [isTransferring, setIsTransferring] = useState(false);
|
||||||
@@ -18,10 +18,11 @@ const MatchChatPage = () => {
|
|||||||
const [webrtcStatus, setWebrtcStatus] = useState('disconnected'); // disconnected, connecting, connected, failed
|
const [webrtcStatus, setWebrtcStatus] = useState('disconnected'); // disconnected, connecting, connected, failed
|
||||||
const [showLinkInput, setShowLinkInput] = useState(false);
|
const [showLinkInput, setShowLinkInput] = useState(false);
|
||||||
const [videoLink, setVideoLink] = useState('');
|
const [videoLink, setVideoLink] = useState('');
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
// Partner user (mockup)
|
// Partner user (mockup - TODO: fetch from backend in Phase 2)
|
||||||
const partner = mockUsers[1]; // sarah_swing
|
const partner = mockUsers[1]; // sarah_swing
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
@@ -32,21 +33,56 @@ const MatchChatPage = () => {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Connect to Socket.IO
|
||||||
|
const socket = connectSocket();
|
||||||
|
|
||||||
|
if (!socket) {
|
||||||
|
console.error('Failed to connect to socket');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Socket event listeners
|
||||||
|
socket.on('connect', () => {
|
||||||
|
setIsConnected(true);
|
||||||
|
// Join match room
|
||||||
|
socket.emit('join_match_room', { matchId: parseInt(matchId) });
|
||||||
|
console.log(`Joined match room ${matchId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
setIsConnected(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Receive messages
|
||||||
|
socket.on('match_message', (message) => {
|
||||||
|
setMessages((prev) => [...prev, message]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
socket.off('connect');
|
||||||
|
socket.off('disconnect');
|
||||||
|
socket.off('match_message');
|
||||||
|
};
|
||||||
|
}, [matchId, user.id]);
|
||||||
|
|
||||||
const handleSendMessage = (e) => {
|
const handleSendMessage = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!newMessage.trim()) return;
|
if (!newMessage.trim()) return;
|
||||||
|
|
||||||
const message = {
|
const socket = getSocket();
|
||||||
id: messages.length + 1,
|
if (!socket || !socket.connected) {
|
||||||
room_id: 10,
|
alert('Not connected to chat server');
|
||||||
user_id: user.id,
|
return;
|
||||||
username: user.username,
|
}
|
||||||
content: newMessage,
|
|
||||||
type: 'text',
|
// Send message via Socket.IO
|
||||||
created_at: new Date().toISOString(),
|
socket.emit('send_match_message', {
|
||||||
};
|
matchId: parseInt(matchId),
|
||||||
|
content: newMessage,
|
||||||
|
});
|
||||||
|
|
||||||
setMessages([...messages, message]);
|
|
||||||
setNewMessage('');
|
setNewMessage('');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -204,8 +240,13 @@ const MatchChatPage = () => {
|
|||||||
<div className="flex flex-col h-[calc(100vh-320px)]">
|
<div className="flex flex-col h-[calc(100vh-320px)]">
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
|
{messages.length === 0 && (
|
||||||
|
<div className="text-center text-gray-500 py-8">
|
||||||
|
No messages yet. Start the conversation!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{messages.map((message) => {
|
{messages.map((message) => {
|
||||||
const isOwnMessage = message.user_id === user.id;
|
const isOwnMessage = message.userId === user.id;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
@@ -213,7 +254,7 @@ const MatchChatPage = () => {
|
|||||||
>
|
>
|
||||||
<div className={`flex items-start space-x-2 max-w-md ${isOwnMessage ? 'flex-row-reverse space-x-reverse' : ''}`}>
|
<div className={`flex items-start space-x-2 max-w-md ${isOwnMessage ? 'flex-row-reverse space-x-reverse' : ''}`}>
|
||||||
<img
|
<img
|
||||||
src={isOwnMessage ? user.avatar : partner.avatar}
|
src={message.avatar}
|
||||||
alt={message.username}
|
alt={message.username}
|
||||||
className="w-8 h-8 rounded-full"
|
className="w-8 h-8 rounded-full"
|
||||||
/>
|
/>
|
||||||
@@ -223,7 +264,7 @@ const MatchChatPage = () => {
|
|||||||
{message.username}
|
{message.username}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
{new Date(message.created_at).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
|
{new Date(message.createdAt).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -376,11 +417,13 @@ const MatchChatPage = () => {
|
|||||||
value={newMessage}
|
value={newMessage}
|
||||||
onChange={(e) => setNewMessage(e.target.value)}
|
onChange={(e) => setNewMessage(e.target.value)}
|
||||||
placeholder="Write a message..."
|
placeholder="Write a message..."
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500"
|
disabled={!isConnected}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
disabled={!isConnected}
|
||||||
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Send className="w-5 h-5" />
|
<Send className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
56
frontend/src/services/socket.js
Normal file
56
frontend/src/services/socket.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { io } from 'socket.io-client';
|
||||||
|
|
||||||
|
const SOCKET_URL = 'http://localhost:8080';
|
||||||
|
|
||||||
|
let socket = null;
|
||||||
|
|
||||||
|
export function connectSocket() {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.error('No token found for socket connection');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socket && socket.connected) {
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket = io(SOCKET_URL, {
|
||||||
|
auth: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionAttempts: 5,
|
||||||
|
reconnectionDelay: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
console.log('✅ Socket connected:', socket.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', (reason) => {
|
||||||
|
console.log('❌ Socket disconnected:', reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('connect_error', (error) => {
|
||||||
|
console.error('Socket connection error:', error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (error) => {
|
||||||
|
console.error('Socket error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disconnectSocket() {
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect();
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSocket() {
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
@@ -49,15 +49,20 @@ server {
|
|||||||
proxy_read_timeout 60s;
|
proxy_read_timeout 60s;
|
||||||
}
|
}
|
||||||
|
|
||||||
# WebSocket dla Socket.IO (do dodania później)
|
# WebSocket for Socket.IO
|
||||||
# location /socket.io {
|
location /socket.io {
|
||||||
# proxy_pass http://backend;
|
proxy_pass http://backend;
|
||||||
# proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
#
|
|
||||||
# proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
# proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
# proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
# proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
# }
|
|
||||||
|
# Timeouts for WebSocket
|
||||||
|
proxy_connect_timeout 7d;
|
||||||
|
proxy_send_timeout 7d;
|
||||||
|
proxy_read_timeout 7d;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user