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 }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
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
|
||||
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",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"axios": "^1.6.7",
|
||||
"dockerode": "^4.0.2",
|
||||
"esbuild": "^0.23.1",
|
||||
"gift-pegjs": "^1.0.2",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
|
|
@ -1897,6 +1898,12 @@
|
|||
"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": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
||||
|
|
@ -5190,6 +5197,15 @@
|
|||
"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": {
|
||||
"version": "3.2.6",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
|
|
@ -5469,6 +5514,17 @@
|
|||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
|
|
@ -5542,12 +5598,45 @@
|
|||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
|
|
@ -5658,6 +5747,12 @@
|
|||
"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": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
|
|
@ -5786,6 +5881,20 @@
|
|||
"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": {
|
||||
"version": "29.7.0",
|
||||
"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_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": {
|
||||
"version": "0.5.16",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
|
||||
|
|
@ -6797,6 +6944,12 @@
|
|||
"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": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
|
||||
|
|
@ -7078,6 +7231,26 @@
|
|||
"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": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
|
|
@ -7153,8 +7326,7 @@
|
|||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
|
|
@ -10208,11 +10380,24 @@
|
|||
"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": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"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": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz",
|
||||
|
|
@ -10284,7 +10469,6 @@
|
|||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
|
|
@ -10661,6 +10845,16 @@
|
|||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
|
@ -10805,6 +10999,20 @@
|
|||
"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": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
|
|
@ -11047,6 +11255,26 @@
|
|||
"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": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
|
|
@ -11182,12 +11410,35 @@
|
|||
"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": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"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": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
|
|
@ -11207,6 +11458,15 @@
|
|||
"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": {
|
||||
"version": "4.0.2",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "6.0.0",
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
|
@ -11765,6 +12059,12 @@
|
|||
"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": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
|
|
@ -12637,8 +12937,7 @@
|
|||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
},
|
||||
"node_modules/write-file-atomic": {
|
||||
"version": "4.0.2",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
"@mui/material": "^6.1.0",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"axios": "^1.6.7",
|
||||
"dockerode": "^4.0.2",
|
||||
"esbuild": "^0.23.1",
|
||||
"gift-pegjs": "^1.0.2",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ describe('WebSocketService', () => {
|
|||
|
||||
test('createRoom should emit create-room event', () => {
|
||||
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
WebsocketService.createRoom();
|
||||
WebsocketService.createRoom('test');
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('create-room');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { ENV_VARIABLES } from '../../../constants';
|
||||
//import { ENV_VARIABLES } from '../../../constants';
|
||||
|
||||
import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz';
|
||||
import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz';
|
||||
|
|
@ -27,14 +27,14 @@ const JoinRoom: React.FC = () => {
|
|||
const [isConnecting, setIsConnecting] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
handleCreateSocket();
|
||||
//handleCreateSocket();
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCreateSocket = () => {
|
||||
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
const socket = webSocketService.connect(`/api/room/${roomName}/socket`);
|
||||
|
||||
socket.on('join-success', () => {
|
||||
setIsWaitingForTeacher(true);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import webSocketService, { AnswerReceptionFromBackendType } from '../../../servi
|
|||
import { QuizType } from '../../../Types/QuizType';
|
||||
|
||||
import './manageRoom.css';
|
||||
import { ENV_VARIABLES } from '../../../constants';
|
||||
//import { ENV_VARIABLES } from '../../../constants';
|
||||
import { StudentType, Answer } from '../../../Types/StudentType';
|
||||
import { Button } from '@mui/material';
|
||||
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
|
||||
|
|
@ -79,13 +79,19 @@ const ManageRoom: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const createWebSocketRoom = () => {
|
||||
const createWebSocketRoom = async () => {
|
||||
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', () => {
|
||||
webSocketService.createRoom();
|
||||
webSocketService.createRoom(room.id);
|
||||
});
|
||||
|
||||
socket.on("error", (error) => {
|
||||
console.error("WebSocket server error:", error);
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
|
||||
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).');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Update the students state using the functional form of setStudents
|
||||
setStudents((prevStudents) => {
|
||||
// print the list of current student names
|
||||
|
|
@ -150,7 +156,7 @@ const ManageRoom: React.FC = () => {
|
|||
prevStudents.forEach((student) => {
|
||||
console.log(student.name);
|
||||
});
|
||||
|
||||
|
||||
let foundStudent = false;
|
||||
const updatedStudents = prevStudents.map((student) => {
|
||||
console.log(`Comparing ${student.id} to ${idUser}`);
|
||||
|
|
@ -170,7 +176,7 @@ const ManageRoom: React.FC = () => {
|
|||
updatedAnswers = [...student.answers, newAnswer];
|
||||
}
|
||||
return { ...student, answers: updatedAnswers };
|
||||
}
|
||||
}
|
||||
return student;
|
||||
});
|
||||
if (!foundStudent) {
|
||||
|
|
|
|||
|
|
@ -80,6 +80,78 @@ class ApiService {
|
|||
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
|
||||
|
||||
/**
|
||||
|
|
@ -302,6 +374,7 @@ class ApiService {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @returns folder array if successful
|
||||
* @returns A error string if unsuccessful,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// WebSocketService.tsx
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import apiService from './ApiService';
|
||||
|
||||
// Must (manually) sync these types to server/socket/socket.js
|
||||
|
||||
|
|
@ -21,10 +22,14 @@ class WebSocketService {
|
|||
private socket: Socket | null = null;
|
||||
|
||||
connect(backendUrl: string): Socket {
|
||||
// console.log(backendUrl);
|
||||
this.socket = io(`${backendUrl}`, {
|
||||
this.socket = io( '/',{
|
||||
path: backendUrl,
|
||||
transports: ['websocket'],
|
||||
reconnectionAttempts: 1
|
||||
autoConnect: true,
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 10,
|
||||
reconnectionDelay: 10000,
|
||||
timeout: 20000,
|
||||
});
|
||||
return this.socket;
|
||||
}
|
||||
|
|
@ -37,9 +42,9 @@ class WebSocketService {
|
|||
}
|
||||
}
|
||||
|
||||
createRoom() {
|
||||
createRoom(roomName: string) {
|
||||
if (this.socket) {
|
||||
this.socket.emit('create-room');
|
||||
this.socket.emit('create-room', roomName || undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +63,8 @@ class WebSocketService {
|
|||
endQuiz(roomName: string) {
|
||||
if (this.socket) {
|
||||
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
|
||||
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
|
||||
nginx:
|
||||
image: fuhrmanator/evaluetonsavoir-routeur:latest
|
||||
|
|
@ -49,13 +60,6 @@ services:
|
|||
- mongodb_data:/data/db
|
||||
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
|
||||
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
|
||||
FROM node:18
|
||||
FROM node:18 AS quizroom
|
||||
|
||||
ARG PORT=4500
|
||||
ENV PORT=${PORT}
|
||||
ENV ROOM_ID=${ROOM_ID}
|
||||
|
||||
# Create a working directory
|
||||
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 ./
|
||||
RUN npm install
|
||||
|
||||
# Copy all source code to the container
|
||||
# Copy the rest of the source code to the container
|
||||
COPY . .
|
||||
|
||||
# Expose WebSocket server port
|
||||
EXPOSE 4500
|
||||
# Build the TypeScript code
|
||||
RUN npm run build
|
||||
|
||||
# Start the WebSocket server
|
||||
CMD ["node", "app.js"]
|
||||
# Expose WebSocket server port
|
||||
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 { Server, ServerOptions } from "socket.io";
|
||||
|
||||
// Import 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> = {
|
||||
path: "/socket.io",
|
||||
path: `/api/room/${roomId}/socket`,
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"],
|
||||
|
|
@ -24,4 +52,4 @@ setupWebsocket(io);
|
|||
|
||||
server.listen(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",
|
||||
"main": "app.js",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"test": "jest --colors"
|
||||
"start": "node dist/app.js",
|
||||
"build": "tsc",
|
||||
"dev": "ts-node app.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"socket.io": "^4.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0",
|
||||
"jest-mock": "^29.7.0"
|
||||
"@types/express": "^5.0.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++;
|
||||
console.log("A user connected:", socket.id, "| Total connections:", totalConnections);
|
||||
|
||||
socket.on("create-room", (sentRoomName?: string) => {
|
||||
const roomName = sentRoomName ? sentRoomName.toUpperCase() : generateRoomName();
|
||||
socket.on("create-room", (sentRoomName) => {
|
||||
// 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)) {
|
||||
socket.join(roomName);
|
||||
socket.emit("create-success", roomName);
|
||||
|
|
@ -88,8 +92,14 @@ export const setupWebsocket = (io: Server): void => {
|
|||
idQuestion,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("error", (error) => {
|
||||
console.error("WebSocket server error:", error);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
const generateRoomName = (length = 6): string => {
|
||||
const characters = "0123456789";
|
||||
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 dotenv = require('dotenv');
|
||||
|
||||
// Import Sockets
|
||||
const { setupWebsocket } = require("./socket/socket");
|
||||
const { Server } = require("socket.io");
|
||||
|
||||
// instantiate the db
|
||||
const db = require('./config/db.js');
|
||||
// instantiate the models
|
||||
|
|
@ -18,6 +14,13 @@ const users = require('./models/users.js');
|
|||
const userModel = new users(db, foldersModel);
|
||||
const images = require('./models/images.js');
|
||||
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
|
||||
const usersController = require('./controllers/users.js');
|
||||
|
|
@ -28,18 +31,22 @@ const quizController = require('./controllers/quiz.js');
|
|||
const quizControllerInstance = new quizController(quizModel, foldersModel);
|
||||
const imagesController = require('./controllers/images.js');
|
||||
const imagesControllerInstance = new imagesController(imageModel);
|
||||
const roomsController = require('./controllers/rooms.js');
|
||||
const roomsControllerInstance = new roomsController(QuizProviderOptions,roomRepModel);
|
||||
|
||||
// export the controllers
|
||||
module.exports.users = usersControllerInstance;
|
||||
module.exports.folders = foldersControllerInstance;
|
||||
module.exports.quizzes = quizControllerInstance;
|
||||
module.exports.images = imagesControllerInstance;
|
||||
module.exports.rooms = roomsControllerInstance;
|
||||
|
||||
//import routers (instantiate controllers as side effect)
|
||||
const userRouter = require('./routers/users.js');
|
||||
const folderRouter = require('./routers/folders.js');
|
||||
const quizRouter = require('./routers/quiz.js');
|
||||
const imagesRouter = require('./routers/images.js');
|
||||
const roomRouter = require('./routers/rooms.js');
|
||||
|
||||
// Setup environment
|
||||
dotenv.config();
|
||||
|
|
@ -50,27 +57,10 @@ const app = express();
|
|||
const cors = require("cors");
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
const configureServer = (httpServer, isDev) => {
|
||||
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 server = http.createServer(app);
|
||||
let isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
console.log(`Environnement: ${process.env.NODE_ENV} (${isDev ? 'dev' : 'prod'})`);
|
||||
|
||||
const io = configureServer(server);
|
||||
|
||||
setupWebsocket(io);
|
||||
app.use(cors());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(bodyParser.json());
|
||||
|
|
@ -80,6 +70,7 @@ app.use('/api/user', userRouter);
|
|||
app.use('/api/folder', folderRouter);
|
||||
app.use('/api/quiz', quizRouter);
|
||||
app.use('/api/image', imagesRouter);
|
||||
app.use('/api/room', roomRouter);
|
||||
|
||||
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) {
|
||||
throw new AppError(UNAUTHORIZED_INVALID_TOKEN)
|
||||
}
|
||||
|
||||
|
||||
req.user = payload;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class Quiz {
|
|||
await this.db.connect()
|
||||
const conn = this.db.getConnection();
|
||||
|
||||
|
||||
const quizCollection = conn.collection('files');
|
||||
|
||||
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",
|
||||
"dependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"@valkey/valkey-glide": "^1.1.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "^4.0.2",
|
||||
"dotenv": "^16.4.4",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongodb": "^6.3.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"net": "^1.0.2",
|
||||
"nodemailer": "^6.9.9",
|
||||
"socket.io": "^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