mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Merge pull request #164 from ets-cfuhrman-pfe/socket-image
QuizRoom separation and duplication
This commit is contained in:
commit
706308d54f
41 changed files with 3246 additions and 5755 deletions
31
.github/workflows/create-branch-images.yml
vendored
31
.github/workflows/create-branch-images.yml
vendored
|
|
@ -103,4 +103,35 @@ jobs:
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-quizroom:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Extract metadata for Quizroom Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ghcr.io/${{ github.repository }}-quizroom
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
- name: Build and push Quizroom Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./quizRoom
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
25
.vscode/launch.json
vendored
Normal file
25
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug backend",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"program": "${workspaceFolder}/server/app.js",
|
||||||
|
"cwd":"${workspaceFolder}/server/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "msedge",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug frontend",
|
||||||
|
"url": "http://localhost:5173",
|
||||||
|
"webRoot": "${workspaceFolder}/client/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
309
client/package-lock.json
generated
309
client/package-lock.json
generated
|
|
@ -19,6 +19,7 @@
|
||||||
"@mui/material": "^6.1.0",
|
"@mui/material": "^6.1.0",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
"dockerode": "^4.0.2",
|
||||||
"esbuild": "^0.23.1",
|
"esbuild": "^0.23.1",
|
||||||
"gift-pegjs": "^1.0.2",
|
"gift-pegjs": "^1.0.2",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
|
@ -1897,6 +1898,12 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@balena/dockerignore": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@bcoe/v8-coverage": {
|
"node_modules/@bcoe/v8-coverage": {
|
||||||
"version": "0.2.3",
|
"version": "0.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
||||||
|
|
@ -5190,6 +5197,15 @@
|
||||||
"dequal": "^2.0.3"
|
"dequal": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asn1": {
|
||||||
|
"version": "0.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||||
|
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": "~2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
"version": "3.2.6",
|
"version": "3.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
|
|
@ -5458,6 +5474,35 @@
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/bcrypt-pbkdf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tweetnacl": "^0.14.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
|
|
@ -5469,6 +5514,17 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^5.5.0",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
|
@ -5542,12 +5598,45 @@
|
||||||
"node-int64": "^0.4.0"
|
"node-int64": "^0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/buildcheck": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
|
|
@ -5658,6 +5747,12 @@
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chownr": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/ci-info": {
|
"node_modules/ci-info": {
|
||||||
"version": "3.9.0",
|
"version": "3.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||||
|
|
@ -5786,6 +5881,20 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cpu-features": {
|
||||||
|
"version": "0.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
|
||||||
|
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"buildcheck": "~0.0.6",
|
||||||
|
"nan": "^2.19.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/create-jest": {
|
"node_modules/create-jest": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
|
||||||
|
|
@ -6057,6 +6166,35 @@
|
||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/docker-modem": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"readable-stream": "^3.5.0",
|
||||||
|
"split-ca": "^1.0.1",
|
||||||
|
"ssh2": "^1.15.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dockerode": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@balena/dockerignore": "^1.0.2",
|
||||||
|
"docker-modem": "^5.0.3",
|
||||||
|
"tar-fs": "~2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dom-accessibility-api": {
|
"node_modules/dom-accessibility-api": {
|
||||||
"version": "0.5.16",
|
"version": "0.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
|
|
@ -6123,6 +6261,15 @@
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/engine.io-client": {
|
"node_modules/engine.io-client": {
|
||||||
"version": "6.5.4",
|
"version": "6.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
|
||||||
|
|
@ -6797,6 +6944,12 @@
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs-constants": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "11.2.0",
|
"version": "11.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
|
||||||
|
|
@ -7078,6 +7231,26 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
|
|
@ -7153,8 +7326,7 @@
|
||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/is-arrayish": {
|
"node_modules/is-arrayish": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
|
|
@ -10208,11 +10380,24 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mkdirp-classic": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"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=="
|
||||||
},
|
},
|
||||||
|
"node_modules/nan": {
|
||||||
|
"version": "2.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
|
||||||
|
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "5.0.7",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz",
|
||||||
|
|
@ -10284,7 +10469,6 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
|
|
@ -10661,6 +10845,16 @@
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||||
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
|
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
|
||||||
},
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|
@ -10805,6 +10999,20 @@
|
||||||
"react-dom": ">=16.6.0"
|
"react-dom": ">=16.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
|
@ -11047,6 +11255,26 @@
|
||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
|
@ -11182,12 +11410,35 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/split-ca": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/sprintf-js": {
|
"node_modules/sprintf-js": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/ssh2": {
|
||||||
|
"version": "1.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
|
||||||
|
"integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"asn1": "^0.2.6",
|
||||||
|
"bcrypt-pbkdf": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.16.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"cpu-features": "~0.0.10",
|
||||||
|
"nan": "^2.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stack-utils": {
|
"node_modules/stack-utils": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||||
|
|
@ -11207,6 +11458,15 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-length": {
|
"node_modules/string-length": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
|
||||||
|
|
@ -11319,6 +11579,34 @@
|
||||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
|
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/tar-fs": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^1.1.1",
|
||||||
|
"mkdirp-classic": "^0.5.2",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-stream": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^4.0.3",
|
||||||
|
"end-of-stream": "^1.4.1",
|
||||||
|
"fs-constants": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/test-exclude": {
|
"node_modules/test-exclude": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||||
|
|
@ -11539,6 +11827,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tweetnacl": {
|
||||||
|
"version": "0.14.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||||
|
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
|
@ -11765,6 +12059,12 @@
|
||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||||
|
|
@ -12637,8 +12937,7 @@
|
||||||
"node_modules/wrappy": {
|
"node_modules/wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/write-file-atomic": {
|
"node_modules/write-file-atomic": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"@mui/material": "^6.1.0",
|
"@mui/material": "^6.1.0",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
"dockerode": "^4.0.2",
|
||||||
"esbuild": "^0.23.1",
|
"esbuild": "^0.23.1",
|
||||||
"gift-pegjs": "^1.0.2",
|
"gift-pegjs": "^1.0.2",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ describe('WebSocketService', () => {
|
||||||
|
|
||||||
test('createRoom should emit create-room event', () => {
|
test('createRoom should emit create-room event', () => {
|
||||||
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
WebsocketService.createRoom();
|
WebsocketService.createRoom('test');
|
||||||
expect(mockSocket.emit).toHaveBeenCalledWith('create-room');
|
expect(mockSocket.emit).toHaveBeenCalledWith('create-room');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { ENV_VARIABLES } from '../../../constants';
|
//import { ENV_VARIABLES } from '../../../constants';
|
||||||
|
|
||||||
import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz';
|
import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz';
|
||||||
import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz';
|
import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz';
|
||||||
|
|
@ -27,14 +27,14 @@ const JoinRoom: React.FC = () => {
|
||||||
const [isConnecting, setIsConnecting] = useState<boolean>(false);
|
const [isConnecting, setIsConnecting] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleCreateSocket();
|
//handleCreateSocket();
|
||||||
return () => {
|
return () => {
|
||||||
disconnect();
|
disconnect();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCreateSocket = () => {
|
const handleCreateSocket = () => {
|
||||||
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
const socket = webSocketService.connect(`/api/room/${roomName}/socket`);
|
||||||
|
|
||||||
socket.on('join-success', () => {
|
socket.on('join-success', () => {
|
||||||
setIsWaitingForTeacher(true);
|
setIsWaitingForTeacher(true);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import webSocketService, { AnswerReceptionFromBackendType } from '../../../servi
|
||||||
import { QuizType } from '../../../Types/QuizType';
|
import { QuizType } from '../../../Types/QuizType';
|
||||||
|
|
||||||
import './manageRoom.css';
|
import './manageRoom.css';
|
||||||
import { ENV_VARIABLES } from '../../../constants';
|
//import { ENV_VARIABLES } from '../../../constants';
|
||||||
import { StudentType, Answer } from '../../../Types/StudentType';
|
import { StudentType, Answer } from '../../../Types/StudentType';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
|
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
|
||||||
|
|
@ -79,13 +79,19 @@ const ManageRoom: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const createWebSocketRoom = () => {
|
const createWebSocketRoom = async () => {
|
||||||
setConnectingError('');
|
setConnectingError('');
|
||||||
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
const room = await ApiService.createRoom();
|
||||||
|
const socket = webSocketService.connect(`/api/room/${room.id}/socket`);
|
||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
webSocketService.createRoom();
|
webSocketService.createRoom(room.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("error", (error) => {
|
||||||
|
console.error("WebSocket server error:", error);
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('connect_error', (error) => {
|
socket.on('connect_error', (error) => {
|
||||||
setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
|
setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
|
||||||
console.error('WebSocket connection error:', error);
|
console.error('WebSocket connection error:', error);
|
||||||
|
|
@ -142,7 +148,7 @@ const ManageRoom: React.FC = () => {
|
||||||
console.log('Quiz questions not found (cannot update answers without them).');
|
console.log('Quiz questions not found (cannot update answers without them).');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the students state using the functional form of setStudents
|
// Update the students state using the functional form of setStudents
|
||||||
setStudents((prevStudents) => {
|
setStudents((prevStudents) => {
|
||||||
// print the list of current student names
|
// print the list of current student names
|
||||||
|
|
@ -150,7 +156,7 @@ const ManageRoom: React.FC = () => {
|
||||||
prevStudents.forEach((student) => {
|
prevStudents.forEach((student) => {
|
||||||
console.log(student.name);
|
console.log(student.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
let foundStudent = false;
|
let foundStudent = false;
|
||||||
const updatedStudents = prevStudents.map((student) => {
|
const updatedStudents = prevStudents.map((student) => {
|
||||||
console.log(`Comparing ${student.id} to ${idUser}`);
|
console.log(`Comparing ${student.id} to ${idUser}`);
|
||||||
|
|
@ -170,7 +176,7 @@ const ManageRoom: React.FC = () => {
|
||||||
updatedAnswers = [...student.answers, newAnswer];
|
updatedAnswers = [...student.answers, newAnswer];
|
||||||
}
|
}
|
||||||
return { ...student, answers: updatedAnswers };
|
return { ...student, answers: updatedAnswers };
|
||||||
}
|
}
|
||||||
return student;
|
return student;
|
||||||
});
|
});
|
||||||
if (!foundStudent) {
|
if (!foundStudent) {
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,78 @@ class ApiService {
|
||||||
return localStorage.removeItem("jwt");
|
return localStorage.removeItem("jwt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Socket Route
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new room.
|
||||||
|
* @returns The room object if successful
|
||||||
|
* @returns An error string if unsuccessful
|
||||||
|
*/
|
||||||
|
public async createRoom(): Promise<any> {
|
||||||
|
try {
|
||||||
|
const url: string = this.constructRequestUrl(`/room`);
|
||||||
|
const headers = this.constructRequestHeaders();
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`La création de la salle a échoué. Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await response.json();
|
||||||
|
return room;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error details: ", error);
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message || 'Erreur serveur inconnue lors de la requête.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Une erreur inattendue s'est produite.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a room by its name.
|
||||||
|
* @param roomName - The name of the room to delete.
|
||||||
|
* @returns true if successful
|
||||||
|
* @returns An error string if unsuccessful
|
||||||
|
*/
|
||||||
|
public async deleteRoom(roomName: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
if (!roomName) {
|
||||||
|
throw new Error(`Le nom de la salle est requis.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = this.constructRequestUrl(`/room/${roomName}`);
|
||||||
|
const headers = this.constructRequestHeaders();
|
||||||
|
fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error details: ", error);
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message || 'Erreur serveur inconnue lors de la requête.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Une erreur inattendue s'est produite.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// User Routes
|
// User Routes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -302,6 +374,7 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns folder array if successful
|
* @returns folder array if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// WebSocketService.tsx
|
// WebSocketService.tsx
|
||||||
import { io, Socket } from 'socket.io-client';
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
import apiService from './ApiService';
|
||||||
|
|
||||||
// Must (manually) sync these types to server/socket/socket.js
|
// Must (manually) sync these types to server/socket/socket.js
|
||||||
|
|
||||||
|
|
@ -21,10 +22,14 @@ class WebSocketService {
|
||||||
private socket: Socket | null = null;
|
private socket: Socket | null = null;
|
||||||
|
|
||||||
connect(backendUrl: string): Socket {
|
connect(backendUrl: string): Socket {
|
||||||
// console.log(backendUrl);
|
this.socket = io( '/',{
|
||||||
this.socket = io(`${backendUrl}`, {
|
path: backendUrl,
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
reconnectionAttempts: 1
|
autoConnect: true,
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionAttempts: 10,
|
||||||
|
reconnectionDelay: 10000,
|
||||||
|
timeout: 20000,
|
||||||
});
|
});
|
||||||
return this.socket;
|
return this.socket;
|
||||||
}
|
}
|
||||||
|
|
@ -37,9 +42,9 @@ class WebSocketService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoom() {
|
createRoom(roomName: string) {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.emit('create-room');
|
this.socket.emit('create-room', roomName || undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,6 +63,8 @@ class WebSocketService {
|
||||||
endQuiz(roomName: string) {
|
endQuiz(roomName: string) {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.emit('end-quiz', { roomName });
|
this.socket.emit('end-quiz', { roomName });
|
||||||
|
//Delete room in mongoDb, roomContainer will be deleted in cleanup
|
||||||
|
apiService.deleteRoom(roomName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
100
docker-compose.local.yaml
Normal file
100
docker-compose.local.yaml
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./client
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: frontend
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
networks:
|
||||||
|
- quiz_network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./server
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: backend
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
PORT: 3000
|
||||||
|
MONGO_URI: "mongodb://mongo:27017/evaluetonsavoir"
|
||||||
|
MONGO_DATABASE: evaluetonsavoir
|
||||||
|
EMAIL_SERVICE: gmail
|
||||||
|
SENDER_EMAIL: infoevaluetonsavoir@gmail.com
|
||||||
|
EMAIL_PSW: 'vvml wmfr dkzb vjzb'
|
||||||
|
JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe
|
||||||
|
FRONTEND_URL: "http://localhost:5173"
|
||||||
|
depends_on:
|
||||||
|
- mongo
|
||||||
|
networks:
|
||||||
|
- quiz_network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
quizroom: # Forces image to update
|
||||||
|
build:
|
||||||
|
context: ./quizRoom
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: quizroom
|
||||||
|
ports:
|
||||||
|
- "4500:4500"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- quiz_network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
build:
|
||||||
|
context: ./nginx
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
- frontend
|
||||||
|
networks:
|
||||||
|
- quiz_network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
mongo:
|
||||||
|
image: mongo
|
||||||
|
container_name: mongo
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- mongodb_data:/data/db
|
||||||
|
networks:
|
||||||
|
- quiz_network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
watchtower:
|
||||||
|
image: containrrr/watchtower
|
||||||
|
container_name: watchtower
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
- TZ=America/Montreal
|
||||||
|
- WATCHTOWER_CLEANUP=true
|
||||||
|
- WATCHTOWER_DEBUG=true
|
||||||
|
- WATCHTOWER_INCLUDE_RESTARTING=true
|
||||||
|
- WATCHTOWER_SCHEDULE=0 0 5 * * * # At 5 am everyday
|
||||||
|
networks:
|
||||||
|
- quiz_network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
networks:
|
||||||
|
quiz_network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodb_data:
|
||||||
|
external: false
|
||||||
|
|
@ -27,6 +27,17 @@ services:
|
||||||
- mongo
|
- mongo
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
quizroom:
|
||||||
|
build:
|
||||||
|
context: ./quizRoom
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: quizroom
|
||||||
|
ports:
|
||||||
|
- "4500:4500"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: always
|
||||||
|
|
||||||
# Ce conteneur sert de routeur pour assurer le bon fonctionnement de l'application
|
# Ce conteneur sert de routeur pour assurer le bon fonctionnement de l'application
|
||||||
nginx:
|
nginx:
|
||||||
image: fuhrmanator/evaluetonsavoir-routeur:latest
|
image: fuhrmanator/evaluetonsavoir-routeur:latest
|
||||||
|
|
@ -49,13 +60,6 @@ services:
|
||||||
- mongodb_data:/data/db
|
- mongodb_data:/data/db
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
# Ce conteneur contient la base de donnée cache nécessaire au bon fonctionnement des salles de quiz
|
|
||||||
valkey:
|
|
||||||
image: valkey/valkey:alpine
|
|
||||||
container_name: valkey
|
|
||||||
volumes:
|
|
||||||
- ./deployments/valkey.conf:/usr/local/etc/valkey/valkey.conf
|
|
||||||
|
|
||||||
# Ce conteneur assure que l'application est à jour en allant chercher s'il y a des mises à jours à chaque heure
|
# Ce conteneur assure que l'application est à jour en allant chercher s'il y a des mises à jours à chaque heure
|
||||||
watchtower:
|
watchtower:
|
||||||
image: containrrr/watchtower
|
image: containrrr/watchtower
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,69 @@
|
||||||
FROM nginx
|
# Stage 1: Build stage
|
||||||
|
FROM nginx:1.27-alpine AS builder
|
||||||
|
|
||||||
COPY ./default.conf /etc/nginx/conf.d/default.conf
|
# Install required packages
|
||||||
|
RUN apk add --no-cache nginx-mod-http-js nginx-mod-http-keyval
|
||||||
|
|
||||||
|
# Stage 2: Final stage
|
||||||
|
FROM alpine:3.19
|
||||||
|
|
||||||
|
# Copy Nginx and NJS modules from builder
|
||||||
|
COPY --from=builder /usr/sbin/nginx /usr/sbin/
|
||||||
|
COPY --from=builder /usr/lib/nginx/modules/ /usr/lib/nginx/modules/
|
||||||
|
COPY --from=builder /etc/nginx/ /etc/nginx/
|
||||||
|
COPY --from=builder /usr/lib/nginx/ /usr/lib/nginx/
|
||||||
|
|
||||||
|
# Install required runtime dependencies
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
pcre2 \
|
||||||
|
ca-certificates \
|
||||||
|
pcre \
|
||||||
|
libgcc \
|
||||||
|
libstdc++ \
|
||||||
|
zlib \
|
||||||
|
libxml2 \
|
||||||
|
libedit \
|
||||||
|
geoip \
|
||||||
|
libxslt \
|
||||||
|
&& mkdir -p /var/cache/nginx \
|
||||||
|
&& mkdir -p /var/log/nginx \
|
||||||
|
&& mkdir -p /etc/nginx/conf.d \
|
||||||
|
&& mkdir -p /etc/nginx/njs \
|
||||||
|
&& ln -sf /dev/stdout /var/log/nginx/access.log \
|
||||||
|
&& ln -sf /dev/stderr /var/log/nginx/error.log \
|
||||||
|
&& addgroup -S nginx \
|
||||||
|
&& adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx
|
||||||
|
|
||||||
|
# Copy necessary libraries from builder
|
||||||
|
COPY --from=builder /usr/lib/libxml2.so* /usr/lib/
|
||||||
|
COPY --from=builder /usr/lib/libexslt.so* /usr/lib/
|
||||||
|
COPY --from=builder /usr/lib/libgd.so* /usr/lib/
|
||||||
|
COPY --from=builder /usr/lib/libxslt.so* /usr/lib/
|
||||||
|
|
||||||
|
# Modify nginx.conf to load modules
|
||||||
|
RUN echo 'load_module modules/ngx_http_js_module.so;' > /tmp/nginx.conf && \
|
||||||
|
cat /etc/nginx/nginx.conf >> /tmp/nginx.conf && \
|
||||||
|
mv /tmp/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Copy our configuration
|
||||||
|
COPY conf.d/default.conf /etc/nginx/conf.d/
|
||||||
|
COPY njs/main.js /etc/nginx/njs/
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
RUN chown -R nginx:nginx /var/cache/nginx \
|
||||||
|
&& chown -R nginx:nginx /var/log/nginx \
|
||||||
|
&& chown -R nginx:nginx /etc/nginx/conf.d \
|
||||||
|
&& touch /var/run/nginx.pid \
|
||||||
|
&& chown -R nginx:nginx /var/run/nginx.pid
|
||||||
|
|
||||||
|
# Verify the configuration
|
||||||
|
# RUN nginx -t --dry-run
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER nginx
|
||||||
|
|
||||||
|
# Expose HTTP port
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Start Nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
59
nginx/conf.d/default.conf
Normal file
59
nginx/conf.d/default.conf
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
js_import njs/main.js;
|
||||||
|
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream frontend {
|
||||||
|
server frontend:5173;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream backend {
|
||||||
|
server backend:3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
set $proxy_target "";
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Game WebSocket routing
|
||||||
|
location ~/api/room/([^/]+)/socket {
|
||||||
|
set $room_id $1;
|
||||||
|
js_content main.routeWebSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
# WebSocket proxy location
|
||||||
|
location @websocket_proxy {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
# Timeouts
|
||||||
|
proxy_connect_timeout 7m;
|
||||||
|
proxy_send_timeout 7m;
|
||||||
|
proxy_read_timeout 7m;
|
||||||
|
proxy_buffering off;
|
||||||
|
|
||||||
|
proxy_pass $proxy_target;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://frontend;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
upstream frontend {
|
|
||||||
server frontend:5173;
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream backend {
|
|
||||||
server backend:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
|
|
||||||
location /api {
|
|
||||||
rewrite /backend/(.*) /$1 break;
|
|
||||||
proxy_pass http://backend;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /socket.io {
|
|
||||||
rewrite /backend/(.*) /$1 break;
|
|
||||||
proxy_pass http://backend;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "Upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_hide_header 'Access-Control-Allow-Origin';
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://frontend;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
54
nginx/njs/main.js
Normal file
54
nginx/njs/main.js
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
async function fetchRoomInfo(r) {
|
||||||
|
try {
|
||||||
|
// Make request to API to get room info
|
||||||
|
let res = await r.subrequest('/api/room/' + r.variables.room_id, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
r.error(`Failed to fetch room info: ${res.status}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let room = JSON.parse(res.responseText);
|
||||||
|
r.error(`Debug: Room info: ${JSON.stringify(room)}`); // Debug log
|
||||||
|
return room;
|
||||||
|
} catch (error) {
|
||||||
|
r.error(`Error fetching room info: ${error}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function routeWebSocket(r) {
|
||||||
|
try {
|
||||||
|
const roomInfo = await fetchRoomInfo(r);
|
||||||
|
|
||||||
|
if (!roomInfo || !roomInfo.host) {
|
||||||
|
r.error(`Debug: Invalid room info: ${JSON.stringify(roomInfo)}`);
|
||||||
|
r.return(404, 'Room not found or invalid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the host includes protocol if not already present
|
||||||
|
let proxyUrl = roomInfo.host;
|
||||||
|
if (!proxyUrl.startsWith('http://') && !proxyUrl.startsWith('https://')) {
|
||||||
|
proxyUrl = 'http://' + proxyUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
r.error(`Debug: Original URL: ${r.uri}`);
|
||||||
|
r.error(`Debug: Setting proxy target to: ${proxyUrl}`);
|
||||||
|
r.error(`Debug: Headers: ${JSON.stringify(r.headersIn)}`);
|
||||||
|
|
||||||
|
// Set the proxy target variable
|
||||||
|
r.variables.proxy_target = proxyUrl;
|
||||||
|
|
||||||
|
// Redirect to the websocket proxy
|
||||||
|
r.internalRedirect('@websocket_proxy');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
r.error(`WebSocket routing error: ${error}`);
|
||||||
|
r.return(500, 'Internal routing error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { routeWebSocket };
|
||||||
2
quizRoom/.dockerignore
Normal file
2
quizRoom/.dockerignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
|
@ -1,18 +1,29 @@
|
||||||
# Use the Node base image
|
# Use the Node base image
|
||||||
FROM node:18
|
FROM node:18 AS quizroom
|
||||||
|
|
||||||
|
ARG PORT=4500
|
||||||
|
ENV PORT=${PORT}
|
||||||
|
ENV ROOM_ID=${ROOM_ID}
|
||||||
|
|
||||||
# Create a working directory
|
# Create a working directory
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
# Copy package.json and install dependencies
|
# Copy package.json and package-lock.json (if available) and install dependencies
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
# Copy all source code to the container
|
# Copy the rest of the source code to the container
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Expose WebSocket server port
|
# Build the TypeScript code
|
||||||
EXPOSE 4500
|
RUN npm run build
|
||||||
|
|
||||||
# Start the WebSocket server
|
# Expose WebSocket server port
|
||||||
CMD ["node", "app.js"]
|
EXPOSE ${PORT}
|
||||||
|
|
||||||
|
# Add healthcheck
|
||||||
|
HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 \
|
||||||
|
CMD /usr/src/app/healthcheck.sh
|
||||||
|
|
||||||
|
# Start the server using the compiled JavaScript file
|
||||||
|
CMD ["node", "dist/app.js"]
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,43 @@
|
||||||
import http from "http";
|
import http from "http";
|
||||||
import { Server, ServerOptions } from "socket.io";
|
import { Server, ServerOptions } from "socket.io";
|
||||||
|
|
||||||
// Import setupWebsocket
|
|
||||||
import { setupWebsocket } from "./socket/setupWebSocket";
|
import { setupWebsocket } from "./socket/setupWebSocket";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
const port = process.env.WS_PORT ? parseInt(process.env.WS_PORT) : 4500;
|
// Load environment variables
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const port = process.env.PORT || 4500;
|
||||||
|
const roomId = process.env.ROOM_ID;
|
||||||
|
console.log(`I am: /api/room/${roomId}/socket`);
|
||||||
|
|
||||||
|
// Create Express app for health check
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get('/health', (_, res) => {
|
||||||
|
try {
|
||||||
|
if (io.engine?.clientsCount !== undefined) {
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'healthy',
|
||||||
|
path: `/api/room/${roomId}/socket`,
|
||||||
|
connections: io.engine.clientsCount,
|
||||||
|
uptime: process.uptime()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('Socket.io server not initialized');
|
||||||
|
}
|
||||||
|
} catch (error: Error | any) {
|
||||||
|
res.status(500).json({
|
||||||
|
status: 'unhealthy',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Create HTTP and WebSocket server
|
|
||||||
const server = http.createServer();
|
|
||||||
const ioOptions: Partial<ServerOptions> = {
|
const ioOptions: Partial<ServerOptions> = {
|
||||||
path: "/socket.io",
|
path: `/api/room/${roomId}/socket`,
|
||||||
cors: {
|
cors: {
|
||||||
origin: "*",
|
origin: "*",
|
||||||
methods: ["GET", "POST"],
|
methods: ["GET", "POST"],
|
||||||
|
|
@ -24,4 +52,4 @@ setupWebsocket(io);
|
||||||
|
|
||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
console.log(`WebSocket server is running on port ${port}`);
|
console.log(`WebSocket server is running on port ${port}`);
|
||||||
});
|
});
|
||||||
19
quizRoom/docker-compose.yml
Normal file
19
quizRoom/docker-compose.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
quizroom:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- PORT=${PORT:-4500}
|
||||||
|
ports:
|
||||||
|
- "${PORT:-4500}:${PORT:-4500}"
|
||||||
|
environment:
|
||||||
|
- PORT=${PORT:-4500}
|
||||||
|
- ROOM_ID=${ROOM_ID}
|
||||||
|
healthcheck:
|
||||||
|
test: curl -f http://localhost:${PORT:-4500}/health || exit 1
|
||||||
|
interval: 30s
|
||||||
|
timeout: 30s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
2
quizRoom/healthcheck.sh
Normal file
2
quizRoom/healthcheck.sh
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
curl -f "http://0.0.0.0:${PORT}/health" || exit 1
|
||||||
4226
quizRoom/package-lock.json
generated
4226
quizRoom/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,21 +1,25 @@
|
||||||
{
|
{
|
||||||
"name": "ets-pfe004-evaluetonsavoir-quizroom",
|
"name": "quizroom",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "app.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node app.js",
|
"start": "node dist/app.js",
|
||||||
"test": "jest --colors"
|
"build": "tsc",
|
||||||
|
"dev": "ts-node app.ts"
|
||||||
},
|
},
|
||||||
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"socket.io": "^4.7.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jest": "^29.7.0",
|
"@types/express": "^5.0.0",
|
||||||
"jest-mock": "^29.7.0"
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.6.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.21.1",
|
||||||
|
"http": "^0.0.1-security",
|
||||||
|
"socket.io": "^4.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
quizRoom/socket/.env.example
Normal file
2
quizRoom/socket/.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ROOM_ID=123456
|
||||||
|
PORT=4500
|
||||||
|
|
@ -17,8 +17,12 @@ export const setupWebsocket = (io: Server): void => {
|
||||||
totalConnections++;
|
totalConnections++;
|
||||||
console.log("A user connected:", socket.id, "| Total connections:", totalConnections);
|
console.log("A user connected:", socket.id, "| Total connections:", totalConnections);
|
||||||
|
|
||||||
socket.on("create-room", (sentRoomName?: string) => {
|
socket.on("create-room", (sentRoomName) => {
|
||||||
const roomName = sentRoomName ? sentRoomName.toUpperCase() : generateRoomName();
|
// Ensure sentRoomName is a string before applying toUpperCase()
|
||||||
|
const roomName = (typeof sentRoomName === "string" && sentRoomName.trim() !== "")
|
||||||
|
? sentRoomName.toUpperCase()
|
||||||
|
: generateRoomName();
|
||||||
|
|
||||||
if (!io.sockets.adapter.rooms.get(roomName)) {
|
if (!io.sockets.adapter.rooms.get(roomName)) {
|
||||||
socket.join(roomName);
|
socket.join(roomName);
|
||||||
socket.emit("create-success", roomName);
|
socket.emit("create-success", roomName);
|
||||||
|
|
@ -88,8 +92,14 @@ export const setupWebsocket = (io: Server): void => {
|
||||||
idQuestion,
|
idQuestion,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("error", (error) => {
|
||||||
|
console.error("WebSocket server error:", error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const generateRoomName = (length = 6): string => {
|
const generateRoomName = (length = 6): string => {
|
||||||
const characters = "0123456789";
|
const characters = "0123456789";
|
||||||
let result = "";
|
let result = "";
|
||||||
|
|
|
||||||
14
quizRoom/tsconfig.json
Normal file
14
quizRoom/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES6",
|
||||||
|
"module": "commonjs",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["./**/*"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -3,10 +3,6 @@ const express = require("express");
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
|
|
||||||
// Import Sockets
|
|
||||||
const { setupWebsocket } = require("./socket/socket");
|
|
||||||
const { Server } = require("socket.io");
|
|
||||||
|
|
||||||
// instantiate the db
|
// instantiate the db
|
||||||
const db = require('./config/db.js');
|
const db = require('./config/db.js');
|
||||||
// instantiate the models
|
// instantiate the models
|
||||||
|
|
@ -18,6 +14,13 @@ const users = require('./models/users.js');
|
||||||
const userModel = new users(db, foldersModel);
|
const userModel = new users(db, foldersModel);
|
||||||
const images = require('./models/images.js');
|
const images = require('./models/images.js');
|
||||||
const imageModel = new images(db);
|
const imageModel = new images(db);
|
||||||
|
const {RoomRepository} = require('./models/room.js');
|
||||||
|
const roomRepModel = new RoomRepository(db);
|
||||||
|
|
||||||
|
// Instantiate the controllers
|
||||||
|
const QuizProviderOptions = {
|
||||||
|
provider: 'docker'
|
||||||
|
};
|
||||||
|
|
||||||
// instantiate the controllers
|
// instantiate the controllers
|
||||||
const usersController = require('./controllers/users.js');
|
const usersController = require('./controllers/users.js');
|
||||||
|
|
@ -28,18 +31,22 @@ const quizController = require('./controllers/quiz.js');
|
||||||
const quizControllerInstance = new quizController(quizModel, foldersModel);
|
const quizControllerInstance = new quizController(quizModel, foldersModel);
|
||||||
const imagesController = require('./controllers/images.js');
|
const imagesController = require('./controllers/images.js');
|
||||||
const imagesControllerInstance = new imagesController(imageModel);
|
const imagesControllerInstance = new imagesController(imageModel);
|
||||||
|
const roomsController = require('./controllers/rooms.js');
|
||||||
|
const roomsControllerInstance = new roomsController(QuizProviderOptions,roomRepModel);
|
||||||
|
|
||||||
// export the controllers
|
// export the controllers
|
||||||
module.exports.users = usersControllerInstance;
|
module.exports.users = usersControllerInstance;
|
||||||
module.exports.folders = foldersControllerInstance;
|
module.exports.folders = foldersControllerInstance;
|
||||||
module.exports.quizzes = quizControllerInstance;
|
module.exports.quizzes = quizControllerInstance;
|
||||||
module.exports.images = imagesControllerInstance;
|
module.exports.images = imagesControllerInstance;
|
||||||
|
module.exports.rooms = roomsControllerInstance;
|
||||||
|
|
||||||
//import routers (instantiate controllers as side effect)
|
//import routers (instantiate controllers as side effect)
|
||||||
const userRouter = require('./routers/users.js');
|
const userRouter = require('./routers/users.js');
|
||||||
const folderRouter = require('./routers/folders.js');
|
const folderRouter = require('./routers/folders.js');
|
||||||
const quizRouter = require('./routers/quiz.js');
|
const quizRouter = require('./routers/quiz.js');
|
||||||
const imagesRouter = require('./routers/images.js');
|
const imagesRouter = require('./routers/images.js');
|
||||||
|
const roomRouter = require('./routers/rooms.js');
|
||||||
|
|
||||||
// Setup environment
|
// Setup environment
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
@ -50,27 +57,10 @@ const app = express();
|
||||||
const cors = require("cors");
|
const cors = require("cors");
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
|
|
||||||
const configureServer = (httpServer, isDev) => {
|
let server = http.createServer(app);
|
||||||
return new Server(httpServer, {
|
|
||||||
path: "/socket.io",
|
|
||||||
cors: {
|
|
||||||
origin: "*",
|
|
||||||
methods: ["GET", "POST"],
|
|
||||||
credentials: true,
|
|
||||||
},
|
|
||||||
secure: !isDev, // true for https, false for http
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start sockets (depending on the dev or prod environment)
|
|
||||||
let server = http.createServer(app);
|
|
||||||
let isDev = process.env.NODE_ENV === 'development';
|
let isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
console.log(`Environnement: ${process.env.NODE_ENV} (${isDev ? 'dev' : 'prod'})`);
|
console.log(`Environnement: ${process.env.NODE_ENV} (${isDev ? 'dev' : 'prod'})`);
|
||||||
|
|
||||||
const io = configureServer(server);
|
|
||||||
|
|
||||||
setupWebsocket(io);
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
|
|
@ -80,6 +70,7 @@ app.use('/api/user', userRouter);
|
||||||
app.use('/api/folder', folderRouter);
|
app.use('/api/folder', folderRouter);
|
||||||
app.use('/api/quiz', quizRouter);
|
app.use('/api/quiz', quizRouter);
|
||||||
app.use('/api/image', imagesRouter);
|
app.use('/api/image', imagesRouter);
|
||||||
|
app.use('/api/room', roomRouter);
|
||||||
|
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
|
|
||||||
94
server/controllers/rooms.js
Normal file
94
server/controllers/rooms.js
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
const {Room} = require('../models/room.js');
|
||||||
|
|
||||||
|
const BaseRoomProvider = require('../roomsProviders/base-provider.js');
|
||||||
|
//const ClusterRoomProvider = require('../roomsProviders/cluster-provider.js');
|
||||||
|
const DockerRoomProvider = require('../roomsProviders/docker-provider.js');
|
||||||
|
//const KubernetesRoomProvider = require('../roomsProviders/kubernetes-provider');
|
||||||
|
|
||||||
|
const NB_CODE_CHARS = 6;
|
||||||
|
const NB_MS_UPDATE_ROOM = 1000;
|
||||||
|
const NB_MS_CLEANUP = 30000;
|
||||||
|
|
||||||
|
class RoomsController {
|
||||||
|
constructor(options = {}, roomRepository) {
|
||||||
|
this.provider = this.createProvider(
|
||||||
|
options.provider || process.env.ROOM_PROVIDER || 'cluster',
|
||||||
|
options.providerOptions,
|
||||||
|
roomRepository
|
||||||
|
);
|
||||||
|
this.roomRepository = roomRepository;
|
||||||
|
this.setupTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
createProvider(type, options, repository) {
|
||||||
|
switch (type) {
|
||||||
|
/*
|
||||||
|
case 'cluster':
|
||||||
|
return new ClusterRoomProvider(options, this.roomRepository);
|
||||||
|
*/
|
||||||
|
// Uncomment these as needed
|
||||||
|
case 'docker':
|
||||||
|
return new DockerRoomProvider(options, repository);
|
||||||
|
/*
|
||||||
|
case 'kubernetes':
|
||||||
|
return new KubernetesRoomProvider(options);
|
||||||
|
*/
|
||||||
|
default:
|
||||||
|
throw new Error(`Type d'approvisionement inconnu: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setupTasks(){
|
||||||
|
await this.provider.syncInstantiatedRooms();
|
||||||
|
// Update rooms
|
||||||
|
setInterval(() => {
|
||||||
|
this.provider.updateRoomsInfo().catch(console.error);
|
||||||
|
}, NB_MS_UPDATE_ROOM);
|
||||||
|
|
||||||
|
// Cleanup rooms
|
||||||
|
setInterval(() => {
|
||||||
|
this.provider.cleanup().catch(console.error);
|
||||||
|
}, NB_MS_CLEANUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRoom(options = {}) {
|
||||||
|
let roomIdValid = false
|
||||||
|
let roomId;
|
||||||
|
|
||||||
|
while(!roomIdValid){
|
||||||
|
roomId = options.roomId || this.generateRoomId();
|
||||||
|
roomIdValid = !(await this.provider.getRoomInfo(roomId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.provider.createRoom(roomId,options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRoom(roomId, info) {
|
||||||
|
return await this.provider.updateRoomInfo(roomId, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRoom(roomId) {
|
||||||
|
return await this.provider.deleteRoom(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRoomStatus(roomId) {
|
||||||
|
return await this.provider.getRoomStatus(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listRooms() {
|
||||||
|
return await this.provider.listRooms();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateRoomId() {
|
||||||
|
const characters = "0123456789";
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < NB_CODE_CHARS; i++) {
|
||||||
|
result += characters.charAt(
|
||||||
|
Math.floor(Math.random() * characters.length)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RoomsController;
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
import { GlideClient, GlideClientConfiguration } from '@valkey/valkey-glide';
|
|
||||||
import {
|
|
||||||
RoomInfo,
|
|
||||||
RoomOptions,
|
|
||||||
ProviderType,
|
|
||||||
ProviderConfig
|
|
||||||
} from '../../types/room';
|
|
||||||
import { BaseRoomProvider } from './providers/base-provider';
|
|
||||||
import { ClusterRoomProvider } from './providers/cluster-provider';
|
|
||||||
import { DockerRoomProvider } from './providers/docker-provider';
|
|
||||||
import { KubernetesRoomProvider } from './providers/kubernetes-provider';
|
|
||||||
|
|
||||||
interface RoomManagerOptions {
|
|
||||||
valkeyConfig?: GlideClientConfiguration;
|
|
||||||
provider?: ProviderType;
|
|
||||||
providerOptions?: ProviderConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RoomManager {
|
|
||||||
private valkey: GlideClient;
|
|
||||||
private provider: BaseRoomProvider<RoomInfo>;
|
|
||||||
|
|
||||||
constructor(options: RoomManagerOptions = {}) {
|
|
||||||
this.valkey = new GlideClient();
|
|
||||||
this.provider = this.createProvider(
|
|
||||||
options.provider || process.env.ROOM_PROVIDER as ProviderType || 'cluster',
|
|
||||||
options.providerOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
this.setupCleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
private createProvider(
|
|
||||||
type: ProviderType,
|
|
||||||
options?: ProviderConfig
|
|
||||||
): BaseRoomProvider<RoomInfo> {
|
|
||||||
switch (type) {
|
|
||||||
case 'cluster':
|
|
||||||
return new ClusterRoomProvider(this.redis, options);
|
|
||||||
case 'docker':
|
|
||||||
return new DockerRoomProvider(this.redis, options);
|
|
||||||
case 'kubernetes':
|
|
||||||
return new KubernetesRoomProvider(this.redis, options);
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown provider type: ${type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupCleanup(): void {
|
|
||||||
setInterval(() => {
|
|
||||||
this.provider.cleanup().catch(console.error);
|
|
||||||
}, 30000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createRoom(options: RoomOptions = {}): Promise<RoomInfo> {
|
|
||||||
const roomId = options.roomId || this.generateRoomId();
|
|
||||||
return await this.provider.createRoom(roomId, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteRoom(roomId: string): Promise<void> {
|
|
||||||
return await this.provider.deleteRoom(roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRoomStatus(roomId: string): Promise<RoomInfo | null> {
|
|
||||||
return await this.provider.getRoomStatus(roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listRooms(): Promise<RoomInfo[]> {
|
|
||||||
return await this.provider.listRooms();
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateRoomId(): string {
|
|
||||||
return `room-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -22,7 +22,7 @@ class Token {
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new AppError(UNAUTHORIZED_INVALID_TOKEN)
|
throw new AppError(UNAUTHORIZED_INVALID_TOKEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.user = payload;
|
req.user = payload;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ class Quiz {
|
||||||
await this.db.connect()
|
await this.db.connect()
|
||||||
const conn = this.db.getConnection();
|
const conn = this.db.getConnection();
|
||||||
|
|
||||||
|
|
||||||
const quizCollection = conn.collection('files');
|
const quizCollection = conn.collection('files');
|
||||||
|
|
||||||
const quiz = await quizCollection.findOne({ _id: ObjectId.createFromHexString(quizId) });
|
const quiz = await quizCollection.findOne({ _id: ObjectId.createFromHexString(quizId) });
|
||||||
|
|
|
||||||
87
server/models/room.js
Normal file
87
server/models/room.js
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
class Room {
|
||||||
|
constructor(id, name, host, nbStudents = 0,) { // Default nbStudents to 0
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
|
||||||
|
if (!host.startsWith('http://') && !host.startsWith('https://')) {
|
||||||
|
host = 'http://' + host;
|
||||||
|
}
|
||||||
|
this.host = host;
|
||||||
|
|
||||||
|
this.nbStudents = nbStudents;
|
||||||
|
this.mustBeCleaned = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoomRepository {
|
||||||
|
constructor(db) {
|
||||||
|
this.db = db;
|
||||||
|
this.connection = null;
|
||||||
|
this.collection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (!this.connection) {
|
||||||
|
await this.db.connect();
|
||||||
|
this.connection = this.db.getConnection();
|
||||||
|
}
|
||||||
|
if (!this.collection) this.collection = this.connection.collection('rooms');
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(room) {
|
||||||
|
await this.init();
|
||||||
|
const existingRoom = await this.collection.findOne({ id: room.id });
|
||||||
|
if (existingRoom) {
|
||||||
|
throw new Error(`Érreur: la salle ${room.id} existe déja`);
|
||||||
|
}
|
||||||
|
const returnedId = await this.collection.insertOne(room);
|
||||||
|
return await this.collection.findOne({ _id: returnedId.insertedId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id) {
|
||||||
|
await this.init();
|
||||||
|
const existingRoom = await this.collection.findOne({ id: id });
|
||||||
|
if (!existingRoom) {
|
||||||
|
console.warn(`La sale avec l'identifiant ${id} n'as pas été trouvé.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return existingRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll() {
|
||||||
|
await this.init();
|
||||||
|
return await this.collection.find().toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(room,roomId = null) {
|
||||||
|
await this.init();
|
||||||
|
|
||||||
|
const searchId = roomId ?? room.id;
|
||||||
|
|
||||||
|
const result = await this.collection.updateOne(
|
||||||
|
{ id: searchId },
|
||||||
|
{ $set: room },
|
||||||
|
{ upsert: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.modifiedCount === 0) {
|
||||||
|
if (result.matchedCount > 0) {
|
||||||
|
return true; // Document exists but no changes needed
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
await this.init();
|
||||||
|
const result = await this.collection.deleteOne({ id: id });
|
||||||
|
if (result.deletedCount === 0) {
|
||||||
|
console.warn(`La salle ${id} n'as pas été trouvée pour éffectuer sa suppression.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Room, RoomRepository };
|
||||||
2770
server/package-lock.json
generated
2770
server/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -14,14 +14,15 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@valkey/valkey-glide": "^1.1.0",
|
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"dockerode": "^4.0.2",
|
||||||
"dotenv": "^16.4.4",
|
"dotenv": "^16.4.4",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mongodb": "^6.3.0",
|
"mongodb": "^6.3.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"net": "^1.0.2",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
"socket.io": "^4.7.2",
|
"socket.io": "^4.7.2",
|
||||||
"socket.io-client": "^4.7.2"
|
"socket.io-client": "^4.7.2"
|
||||||
|
|
|
||||||
75
server/roomsProviders/base-provider.js
Normal file
75
server/roomsProviders/base-provider.js
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @typedef {import('../../types/room').RoomInfo} RoomInfo
|
||||||
|
* @typedef {import('../../types/room').RoomOptions} RoomOptions
|
||||||
|
* @typedef {import('../../types/room').BaseProviderConfig} BaseProviderConfig
|
||||||
|
*/
|
||||||
|
|
||||||
|
const MIN_NB_SECONDS_BEFORE_CLEANUP = process.env.MIN_NB_SECONDS_BEFORE_CLEANUP || 60
|
||||||
|
|
||||||
|
class BaseRoomProvider {
|
||||||
|
constructor(config = {}, roomRepository) {
|
||||||
|
this.config = config;
|
||||||
|
this.roomRepository = roomRepository;
|
||||||
|
|
||||||
|
this.quiz_docker_image = process.env.QUIZROOM_IMAGE || "evaluetonsavoir-quizroom";
|
||||||
|
this.quiz_docker_port = process.env.QUIZROOM_PORT || 4500;
|
||||||
|
this.quiz_expose_port = process.env.QUIZROOM_EXPOSE_PORT || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRoom(roomId, options) {
|
||||||
|
throw new Error("Fonction non-implantée - classe abstraite");
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRoom(roomId) {
|
||||||
|
throw new Error("Fonction non-implantée - classe abstraite");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRoomStatus(roomId) {
|
||||||
|
throw new Error("Fonction non-implantée - classe abstraite");
|
||||||
|
}
|
||||||
|
|
||||||
|
async listRooms() {
|
||||||
|
throw new Error("Fonction non-implantée - classe abstraite");
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup() {
|
||||||
|
throw new Error("Fonction non-implantée - classe abstraite");
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncInstantiatedRooms(){
|
||||||
|
throw new Error("Fonction non-implantée - classe abstraite");
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRoomsInfo() {
|
||||||
|
const rooms = await this.roomRepository.getAll();
|
||||||
|
for(var room of rooms){
|
||||||
|
const url = `${room.host}/health`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
room.mustBeCleaned = true;
|
||||||
|
await this.roomRepository.update(room);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
room.nbStudents = json.connections;
|
||||||
|
room.mustBeCleaned = room.nbStudents === 0 && json.uptime >MIN_NB_SECONDS_BEFORE_CLEANUP;
|
||||||
|
|
||||||
|
await this.roomRepository.update(room);
|
||||||
|
} catch (error) {
|
||||||
|
room.mustBeCleaned = true;
|
||||||
|
await this.roomRepository.update(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRoomInfo(roomId) {
|
||||||
|
const info = await this.roomRepository.get(roomId);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BaseRoomProvider;
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import { GlideClient, GlideString } from "@valkey/valkey-glide";
|
|
||||||
import { RoomInfo, RoomOptions, BaseProviderConfig } from '../../types/room';
|
|
||||||
|
|
||||||
export abstract class BaseRoomProvider<T extends RoomInfo> {
|
|
||||||
protected valkey: GlideClient;
|
|
||||||
|
|
||||||
constructor(valkey: GlideClient, protected config: BaseProviderConfig = {}) {
|
|
||||||
this.valkey = valkey;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract createRoom(roomId: string, options?: RoomOptions): Promise<T>;
|
|
||||||
abstract deleteRoom(roomId: string): Promise<void>;
|
|
||||||
abstract getRoomStatus(roomId: string): Promise<T | null>;
|
|
||||||
abstract listRooms(): Promise<T[]>;
|
|
||||||
abstract cleanup(): Promise<void>;
|
|
||||||
|
|
||||||
protected async updateRoomInfo(roomId: string, info: Partial<RoomInfo>): Promise<boolean> {
|
|
||||||
let room = await this.getRoomInfo(roomId);
|
|
||||||
|
|
||||||
if(!room) return false;
|
|
||||||
|
|
||||||
for(let key in Object.keys(room)){
|
|
||||||
room[key]= info[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.valkey.set(`room:${roomId}`,room as Object as GlideString);
|
|
||||||
return result != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getRoomInfo(roomId: string): Promise<RoomInfo | null> {
|
|
||||||
const info = (await this.valkey.get(`room:${roomId}`)) as RoomInfo | null;
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
110
server/roomsProviders/cluster-provider.js
Normal file
110
server/roomsProviders/cluster-provider.js
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
const cluster = require("node:cluster");
|
||||||
|
const { cpus } = require("node:os");
|
||||||
|
const BaseRoomProvider = require("./base-provider.js");
|
||||||
|
|
||||||
|
class ClusterRoomProvider extends BaseRoomProvider {
|
||||||
|
constructor(config = {}, roomRepository) {
|
||||||
|
super(config, roomRepository);
|
||||||
|
this.workers = new Map();
|
||||||
|
|
||||||
|
if (cluster.isPrimary) {
|
||||||
|
this.initializeCluster();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeCluster() {
|
||||||
|
const numCPUs = cpus().length;
|
||||||
|
|
||||||
|
for (let i = 0; i < numCPUs; i++) {
|
||||||
|
const worker = cluster.fork();
|
||||||
|
this.handleWorkerMessages(worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster.on("exit", (worker, code, signal) => {
|
||||||
|
console.log(`Worker ${worker.process.pid} died. Starting new worker...`);
|
||||||
|
const newWorker = cluster.fork();
|
||||||
|
this.handleWorkerMessages(newWorker);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWorkerMessages(worker) {
|
||||||
|
worker.on("message", async (msg) => {
|
||||||
|
if (msg.type === "room_status") {
|
||||||
|
await this.updateRoomInfo(msg.roomId, {
|
||||||
|
status: msg.status,
|
||||||
|
workerId: worker.id,
|
||||||
|
lastUpdate: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRoom(roomId, options = {}) {
|
||||||
|
const workerLoads = Array.from(this.workers.entries())
|
||||||
|
.map(([id, data]) => ({
|
||||||
|
id,
|
||||||
|
rooms: data.rooms.size,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.rooms - b.rooms);
|
||||||
|
|
||||||
|
const workerId = workerLoads[0].id;
|
||||||
|
const worker = cluster.workers[workerId];
|
||||||
|
|
||||||
|
if (!worker) {
|
||||||
|
throw new Error("No available workers");
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.send({ type: "create_room", roomId, options });
|
||||||
|
|
||||||
|
const roomInfo = {
|
||||||
|
roomId,
|
||||||
|
provider: "cluster",
|
||||||
|
status: "creating",
|
||||||
|
workerId,
|
||||||
|
pid: worker.process.pid,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.updateRoomInfo(roomId, roomInfo);
|
||||||
|
return roomInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRoom(roomId) {
|
||||||
|
const roomInfo = await this.getRoomInfo(roomId);
|
||||||
|
if (roomInfo?.workerId && cluster.workers[roomInfo.workerId]) {
|
||||||
|
cluster.workers[roomInfo.workerId].send({
|
||||||
|
type: "delete_room",
|
||||||
|
roomId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//await this.valkey.del(["room", roomId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRoomStatus(roomId) {
|
||||||
|
return await this.getRoomInfo(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listRooms() {
|
||||||
|
let rooms = [];
|
||||||
|
/*
|
||||||
|
const keys = await this.valkey.hkeys("room:*");
|
||||||
|
const rooms = await Promise.all(
|
||||||
|
keys.map((key) => this.getRoomInfo(key.split(":")[1]))
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
return rooms.filter((room) => room !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup() {
|
||||||
|
const rooms = await this.listRooms();
|
||||||
|
const staleTimeout = 30000;
|
||||||
|
|
||||||
|
for (const room of rooms) {
|
||||||
|
if (Date.now() - (room.lastUpdate || room.createdAt) > staleTimeout) {
|
||||||
|
await this.deleteRoom(room.roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ClusterRoomProvider;
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
import * as cluster from 'node:cluster';
|
|
||||||
import { cpus } from 'node:os';
|
|
||||||
import { GlideClient } from "@valkey/valkey-glide";
|
|
||||||
import {
|
|
||||||
ClusterRoomInfo,
|
|
||||||
RoomOptions,
|
|
||||||
ClusterProviderConfig
|
|
||||||
} from '../../types/room';
|
|
||||||
import { BaseRoomProvider } from './base-provider';
|
|
||||||
|
|
||||||
export class ClusterRoomProvider extends BaseRoomProvider<ClusterRoomInfo> {
|
|
||||||
private workers: Map<number, { rooms: Set<string> }>;
|
|
||||||
|
|
||||||
constructor(valkey: GlideClient, config: ClusterProviderConfig = {}) {
|
|
||||||
super(valkey, config);
|
|
||||||
this.workers = new Map();
|
|
||||||
|
|
||||||
if (cluster.default.isPrimary) {
|
|
||||||
this.initializeCluster();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private initializeCluster(): void {
|
|
||||||
const numCPUs = cpus().length;
|
|
||||||
|
|
||||||
for (let i = 0; i < numCPUs; i++) {
|
|
||||||
const worker = cluster.default.fork();
|
|
||||||
this.handleWorkerMessages(worker);
|
|
||||||
}
|
|
||||||
|
|
||||||
cluster.default.on('exit', (worker, code, signal) => {
|
|
||||||
console.log(`Worker ${worker.process.pid} died. Starting new worker...`);
|
|
||||||
const newWorker = cluster.default.fork();
|
|
||||||
this.handleWorkerMessages(newWorker);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleWorkerMessages(worker: cluster.Worker): void {
|
|
||||||
worker.on('message', async (msg: {
|
|
||||||
type: string;
|
|
||||||
roomId: string;
|
|
||||||
status: string;
|
|
||||||
}) => {
|
|
||||||
if (msg.type === 'room_status') {
|
|
||||||
await this.updateRoomInfo(msg.roomId, {
|
|
||||||
status: msg.status as any,
|
|
||||||
workerId: worker.id,
|
|
||||||
lastUpdate: Date.now()
|
|
||||||
} as Partial<ClusterRoomInfo>);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async createRoom(roomId: string, options: RoomOptions = {}): Promise<ClusterRoomInfo> {
|
|
||||||
const workerLoads = Array.from(this.workers.entries())
|
|
||||||
.map(([id, data]) => ({
|
|
||||||
id,
|
|
||||||
rooms: data.rooms.size
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.rooms - b.rooms);
|
|
||||||
|
|
||||||
const workerId = workerLoads[0].id;
|
|
||||||
const worker = cluster.default.workers?.[workerId];
|
|
||||||
|
|
||||||
if (!worker) {
|
|
||||||
throw new Error('No available workers');
|
|
||||||
}
|
|
||||||
|
|
||||||
worker.send({ type: 'create_room', roomId, options });
|
|
||||||
|
|
||||||
const roomInfo: ClusterRoomInfo = {
|
|
||||||
roomId,
|
|
||||||
provider: 'cluster',
|
|
||||||
status: 'creating',
|
|
||||||
workerId,
|
|
||||||
pid: worker.process.pid!,
|
|
||||||
createdAt: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.updateRoomInfo(roomId, roomInfo);
|
|
||||||
return roomInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteRoom(roomId: string): Promise<void> {
|
|
||||||
const roomInfo = await this.getRoomInfo(roomId) as ClusterRoomInfo;
|
|
||||||
if (roomInfo?.workerId && cluster.Worker?.[roomInfo.workerId]) {
|
|
||||||
cluster.Worker[roomInfo.workerId].send({
|
|
||||||
type: 'delete_room',
|
|
||||||
roomId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await this.valkey.del(['room',roomId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRoomStatus(roomId: string): Promise<ClusterRoomInfo | null> {
|
|
||||||
return await this.getRoomInfo(roomId) as ClusterRoomInfo | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async listRooms(): Promise<ClusterRoomInfo[]> {
|
|
||||||
const keys = await this.valkey.hkeys('room:*');
|
|
||||||
const rooms = await Promise.all(
|
|
||||||
keys.map(key => this.getRoomInfo(key.toString().split(':')[1]))
|
|
||||||
);
|
|
||||||
return rooms.filter((room): room is ClusterRoomInfo => room !== null);
|
|
||||||
}
|
|
||||||
|
|
||||||
async cleanup(): Promise<void> {
|
|
||||||
const rooms = await this.listRooms();
|
|
||||||
const staleTimeout = 30000; // 30 seconds
|
|
||||||
|
|
||||||
for (const room of rooms) {
|
|
||||||
if (Date.now() - (room.lastUpdate || room.createdAt) > staleTimeout) {
|
|
||||||
await this.deleteRoom(room.roomId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
230
server/roomsProviders/docker-provider.js
Normal file
230
server/roomsProviders/docker-provider.js
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
const Docker = require("dockerode");
|
||||||
|
const { Room } = require("../models/room.js");
|
||||||
|
const BaseRoomProvider = require("./base-provider.js");
|
||||||
|
|
||||||
|
class DockerRoomProvider extends BaseRoomProvider {
|
||||||
|
constructor(config, roomRepository) {
|
||||||
|
super(config, roomRepository);
|
||||||
|
const dockerSocket = process.env.DOCKER_SOCKET || "/var/run/docker.sock";
|
||||||
|
|
||||||
|
this.docker = new Docker({ socketPath: dockerSocket });
|
||||||
|
this.docker_network = 'evaluetonsavoir_quiz_network';
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncInstantiatedRooms() {
|
||||||
|
let containers = await this.docker.listContainers();
|
||||||
|
containers = containers.filter(container => container.Image === this.quiz_docker_image);
|
||||||
|
|
||||||
|
const containerIds = new Set(containers.map(container => container.Id));
|
||||||
|
|
||||||
|
for (let container of containers) {
|
||||||
|
const container_name = container.Names[0].slice(1);
|
||||||
|
if (!container_name.startsWith("room_")) {
|
||||||
|
console.warn(`Le conteneur ${container_name} ne suit pas la convention de nommage, il sera supprimé.`);
|
||||||
|
const curContainer = this.docker.getContainer(container.Id);
|
||||||
|
await curContainer.stop();
|
||||||
|
await curContainer.remove();
|
||||||
|
containerIds.delete(container.Id);
|
||||||
|
console.warn(`Le conteneur ${container_name} a été supprimé.`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.warn(`Conteneur orphelin trouvé : ${container_name}`);
|
||||||
|
const roomId = container_name.slice(5);
|
||||||
|
const room = await this.roomRepository.get(roomId);
|
||||||
|
|
||||||
|
if (!room) {
|
||||||
|
console.warn(`Le conteneur n'est pas dans notre base de données.`);
|
||||||
|
const containerInfo = await this.docker.getContainer(container.Id).inspect();
|
||||||
|
const containerIP = containerInfo.NetworkSettings.Networks.evaluetonsavoir_quiz_network.IPAddress;
|
||||||
|
const host = `${containerIP}:4500`;
|
||||||
|
console.warn(`Création de la salle ${roomId} dans notre base de donnée - hôte : ${host}`);
|
||||||
|
return await this.roomRepository.create(new Room(roomId, container_name, host));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(`La salle ${roomId} est déjà dans notre base de données.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRoom(roomId, options) {
|
||||||
|
const container_name = `room_${roomId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const containerConfig = {
|
||||||
|
Image: this.quiz_docker_image,
|
||||||
|
name: container_name,
|
||||||
|
HostConfig: {
|
||||||
|
NetworkMode: this.docker_network,
|
||||||
|
RestartPolicy: {
|
||||||
|
Name: 'unless-stopped'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Env: [
|
||||||
|
`ROOM_ID=${roomId}`,
|
||||||
|
`PORT=${this.quiz_docker_port}`,
|
||||||
|
...(options.env || [])
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.quiz_expose_port) {
|
||||||
|
containerConfig.ExposedPorts = {
|
||||||
|
[`${this.quiz_docker_port}/tcp`]: {}
|
||||||
|
};
|
||||||
|
containerConfig.HostConfig.PortBindings = {
|
||||||
|
[`${this.quiz_docker_port}/tcp`]: [{ HostPort: '' }] // Empty string for random port
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = await this.docker.createContainer(containerConfig);
|
||||||
|
await container.start();
|
||||||
|
|
||||||
|
const containerInfo = await container.inspect();
|
||||||
|
const networkInfo = containerInfo.NetworkSettings.Networks[this.docker_network];
|
||||||
|
|
||||||
|
if (!networkInfo) {
|
||||||
|
throw new Error(`Le conteneur n'as pu se connecter au réseau: ${this.docker_network}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerIP = networkInfo.IPAddress;
|
||||||
|
const host = `http://${containerIP}:${this.quiz_docker_port}`;
|
||||||
|
|
||||||
|
|
||||||
|
let health = false;
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 15;
|
||||||
|
|
||||||
|
while (!health && attempts < maxAttempts) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${host}/health`, {
|
||||||
|
timeout: 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
health = true;
|
||||||
|
console.log(`Le conteneur ${container_name} est tombé actif en ${attempts + 1} tentatives`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Health check failed with status ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
attempts++;
|
||||||
|
console.log(`Attente du conteneur: ${container_name} (tentative ${attempts}/${maxAttempts})`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!health) {
|
||||||
|
console.error(`Container ${container_name} failed health check after ${maxAttempts} attempts`);
|
||||||
|
await container.stop();
|
||||||
|
await container.remove();
|
||||||
|
throw new Error(`Room ${roomId} did not respond within acceptable timeout`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.roomRepository.create(new Room(roomId, container_name, host, 0));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Échec de la création de la salle ${roomId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRoom(roomId) {
|
||||||
|
const container_name = `room_${roomId}`;
|
||||||
|
await this.roomRepository.delete(roomId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const container = this.docker.getContainer(container_name);
|
||||||
|
const containerInfo = await container.inspect();
|
||||||
|
|
||||||
|
if (containerInfo) {
|
||||||
|
await container.stop();
|
||||||
|
await container.remove();
|
||||||
|
console.log(`Le conteneur pour la salle ${roomId} a été arrêté et supprimé.`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.statusCode === 404) {
|
||||||
|
console.warn(`Le conteneur pour la salle ${roomId} n'as pas été trouvé, la salle sera supprimée de la base de données.`);
|
||||||
|
} else {
|
||||||
|
console.error(`Erreur pour la salle ${roomId}:`, error);
|
||||||
|
throw new Error("La salle :${roomId} n'as pas pu être supprimée.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`La salle ${roomId} a été supprimée.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRoomStatus(roomId) {
|
||||||
|
const room = await this.roomRepository.get(roomId);
|
||||||
|
if (!room) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const container = this.docker.getContainer(room.containerId || `room_${roomId}`);
|
||||||
|
const info = await container.inspect();
|
||||||
|
|
||||||
|
const updatedRoomInfo = {
|
||||||
|
...room,
|
||||||
|
status: info.State.Running ? "running" : "terminated",
|
||||||
|
containerStatus: {
|
||||||
|
Running: info.State.Running,
|
||||||
|
StartedAt: info.State.StartedAt,
|
||||||
|
FinishedAt: info.State.FinishedAt,
|
||||||
|
},
|
||||||
|
lastUpdate: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.roomRepository.update(updatedRoomInfo);
|
||||||
|
return updatedRoomInfo;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.statusCode === 404) {
|
||||||
|
console.warn(`Le conteneur pour la salle ${roomId} n'as pas été trouvé, il sera mis en état "terminé".`);
|
||||||
|
const terminatedRoomInfo = {
|
||||||
|
...room,
|
||||||
|
status: "terminated",
|
||||||
|
containerStatus: {
|
||||||
|
Running: false,
|
||||||
|
StartedAt: room.containerStatus?.StartedAt || null,
|
||||||
|
FinishedAt: Date.now(),
|
||||||
|
},
|
||||||
|
lastUpdate: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.roomRepository.update(terminatedRoomInfo);
|
||||||
|
return terminatedRoomInfo;
|
||||||
|
} else {
|
||||||
|
console.error(`Une érreur s'est produite lors de l'obtention de l'état de la salle ${roomId}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listRooms() {
|
||||||
|
const rooms = await this.roomRepository.getAll();
|
||||||
|
return rooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup() {
|
||||||
|
const rooms = await this.roomRepository.getAll();
|
||||||
|
for (let room of rooms) {
|
||||||
|
if (room.mustBeCleaned) {
|
||||||
|
try {
|
||||||
|
await this.deleteRoom(room.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Érreur lors du néttoyage de la salle ${room.id}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let containers = await this.docker.listContainers();
|
||||||
|
containers = containers.filter(container => container.Image === this.quiz_docker_image);
|
||||||
|
const roomIds = rooms.map(room => room.id);
|
||||||
|
|
||||||
|
for (let container of containers) {
|
||||||
|
if (!roomIds.includes(container.Names[0].slice(6))) {
|
||||||
|
const curContainer = this.docker.getContainer(container.Id);
|
||||||
|
await curContainer.stop();
|
||||||
|
await curContainer.remove();
|
||||||
|
console.warn(`Conteneur orphelin ${container.Names[0]} supprimé.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DockerRoomProvider;
|
||||||
54
server/routers/rooms.js
Normal file
54
server/routers/rooms.js
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
const { Router } = require("express");
|
||||||
|
const roomsController = require('../app.js').rooms;
|
||||||
|
const jwt = require('../middleware/jwtToken.js');
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/",jwt.authenticate, async (req, res)=> {
|
||||||
|
try {
|
||||||
|
const data = await roomsController.listRooms();
|
||||||
|
res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Échec de listage des salle" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
router.post("/",jwt.authenticate, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = await roomsController.createRoom();
|
||||||
|
res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).json({ error: "Échec de la création de salle :" + error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put("/:id",jwt.authenticate, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = await roomsController.updateRoom(req.params.id);
|
||||||
|
res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Échec de la mise a jour de salle : "+error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete("/:id",jwt.authenticate, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = await roomsController.deleteRoom(req.params.id);
|
||||||
|
res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: `Échec de suppression de la salle: `+error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/:id", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = await roomsController.getRoomStatus(req.params.id);
|
||||||
|
res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Impossible d'afficher les informations de la salle: " + error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
import {express} from 'express'
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
const MAX_USERS_PER_ROOM = 60;
|
|
||||||
const MAX_TOTAL_CONNECTIONS = 2000;
|
|
||||||
|
|
||||||
const setupWebsocket = (io) => {
|
|
||||||
let totalConnections = 0;
|
|
||||||
|
|
||||||
io.on("connection", (socket) => {
|
|
||||||
if (totalConnections >= MAX_TOTAL_CONNECTIONS) {
|
|
||||||
console.log("Connection limit reached. Disconnecting client.");
|
|
||||||
socket.emit(
|
|
||||||
"join-failure",
|
|
||||||
"Le nombre maximum de connexions a été atteint"
|
|
||||||
);
|
|
||||||
socket.disconnect(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalConnections++;
|
|
||||||
// console.log(
|
|
||||||
// "A user connected:",
|
|
||||||
// socket.id,
|
|
||||||
// "| Total connections:",
|
|
||||||
// totalConnections
|
|
||||||
// );
|
|
||||||
|
|
||||||
socket.on("create-room", (sentRoomName) => {
|
|
||||||
if (sentRoomName) {
|
|
||||||
const roomName = sentRoomName.toUpperCase();
|
|
||||||
if (!io.sockets.adapter.rooms.get(roomName)) {
|
|
||||||
socket.join(roomName);
|
|
||||||
socket.emit("create-success", roomName);
|
|
||||||
} else {
|
|
||||||
socket.emit("create-failure");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const roomName = generateRoomName();
|
|
||||||
if (!io.sockets.adapter.rooms.get(roomName)) {
|
|
||||||
socket.join(roomName);
|
|
||||||
socket.emit("create-success", roomName);
|
|
||||||
} else {
|
|
||||||
socket.emit("create-failure");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("join-room", ({ enteredRoomName, username }) => {
|
|
||||||
if (io.sockets.adapter.rooms.has(enteredRoomName)) {
|
|
||||||
const clientsInRoom =
|
|
||||||
io.sockets.adapter.rooms.get(enteredRoomName).size;
|
|
||||||
|
|
||||||
if (clientsInRoom <= MAX_USERS_PER_ROOM) {
|
|
||||||
const newStudent = {
|
|
||||||
id: socket.id,
|
|
||||||
name: username,
|
|
||||||
answers: [],
|
|
||||||
};
|
|
||||||
socket.join(enteredRoomName);
|
|
||||||
socket
|
|
||||||
.to(enteredRoomName)
|
|
||||||
.emit("user-joined", newStudent);
|
|
||||||
socket.emit("join-success");
|
|
||||||
} else {
|
|
||||||
socket.emit("join-failure", "La salle est remplie");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
socket.emit("join-failure", "Le nom de la salle n'existe pas");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("next-question", ({ roomName, question }) => {
|
|
||||||
// console.log("next-question", roomName, question);
|
|
||||||
socket.to(roomName).emit("next-question", question);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("launch-student-mode", ({ roomName, questions }) => {
|
|
||||||
socket.to(roomName).emit("launch-student-mode", questions);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("end-quiz", ({ roomName }) => {
|
|
||||||
socket.to(roomName).emit("end-quiz");
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("message", (data) => {
|
|
||||||
console.log("Received message from", socket.id, ":", data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
|
||||||
totalConnections--;
|
|
||||||
console.log(
|
|
||||||
"A user disconnected:",
|
|
||||||
socket.id,
|
|
||||||
"| Total connections:",
|
|
||||||
totalConnections
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const [room] of io.sockets.adapter.rooms) {
|
|
||||||
if (room !== socket.id) {
|
|
||||||
io.to(room).emit("user-disconnected", socket.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("submit-answer", ({ roomName, username, answer, idQuestion }) => {
|
|
||||||
socket.to(roomName).emit("submit-answer-room", {
|
|
||||||
idUser: socket.id,
|
|
||||||
username,
|
|
||||||
answer,
|
|
||||||
idQuestion,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const generateRoomName = (length = 6) => {
|
|
||||||
const characters = "0123456789";
|
|
||||||
let result = "";
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
result += characters.charAt(
|
|
||||||
Math.floor(Math.random() * characters.length)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = { setupWebsocket };
|
|
||||||
Loading…
Reference in a new issue