diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5ae50d6..02aae8da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: 20 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4ce47e3e..da48ef08 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -163,4 +163,4 @@ jobs: wget https://raw.githubusercontent.com/arquisoft/wiq_es2b/master/docker-compose.yml -O docker-compose.yml wget https://raw.githubusercontent.com/arquisoft/wiq_es2b/master/.env -O .env docker compose --profile prod down - docker compose --profile prod up -d --pull always + docker compose --profile prod up -d --pull always \ No newline at end of file diff --git a/README.md b/README.md index dfea3335..924783d7 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,12 @@ and launch it with docker compose: docker compose --profile dev up --build ``` +and tear it down: + +```sh +docker compose --profile dev down +``` + ### Starting Component by component First, start the database. Either install and run Mongo or run it using docker: @@ -100,10 +106,12 @@ deploy: user: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_KEY }} command: | - wget https://raw.githubusercontent.com/arquisoft/wiq_es2b/master/docker-compose.yml -O docker-compose.yml - wget https://raw.githubusercontent.com/arquisoft/wiq_es2b/master/.env -O .env - docker compose down - docker compose --profile prod up -d + + wget https://raw.githubusercontent.com/arquisoft/wiq_0/master/docker-compose.yml -O docker-compose.yml + wget https://raw.githubusercontent.com/arquisoft/wiq_0/master/.env -O .env + docker compose --profile prod down + docker compose --profile prod up -d --pull always + ``` This action uses three secrets that must be configured in the repository: @@ -111,4 +119,5 @@ This action uses three secrets that must be configured in the repository: - DEPLOY_USER: user with permission to execute the commands in the remote machine. - DEPLOY_KEY: key to authenticate the user in the remote machine. -Note that this action logs in the remote machine and downloads the docker-compose file from the repository and launches it. Obviously, previous actions have been executed which have uploaded the docker images to the GitHub Packages repository. +Note that this action logs in the remote machine and downloads the docker-compose file from the repository and launches it. +Obviously, previous actions have been executed which have uploaded the docker images to the GitHub Packages repository. diff --git a/docs/src/06_runtime_view.adoc b/docs/src/06_runtime_view.adoc index e10f375b..c9e002f6 100644 --- a/docs/src/06_runtime_view.adoc +++ b/docs/src/06_runtime_view.adoc @@ -3,63 +3,27 @@ ifndef::imagesdir[:imagesdir: ../images] [[section-runtime-view]] == Runtime View - -[role="arc42help"] -**** -.Contents -The runtime view describes concrete behavior and interactions of the system’s building blocks in form of scenarios from the following areas: - -* important use cases or features: how do building blocks execute them? -* interactions at critical external interfaces: how do building blocks cooperate with users and neighboring systems? -* operation and administration: launch, start-up, stop -* error and exception scenarios - -Remark: The main criterion for the choice of possible scenarios (sequences, workflows) is their *architectural relevance*. It is *not* important to describe a large number of scenarios. You should rather document a representative selection. - -.Motivation -You should understand how (instances of) building blocks of your system perform their job and communicate at runtime. -You will mainly capture scenarios in your documentation to communicate your architecture to stakeholders that are less willing or able to read and understand the static models (building block view, deployment view). - -.Form -There are many notations for describing scenarios, e.g. - -* numbered list of steps (in natural language) -* activity diagrams or flow charts -* sequence diagrams -* BPMN or EPCs (event process chains) -* state machines -* ... - - -.Further Information - -See https://docs.arc42.org/section-6/[Runtime View] in the arc42 documentation. - -**** - -=== - - -* __ -* __ - -It is possible to use a sequence diagram: - -[plantuml,"Sequence diagram",png] +=== User plays a game +When the game is started, the app will call the createquestion service that is in charge of provide generated questions from wikidata information. +[plantuml,"Start a game",png] ---- -actor Alice -actor Bob -database Pod as "Bob's Pod" -Alice -> Bob: Authentication Request -Bob --> Alice: Authentication Response -Alice --> Pod: Store route -Alice -> Bob: Another authentication Request -Alice <-- Bob: another authentication Response +actor a as "User" +participant q as "Game GUI" +participant w as "CreateQuestions service" +database d as "Database" + + +a -> q: Start the game +loop number of questions +q -> w: Ask for a question +w -->q: Returns the question +q -> d: Store the question +q -> a: Returns the question + +a -> q: Pick an answer +q -> a: Shows if the answer was valid or not +a -> q: Asks for next question +end +q -> a: Show the game stats +q -> d: Store the game with questions, answers and stats ---- - -=== - -=== ... - -=== diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index a20b94cc..82e9f886 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -2,6 +2,11 @@ const express = require('express'); const axios = require('axios'); const cors = require('cors'); const promBundle = require('express-prom-bundle'); +//libraries required for OpenAPI-Swagger +const swaggerUi = require('swagger-ui-express'); +const fs = require("fs") +const YAML = require('yaml') + const app = express(); const port = 8000; @@ -41,10 +46,33 @@ app.post('/adduser', async (req, res) => { } }); -app.post('/createquestion', async (req, res) => { + +app.post('/addgame', async (req, res) => { + try { + const userResponse = await axios.post(userServiceUrl+'/addgame', req.body); + res.json(userResponse.data); + } catch (error) { + res.status(error.response.status).json({ error: error.response.data.error }); + } +}); + + +app.get('/getgamehistory/:username', async (req, res) => { + try { + const username = req.params.username; + const userResponse = await axios.get(`${userServiceUrl}/getgamehistory/${username}`); + res.json(userResponse.data); + } catch (error) { + res.status(error.response.status).json({ error: error.response.data.error }); + } +}); + + + +app.get('/createquestion', async (req, res) => { try { // Create a petition to the URL (le llegará a creation-service.js) with the option /createquestion and the req.body params - const questionResponse = await axios.post(creationServiceUrl+'/createquestion', req.body); + const questionResponse = await axios.get(creationServiceUrl+'/createquestion', req.body); // Return a json response with what we obtained on the petition res.json(questionResponse.data); } catch (error) { @@ -52,10 +80,10 @@ app.post('/createquestion', async (req, res) => { } }); -app.post('/getquestionshistory', async (req, res) => { +app.get('/getquestionshistory', async (req, res) => { try { // Create a petition to the URL (le llegará a retrieve-service.js) with the option /getgeneratedquestions and the req.body params - const questionResponse = await axios.post(retrieveServiceUrl+'/getquestionshistory', req.body); + const questionResponse = await axios.get(retrieveServiceUrl+'/getquestionshistory', req.body); // Return a json response with what we obtained on the petition res.json(questionResponse.data); } catch (error) { @@ -63,9 +91,29 @@ app.post('/getquestionshistory', async (req, res) => { } }); + + +// Read the OpenAPI YAML file synchronously +openapiPath='./openapi.yaml' +if (fs.existsSync(openapiPath)) { + const file = fs.readFileSync(openapiPath, 'utf8'); + + // Parse the YAML content into a JavaScript object representing the Swagger document + const swaggerDocument = YAML.parse(file); + + // Serve the Swagger UI documentation at the '/api-doc' endpoint + // This middleware serves the Swagger UI files and sets up the Swagger UI page + // It takes the parsed Swagger document as input + app.use('/api-doc', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); +} else { + console.log("Not configuring OpenAPI. Configuration file not present.") +} + + // Start the gateway service const server = app.listen(port, () => { console.log(`Gateway Service listening at http://localhost:${port}`); }); + module.exports = server diff --git a/gatewayservice/openapi.yaml b/gatewayservice/openapi.yaml new file mode 100644 index 00000000..bc411488 --- /dev/null +++ b/gatewayservice/openapi.yaml @@ -0,0 +1,233 @@ +openapi: 3.0.0 +info: + title: Gatewayservice API + description: Gateway OpenAPI specification. + version: 0.2.0 +servers: + - url: http://localhost:8000 + description: Development server + - url: http://SOMEIP:8000 + description: Production server +paths: + /adduser: + post: + summary: Add a new user to the database. + operationId: addUser + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + description: User ID. + example: student + password: + type: string + description: User password. + example: pass + responses: + '200': + description: User added successfully. + content: + application/json: + schema: + type: object + properties: + username: + type: string + description: User ID + password: + type: string + description: Hashed password + example: $2b$10$ZKdNYLWFQxzt5Rei/YTc/OsZNi12YiWz30JeUFHNdAt7MyfmkTuvC + _id: + type: string + description: Identification + example: 65f756db3fa22d227a4b7c7d + createdAt: + type: string + description: Creation date. + example: '2024-03-17T20:47:23.935Z' + ___v: + type: integer + example: '0' + '400': + description: Failed to add user. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error information. + example: getaddrinfo EAI_AGAIN mongodb + /health: + get: + summary: Check the health status of the service. + operationId: checkHealth + responses: + '200': + description: Service is healthy. + content: + application/json: + schema: + type: object + properties: + status: + type: string + description: Health status. + example: OK + /login: + post: + summary: Log in to the system. + operationId: loginUser + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + description: User ID. + example: student + password: + type: string + description: User password. + example: pass + responses: + '200': + description: Login successful. Returns user token, username, and creation date. + content: + application/json: + schema: + type: object + properties: + token: + type: string + description: User token. + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2NWY3NTZkYjNmYTIyZDIyN2E0YjdjN2QiLCJpYXQiOjE3MTA3MDg3NDUsImV4cCI6MTcxMDcxMjM0NX0.VMG_5DOyQ4GYlJQRcu1I6ICG1IGzuo2Xuei093ONHxw + username: + type: string + description: Username. + example: student + createdAt: + type: string + description: Creation date. + example: '2024-03-17T20:47:23.935Z' + '401': + description: Invalid credentials. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Shows the error info.. + example: Invalid credentials + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error information. + example: Internal Server Error + /addgame: + post: + summary: Add a new game to the database. + operationId: addGame + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + gameName: + type: string + description: Name of the game. + example: Game1 + responses: + '200': + description: Game added successfully. + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message indicating success. + example: Game added successfully. + '400': + description: Failed to add game. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error information. + example: Game already exists. + + + /getgamehistory/{username}: + get: + summary: Get game history for a specific user. + operationId: getGameHistory + parameters: + - name: username + in: path + description: Username for which to retrieve game history. + required: true + schema: + type: string + example: student + responses: + '200': + description: Game history retrieved successfully. + content: + application/json: + schema: + type: array + items: + type: object + properties: + gameId: + type: string + description: ID of the game. + example: 12345 + gameName: + type: string + description: Name of the game. + example: Game1 + score: + type: integer + description: Score achieved in the game. + example: 100 + datePlayed: + type: string + description: Date when the game was played. + example: '2024-03-20T15:00:00Z' + '400': + description: Failed to retrieve game history. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error information. + example: User not found. \ No newline at end of file diff --git a/gatewayservice/package-lock.json b/gatewayservice/package-lock.json index fc5f2d60..430cbe99 100644 --- a/gatewayservice/package-lock.json +++ b/gatewayservice/package-lock.json @@ -12,7 +12,10 @@ "axios": "^1.6.5", "cors": "^2.8.5", "express": "^4.18.2", - "express-prom-bundle": "^7.0.0" + "express-openapi": "^12.1.3", + "express-prom-bundle": "^7.0.0", + "swagger-ui-express": "^5.0.0", + "yaml": "^2.4.1" }, "devDependencies": { "jest": "^29.7.0", @@ -1277,6 +1280,37 @@ "node": ">= 0.6" } }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1333,7 +1367,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -1474,8 +1507,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/bintrees": { "version": "1.0.2", @@ -1510,7 +1542,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1751,8 +1782,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/content-disposition": { "version": "0.5.4", @@ -1942,6 +1972,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/difunc": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/difunc/-/difunc-0.0.4.tgz", + "integrity": "sha512-zBiL4ALDmviHdoLC0g0G6wVme5bwAow9WfhcZLLopXCAWgg3AEf7RYTs2xugszIGulRHzEVDF/SHl9oyQU07Pw==", + "dependencies": { + "esprima": "^4.0.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2015,7 +2053,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -2121,6 +2158,21 @@ "node": ">= 0.10.0" } }, + "node_modules/express-normalize-query-params-middleware": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/express-normalize-query-params-middleware/-/express-normalize-query-params-middleware-0.5.1.tgz", + "integrity": "sha512-KUBjEukYL9KJkrphVX3ZgMHgMTdgaSJe+FIOeWwJIJpCw8UZQPIylt0MYddSyUwbms4LQ8RC4wmavcLUP9uduA==" + }, + "node_modules/express-openapi": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/express-openapi/-/express-openapi-12.1.3.tgz", + "integrity": "sha512-F570dVC5ENSkLu1SpDFPRQ13Y3a/7Udh0rfHyn3O1QrE81fPmlhnAo1JRgoNtbMRJ6goHNymxU1TVSllgFZBlQ==", + "dependencies": { + "express-normalize-query-params-middleware": "^0.5.0", + "openapi-framework": "^12.1.3", + "openapi-types": "^12.1.3" + } + }, "node_modules/express-prom-bundle": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-7.0.0.tgz", @@ -2138,6 +2190,11 @@ "prom-client": ">=15.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2264,11 +2321,18 @@ "node": ">= 0.6" } }, + "node_modules/fs-routes": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/fs-routes/-/fs-routes-12.1.3.tgz", + "integrity": "sha512-Vwxi5StpKj/pgH7yRpNpVFdaZr16z71KNTiYuZEYVET+MfZ31Zkf7oxUmNgyZxptG8BolRtdMP90agIhdyiozg==", + "peerDependencies": { + "glob": ">=7.1.6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -2349,7 +2413,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2526,7 +2589,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2563,6 +2625,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-dir/-/is-dir-1.0.0.tgz", + "integrity": "sha512-vLwCNpTNkFC5k7SBRxPubhOCryeulkOsSkjbGyZ8eOzZmzMS+hSEO/Kn9ZOVhFNAlRZTFc4ZKql48hESuYUPIQ==" + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3328,7 +3395,6 @@ "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -3355,6 +3421,11 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3403,6 +3474,11 @@ "node": ">=8" } }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3552,7 +3628,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3643,7 +3718,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" } @@ -3663,6 +3737,97 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-default-setter": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-default-setter/-/openapi-default-setter-12.1.3.tgz", + "integrity": "sha512-wHKwvEuOWwke5WcQn8pyCTXT5WQ+rm9FpJmDeEVECEBWjEyB/MVLYfXi+UQeSHTTu2Tg4VDHHmzbjOqN6hYeLQ==", + "dependencies": { + "openapi-types": "^12.1.3" + } + }, + "node_modules/openapi-framework": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-framework/-/openapi-framework-12.1.3.tgz", + "integrity": "sha512-p30PHWVXda9gGxm+t/1X2XvEcufW1YhzeDQwc5SsgDnBXt8gkuu1SwrioGJ66wxVYEzfSRTTf/FMLhI49ut8fQ==", + "dependencies": { + "difunc": "0.0.4", + "fs-routes": "^12.1.3", + "glob": "*", + "is-dir": "^1.0.0", + "js-yaml": "^3.10.0", + "openapi-default-setter": "^12.1.3", + "openapi-request-coercer": "^12.1.3", + "openapi-request-validator": "^12.1.3", + "openapi-response-validator": "^12.1.3", + "openapi-schema-validator": "^12.1.3", + "openapi-security-handler": "^12.1.3", + "openapi-types": "^12.1.3", + "ts-log": "^2.1.4" + } + }, + "node_modules/openapi-jsonschema-parameters": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-jsonschema-parameters/-/openapi-jsonschema-parameters-12.1.3.tgz", + "integrity": "sha512-aHypKxWHwu2lVqfCIOCZeJA/2NTDiP63aPwuoIC+5ksLK5/IQZ3oKTz7GiaIegz5zFvpMDxDvLR2DMQQSkOAug==", + "dependencies": { + "openapi-types": "^12.1.3" + } + }, + "node_modules/openapi-request-coercer": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-request-coercer/-/openapi-request-coercer-12.1.3.tgz", + "integrity": "sha512-CT2ZDhBmAZpHhAzHhEN+/J5oMK3Ds99ayLLdXh2Aw1DCcn72EM8VuIGVwG5fSjvkMsgtn7FgltFosHqeM6PRFQ==", + "dependencies": { + "openapi-types": "^12.1.3", + "ts-log": "^2.1.4" + } + }, + "node_modules/openapi-request-validator": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-request-validator/-/openapi-request-validator-12.1.3.tgz", + "integrity": "sha512-HW1sG00A9Hp2oS5g8CBvtaKvRAc4h5E4ksmuC5EJgmQ+eAUacL7g+WaYCrC7IfoQaZrjxDfeivNZUye/4D8pwA==", + "dependencies": { + "ajv": "^8.3.0", + "ajv-formats": "^2.1.0", + "content-type": "^1.0.4", + "openapi-jsonschema-parameters": "^12.1.3", + "openapi-types": "^12.1.3", + "ts-log": "^2.1.4" + } + }, + "node_modules/openapi-response-validator": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-response-validator/-/openapi-response-validator-12.1.3.tgz", + "integrity": "sha512-beZNb6r1SXAg1835S30h9XwjE596BYzXQFAEZlYAoO2imfxAu5S7TvNFws5k/MMKMCOFTzBXSjapqEvAzlblrQ==", + "dependencies": { + "ajv": "^8.4.0", + "openapi-types": "^12.1.3" + } + }, + "node_modules/openapi-schema-validator": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-12.1.3.tgz", + "integrity": "sha512-xTHOmxU/VQGUgo7Cm0jhwbklOKobXby+/237EG967+3TQEYJztMgX9Q5UE2taZKwyKPUq0j11dngpGjUuxz1hQ==", + "dependencies": { + "ajv": "^8.1.0", + "ajv-formats": "^2.0.2", + "lodash.merge": "^4.6.1", + "openapi-types": "^12.1.3" + } + }, + "node_modules/openapi-security-handler": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-security-handler/-/openapi-security-handler-12.1.3.tgz", + "integrity": "sha512-25UTAflxqqpjCLrN6rRhINeM1L+MCDixMltiAqtBa9Zz/i7UkWwYwdzqgZY3Cx3vRZElFD09brYxo5VleeP3HQ==", + "dependencies": { + "openapi-types": "^12.1.3" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3753,7 +3918,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3886,6 +4050,14 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", @@ -3953,6 +4125,14 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -4171,8 +4351,7 @@ "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 + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, "node_modules/stack-utils": { "version": "2.0.6", @@ -4389,6 +4568,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.12.0.tgz", + "integrity": "sha512-Rt1xUpbHulJVGbiQjq9yy9/r/0Pg6TmpcG+fXTaMePDc8z5WUw4LfaWts5qcNv/8ewPvBIbY7DKq7qReIKNCCQ==" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz", + "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tdigest": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", @@ -4447,6 +4645,11 @@ "node": ">=0.6" } }, + "node_modules/ts-log": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/ts-log/-/ts-log-2.2.5.tgz", + "integrity": "sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -4523,6 +4726,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/url-value-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/url-value-parser/-/url-value-parser-2.2.0.tgz", @@ -4605,8 +4816,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", @@ -4636,6 +4846,17 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/gatewayservice/package.json b/gatewayservice/package.json index 87d5256c..779178c7 100644 --- a/gatewayservice/package.json +++ b/gatewayservice/package.json @@ -21,7 +21,10 @@ "axios": "^1.6.5", "cors": "^2.8.5", "express": "^4.18.2", - "express-prom-bundle": "^7.0.0" + "express-openapi": "^12.1.3", + "express-prom-bundle": "^7.0.0", + "swagger-ui-express": "^5.0.0", + "yaml": "^2.4.1" }, "devDependencies": { "jest": "^29.7.0", diff --git a/package-lock.json b/package-lock.json index 6b9c122e..e12e1c11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "WIQ_ES2B", + "name": "wiq_es2b", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/questions/creationservice/creation-service.js b/questions/creationservice/creation-service.js index 7fc89323..1243145c 100644 --- a/questions/creationservice/creation-service.js +++ b/questions/creationservice/creation-service.js @@ -34,10 +34,14 @@ function getQuestionInfo(info){ // Select 4 random rows of the data for (let i = 0; i { +app.get('/createquestion', async (req, res) => { selectRandomQuery(); const apiUrl = `https://query.wikidata.org/sparql?query=${encodeURIComponent(queries[randomQuerySelector])}&format=json`; diff --git a/questions/retrieveservice/retrieve-service.js b/questions/retrieveservice/retrieve-service.js index 3ece0bc3..0d65c42a 100644 --- a/questions/retrieveservice/retrieve-service.js +++ b/questions/retrieveservice/retrieve-service.js @@ -11,7 +11,7 @@ const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/questiond mongoose.connect(mongoUri); -app.post('/getquestionshistory', async (req, res) => { +app.get('/getquestionshistory', async (req, res) => { const questions = await Question.find({}); var solution = []; diff --git a/sonar-project.properties b/sonar-project.properties index 8feb5c57..1807846d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,15 +1,19 @@ + sonar.projectKey=Arquisoft_wiq_es2b sonar.organization=arquisoft # This is the name and version displayed in the SonarCloud UI. sonar.projectName=wiq_es2b + sonar.projectVersion=1.0 # Encoding of the source code. Default is default system encoding sonar.host.url=https://sonarcloud.io sonar.language=js + sonar.projectName=wiq_es2b + sonar.coverage.exclusions=**/*.test.js sonar.sources=webapp/src/components,users/authservice,users/userservice,gatewayservice sonar.sourceEncoding=UTF-8 diff --git a/users/userservice/playedGame-model.js b/users/userservice/playedGame-model.js new file mode 100644 index 00000000..ef3f07cd --- /dev/null +++ b/users/userservice/playedGame-model.js @@ -0,0 +1,17 @@ +const mongoose = require('mongoose'); + +const gameSchema = new mongoose.Schema({ + username: { type: String, required: true }, + duration: Number, + questions: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Question' }], + date: { type: Date, default: Date.now } , + percentage: Number, + totalQuestions: Number, + correctAnswers: Number, + incorrectAnswers: Number +}); + +const Game = mongoose.model('Game', gameSchema); + +module.exports = Game; + diff --git a/users/userservice/question-model.js b/users/userservice/question-model.js new file mode 100644 index 00000000..80b3edcd --- /dev/null +++ b/users/userservice/question-model.js @@ -0,0 +1,11 @@ +const mongoose = require('mongoose'); + +const questionSchema = new mongoose.Schema({ + question: String, + correctAnswer: String, + userAnswer: String +}); + +const Question = mongoose.model('Question', questionSchema); + +module.exports = Question; diff --git a/users/userservice/user-model.js b/users/userservice/user-model.js index 71d81b5f..a1bd2e1b 100644 --- a/users/userservice/user-model.js +++ b/users/userservice/user-model.js @@ -4,6 +4,7 @@ const userSchema = new mongoose.Schema({ username: { type: String, required: true, + unique: true }, password: { type: String, diff --git a/users/userservice/user-service.js b/users/userservice/user-service.js index be958427..9f083dba 100644 --- a/users/userservice/user-service.js +++ b/users/userservice/user-service.js @@ -4,6 +4,8 @@ const mongoose = require('mongoose'); const bcrypt = require('bcrypt'); const bodyParser = require('body-parser'); const User = require('./user-model') +const Game = require('./playedGame-model') +const Question = require('./question-model') const app = express(); const port = 8001; @@ -20,9 +22,9 @@ mongoose.connect(mongoUri); // Function to validate required fields in the request body function validateRequiredFields(req, requiredFields) { for (const field of requiredFields) { - if (!(field in req.body)) { - throw new Error(`Missing required field: ${field}`); - } + if (!(field in req.body)) { + throw new Error(`Missing required field: ${field}`); + } } } @@ -42,17 +44,78 @@ app.post('/adduser', async (req, res) => { await newUser.save(); res.json(newUser); } catch (error) { - res.status(400).json({ error: error.message }); - }}); + res.status(400).json({ + error: error.message + }); + } +}); + +app.post('/addgame', async (req, res) => { + try { + // Obtener los datos del juego desde el cuerpo de la solicitud + const gameData = req.body; + + // Convertir las preguntas del juego en ObjectId + const questionIds = await Promise.all(gameData.questions.map(async (question) => { + const existingQuestion = await Question.findOne({ + question: question.question, + correctAnswer: question.correctAnswer, + userAnswer: question.userAnswer + }); + if (existingQuestion) { + return existingQuestion._id; + } else { + const newQuestion = new Question(question); + await newQuestion.save(); + return newQuestion._id; + } + })); + + // Reemplazar las preguntas en el juego con sus ObjectId + gameData.questions = questionIds; + + // Crear una nueva instancia del modelo de juego con los datos proporcionados + const newGame = new Game(gameData); + + // Guardar el nuevo juego en la base de datos + await newGame.save(); + + // Enviar una respuesta de éxito + res.status(200).json({ message: "Partida guardada exitosamente" }); + } catch (error) { + // Manejar errores y enviar una respuesta de error con el mensaje de error + console.error("Error al guardar el juego:", error); + res.status(400).json({ error: error.message }); + } +}); + + + +app.get('/getgamehistory/:username', async (req, res) => { + try { + const username = req.params.username; + console.log("Se está intentando encontrar el historial del usuario " + username); + // Buscar las partidas asociadas al nombre de usuario proporcionado + const games = await Game.find({ username }).populate('questions'); + console.log("Se encontraron los juegos para " + username + ": ", games); + res.json(games); + } catch (error) { + res.status(400).json({ + error: error.message + }); + } +}); const server = app.listen(port, () => { - console.log(`User Service listening at http://localhost:${port}`); + console.log(`User Service listening at http://localhost:${port}`); }); + + // Listen for the 'close' event on the Express.js server server.on('close', () => { // Close the Mongoose connection mongoose.connection.close(); - }); +}); module.exports = server \ No newline at end of file diff --git a/webapp/README.md b/webapp/README.md index 9568101e..33dd89a5 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -108,14 +108,21 @@ E2E tests are maybe the most difficult part to integrate in our system. We have In this project, the E2E testing user stories are defined using Cucumber. Cucumber uses a language called Gherkin to define the user stories. You can find the example in the `features` directory. Then, the actual tests are in the folder `steps`. We are going to configure jest to execute only the tests of this directory (check the `jest.config.ts` file in the `e2e` directory). -The E2E tests have two extra difficulties. The first one, we need a browser to perform the tests as if the user was using the application. For this matter, we use `jest-puppeteer` that will launch a Chromium instance for running the tests. The browser is started in the `beforeAll` function. Note that the browser is launched in a headless mode. This is necessary for the tests to run in the CI environment. If you want to debug the tests you can always turn this feature off. The second problem is that we need all our services at the same time to be able to run the tests. For achieving this, we are going to use the package `start-server-and-test`. This package, allows us to launch multiple servers and then run the tests. No need for any configuration. We can configure it straight in the `package.json` file: +The E2E tests have two extra difficulties. The first one, we need a browser to perform the tests as if the user was using the application. +For this matter, we use `jest-puppeteer` that will launch a Chromium instance for running the tests. +The browser is started in the `beforeAll` function. Note that the browser is launched in a headless mode. +This is necessary for the tests to run in the CI environment. If you want to debug the tests you can always turn this feature off. +The second problem is that we need all our services at the same time to be able to run the tests. +For achieving this, we are going to use the package `start-server-and-test`. +This package, allows us to launch multiple servers and then run the tests. +No need for any configuration. We can configure it straight in the `package.json` file: ```json "test:e2e": "start-server-and-test 'node e2e/test-environment-setup.js' http://localhost:8000/health prod 3000 \"cd e2e && jest\"", ``` -The package accepts pairs of parameters (launch a server and an URL to check if it is running. It also accepts npm commands (for instance prod, for the webapp, that will run `npm run prod`). The last parameter of the task will be launching Jest to run the E2E tests. +The package accepts pairs of parameters (launch a server and an URL to check if it is running. It also accepts npm commands (for instance prod, for the webapp, that will run `npm run prod`). The last parameter of the task will be launching Jest to run the e2e tests. Note that we are handling all the setup for the auth and user microservices using the file `test-environment-setup.js`. This file has the code needed to run everything, including an in-memory Mongo database to be able to execute the tests. diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 7b2f7861..0256e663 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -22,6 +22,8 @@ "web-vitals": "^3.5.1" }, "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", "axios-mock-adapter": "^1.22.0", "expect-puppeteer": "^9.0.2", "jest": "^29.3.1", @@ -659,9 +661,17 @@ } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, "engines": { "node": ">=6.9.0" }, @@ -1905,6 +1915,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", diff --git a/webapp/package.json b/webapp/package.json index af6ba0d7..18a15f71 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -43,6 +43,8 @@ ] }, "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", "axios-mock-adapter": "^1.22.0", "expect-puppeteer": "^9.0.2", "jest": "^29.3.1", diff --git a/webapp/src/App.css b/webapp/src/App.css index 74b5e053..60638c44 100644 --- a/webapp/src/App.css +++ b/webapp/src/App.css @@ -1,9 +1,10 @@ .App { text-align: center; + font-family: Arial, sans-serif; } .App-logo { - height: 40vmin; + height: 150px; pointer-events: none; } @@ -14,18 +15,24 @@ } .App-header { - background-color: #282c34; + background-color: #007bff; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; - font-size: calc(10px + 2vmin); + font-size: 24px; color: white; } .App-link { color: #61dafb; + text-decoration: none; + transition: color 0.3s ease; +} + +.App-link:hover { + color: #007bff; } @keyframes App-logo-spin { diff --git a/webapp/src/App.js b/webapp/src/App.js index 6e130102..50a60ea3 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -16,6 +16,7 @@ function App() { return ( + @@ -35,6 +36,7 @@ function App() { + ); } diff --git a/webapp/src/App.test.js b/webapp/src/App.test.js index d182fde3..8602a898 100644 --- a/webapp/src/App.test.js +++ b/webapp/src/App.test.js @@ -3,11 +3,13 @@ import {BrowserRouter as Router} from "react-router-dom"; import App from './App'; test('renders learn react link', () => { + render( ); const linkElement = screen.getByText(/Bienvenido a WIQ 2024 del curso de Arquitectura del Software/i); + expect(linkElement).toBeInTheDocument(); }); diff --git a/webapp/src/Timer.css b/webapp/src/Timer.css new file mode 100644 index 00000000..2fe296ea --- /dev/null +++ b/webapp/src/Timer.css @@ -0,0 +1,71 @@ +.Timer { + font-family: sans-serif; + text-align: center; + height: 50vh; + display: flex; + justify-content: center; + align-items: center; + } + + .Timer .container { + position: relative; + transform: scale(200%); + } + + .Timer .text { + position: absolute; + color: #007bff; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-weight: bold; + font-size: 36px; + } + + .Timer .container svg { + width: 150px; + height: 150px; + /* background-color: red; */ + transform: rotate(-90deg); + } + + .Timer .container svg circle { + fill: transparent; + stroke: #415262; + stroke-width: 4; + transform: translate(5px, 5px); + transition: all 2s; + z-index: 0; + } + + .Timer .container svg circle:nth-child(2) { + fill: transparent; + stroke: #007bff; + stroke-width: 6; + stroke-dasharray: 440; + /* stroke-dashoffset: 0; */ + transition: all 0.1s; + } + + .Timer .dot { + position: absolute; + width: 100%; + height: 100%; + /* background-color: rgba(0, 128, 0, 0.281); */ + transition: all 0.1s; + z-index: 9999; + } + + .Timer .dot::before { + content: ""; + width: 20px; + height: 20px; + background-color: #007bff; + z-index: 1000; + position: absolute; + border-radius: 10px; + top: -2px; + transform: translate(-10px); + box-shadow: 0 0 20px 4px #007bff; + } + \ No newline at end of file diff --git a/webapp/src/components/Game.css b/webapp/src/components/Game.css index 1e4d4c29..dca78363 100644 --- a/webapp/src/components/Game.css +++ b/webapp/src/components/Game.css @@ -17,3 +17,12 @@ button[title="btnsPreg"]{ background-color: rgba(41, 120, 152, 0.764); } +button[title="puntuacion"]:disabled{ + margin: 1em; + background-color: rgba(31, 60, 134, 0.764); + color: white; + padding-top: 0.4em; + padding-bottom: 0.2em; + font-size: 1.5em; +} + diff --git a/webapp/src/components/Game.js b/webapp/src/components/Game.js index 005d7ade..8c44e537 100644 --- a/webapp/src/components/Game.js +++ b/webapp/src/components/Game.js @@ -1,13 +1,17 @@ import React, { useState, useEffect } from 'react'; import axios from 'axios'; import { Container, Typography, Button, Paper} from '@mui/material'; +import { useNavigate } from 'react-router-dom'; import './Game.css'; +import '../Timer.css'; + const colorPreguntas= 'rgba(51, 139, 173, 0.764)'; const colorOnMousePreguntas= 'rgba(28, 84, 106, 0.764)'; const Game = () => { + const navigate = useNavigate(); const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; const [questionObject, setQuestionObject] = useState(''); @@ -17,34 +21,114 @@ const Game = () => { const [questionCounter, setQuestionCounter] = useState(0); const [incorrectCounter, setIncorrectCounter] = useState(0); + + const [numberOfQuestions] = useState(6); + const [questionsToAnswer, setQuestionsToAnswer] = useState(6); + const [isFinished, setFinished] = useState(false); + + // Porcentaje de aciertos + const [percentage, setPercentage] = useState(0); + + + //para el final de partida + const [gameUserOptions, setGameUserOptions] = useState([]); + const [gameCorrectOptions, setGameCorrectOptions] = useState([]); + const [gameQuestions, setGameQuestions] = useState([]); + + const [seconds, setSeconds] = useState(120); - // Temporizador - const [seconds, setSeconds] = useState(120); // 2 minutes + // Temporizador desde 20 segundos + const [time, setTime] = useState(20); + const [isTimedOut, setTimedOut] = useState(false); + + // Estado para controlar si el temporizador está activo o no + const [isTimerActive, setIsTimerActive] = useState(true); + useEffect(() => { - handleShowQuestion(); - }); + const id = setInterval(() => { + if (isTimerActive) { // Solo decrementa el tiempo si el temporizador está activo + setTime((prev) => { + if (prev > 0) { + return prev - 1; + } else { + // Se acabó el tiempo + setTimedOut(true); + } + }); + } + }, 1000); + return () => clearInterval(id); + }, [isTimerActive]); + + + // Calcular el porcentaje de tiempo transcurrido para el círculo del temporizador + const percentageTime = ((20 - time) / 20) * 100; + + + // Detener el temporizador + const stopTimer = () => { + setIsTimerActive(false); + }; + + // Activar el temporizador + const restartTimer = () => { + setTime(20); // Reiniciar el tiempo a 20 segundos + setIsTimerActive(true); + }; + + + useEffect(() => { + handleShowQuestion(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { + console.log("eyou"); const intervalId = setInterval(() => { setSeconds(prevSeconds => prevSeconds - 1); }, 1000); return () => clearInterval(intervalId); }, []); + + + useEffect(() => { + if (isGameFinished() && !isFinished){ + finishGame(); + setFinished(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [correctCounter]); + + useEffect(() => { + if (isGameFinished() && !isFinished){ + finishGame(); + setFinished(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [incorrectCounter]); // This method will call the create question service const handleShowQuestion = async () => { try{ // It makes a petition to the api and store the response - const response = await axios.post(`${apiEndpoint}/createquestion`, { }); + const response = await axios.get(`${apiEndpoint}/createquestion`, { }); // Extract all the info of the response and store it setQuestionObject(response.data.responseQuestionObject); setCorrectOption(response.data.responseCorrectOption); setAnswerOptions(response.data.responseAnswerOptions); + + //guardar para el final + // Actualizar las preguntas del juego + setGameQuestions(prevQuestions => [...prevQuestions, response.data.responseQuestionObject]); + // Actualizar las opciones correctas del juego + setGameCorrectOptions(prevCorrectOptions => [...prevCorrectOptions, response.data.responseCorrectOption]); + + const buttons = document.querySelectorAll('button[title="btnsPreg"]'); buttons.forEach(button => { button.name = "sinContestar"; @@ -54,6 +138,10 @@ const Game = () => { }); incrementQuestion(); + + // Resetear temporizador a 20 segundos + restartTimer(); + }catch (error){ console.error('Error:', error); } @@ -61,7 +149,9 @@ const Game = () => { // Method that checks if the answer clicked is the correct one const handleAnswerClick = (option, index) => { - // Get what component is the button to change its color later + // Almacenar la opción seleccionada por el usuario en gameUserOptions + setGameUserOptions(prevUserOptions => [...prevUserOptions, option]); + if(option === correctOption) { const buttonId = `button_${index}`; const correctButton = document.getElementById(buttonId); @@ -73,6 +163,22 @@ const Game = () => { const buttonId = `button_${index}`; const incorrectButton = document.getElementById(buttonId); incorrectButton.style.backgroundColor = "rgba(208, 22, 22, 0.952)"; + + // parar el temporizador + stopTimer(); + + // mostrar la correcta + for (let correctIndex = 0; correctIndex < 4; correctIndex++){ + const buttonIdCorrect = `button_${correctIndex}`; + const correctButton = document.getElementById(buttonIdCorrect); + + console.log("BOTON A COMPROBAR: " + correctButton.textContent); + + if (correctButton.textContent === correctOption) { + correctButton.style.backgroundColor = "rgba(79, 141, 18, 0.726)"; + } + } + incrementIncorrect(); } @@ -81,31 +187,162 @@ const Game = () => { button.disabled = true; button.onmouse = null; }); + + + decrementQuestionsToAnswer(); + + if (!isGameFinished()) { + setTimeout(() => { + handleShowQuestion(); + }, 1000); + } + } - // Cambiar a la siguiente pregunta después de 3 segundos - setTimeout(() => { - handleShowQuestion(); - }, 1500); + const isGameFinished = () => { + return questionCounter >= numberOfQuestions; } + const handleMainPage = () => { + let path= '/mainPage'; + navigate(path); +}; +const getQuestions = () => { + const questionsList = []; + + // Iterar sobre cada pregunta generada dinámicamente y agregarla a la lista + for (let i = 0; i < gameQuestions.length; i++) { + const questionObject = gameQuestions[i]; + const correctAnswer = gameCorrectOptions[i]; + const userAnswer = gameUserOptions[i] || ''; // Establecer la respuesta del usuario como cadena vacía si no hay respuesta + questionsList.push({ question: questionObject, correctAnswer, userAnswer }); + } + + return questionsList; +}; + + + + + const finishGame = () => { + const buttons = document.querySelectorAll('button[title="btnsPreg"]'); + buttons.forEach(button => { + button.disabled = true; + button.onmouse = null; + }); + console.log("finishGame " + correctCounter); + var correctas = (correctCounter / numberOfQuestions) * 100; + console.log("corr1 " + correctas); + if (!Number.isInteger(percentage)){ + correctas = correctas.toFixed(2); + console.log("dentro " + correctas); + } + console.log("corr2 " + correctas); + setPercentage(correctas); + + //a partir de aqui guardar la partida + const username=localStorage.getItem('username'); + const newGame = { + username: username, + duration: seconds, + questions: getQuestions() , + percentage: correctas, + totalQuestions: numberOfQuestions, + correctAnswers: correctCounter, + incorrectAnswers: numberOfQuestions-correctCounter + }; + console.log("Se va a guardar la siguiente partida:"); + console.log("Username:", newGame.username); + console.log("Duración:", newGame.duration); + console.log("Preguntas:", newGame.questions); + console.log("Porcentaje de Aciertos:", newGame.percentage); + console.log("Número Total de Preguntas:", newGame.totalQuestions); + console.log("Número de Respuestas Correctas:", newGame.correctAnswers); + console.log("Número de Respuestas Incorrectas:", newGame.incorrectAnswers); + + + + axios.post(`${apiEndpoint}/addgame`, newGame) + .then(response => { + console.log("Respuesta del servidor:", response.data); + }) + .catch(error => { + console.error("Error al enviar la solicitud:", error); + }); + } + const incrementCorrect = () => { - setCorrectCounter(correctCounter + 1); + setCorrectCounter(correct => correct + 1); }; const incrementIncorrect = () => { - setIncorrectCounter(incorrectCounter + 1); + setIncorrectCounter(incorrect => incorrect + 1); + } + + const decrementQuestionsToAnswer = () => { + setQuestionsToAnswer(toAnswer => toAnswer - 1); } const incrementQuestion = () => { - setQuestionCounter(questionCounter + 1); + setQuestionCounter(qc => qc + 1); } + return ( + + {!isFinished && ( Saber y Ganar Juego + + {!isFinished && ( + +
+
+ + +
+
+
{time}
+
+ + + + +
+ +
+
+ + )} + + {isTimedOut && ( + + + ¡Tiempo agotado! + + + + )} + + + + Pregunta {questionCounter}: {questionObject} @@ -116,23 +353,55 @@ const Game = () => { ))} + +
+ )} + {!isFinished && ( + + )} + {!isFinished && ( - + )} + {!isFinished && ( + )} + + + + + -
- + + + {isFinished && (
- Time Remaining: {Math.floor(seconds / 60)}:{(seconds % 60).toLocaleString('en-US', { minimumIntegerDigits: 2 })} + + + Partida finalizada. ¡Gracias por jugar! + +
+ + +
+ + +
-
+ )} +
); diff --git a/webapp/src/components/HistoricalData.css b/webapp/src/components/HistoricalData.css new file mode 100644 index 00000000..1e20a1c4 --- /dev/null +++ b/webapp/src/components/HistoricalData.css @@ -0,0 +1,50 @@ +.contenedor { + display: flex; + flex-direction: column; + align-items: center; +} + +div[title="botones"]{ + display: grid; + grid-template-columns: 1fr 1fr; + align-items: center; + margin-top: 2em; + margin-bottom: 2em; + grid-gap: 2em; +} + +button{ + margin: 1em; + padding: 0.25em; + background-color: rgba(31, 60, 134, 0.764); + color: white; + font-size: 0.75em; +} + +table { + width: 90%; + border-collapse: collapse; + color: black; + } + + th, td { + padding: 0.25em; + text-align: center; + border: 0.1em solid #000; + } + + th[title='pregunta'] { + background-color: rgba(41, 120, 152, 0.764); + } + + th[title='correcta'] { + background-color: rgba(79, 141, 18, 0.726); + } + + th[title='incorrecta'] { + background-color: rgba(230, 72, 72, 0.952); + } + + td{ + background-color: rgba(61, 178, 224, 0.764); + } \ No newline at end of file diff --git a/webapp/src/components/HistoricalData.js b/webapp/src/components/HistoricalData.js index 6e746026..58ea119e 100644 --- a/webapp/src/components/HistoricalData.js +++ b/webapp/src/components/HistoricalData.js @@ -2,6 +2,7 @@ import axios from 'axios'; import React, { useState} from 'react'; import { useNavigate} from 'react-router-dom'; import { Container, Button} from '@mui/material'; +import './HistoricalData.css'; const HistoricalData = () => { const navigate = useNavigate(); @@ -12,7 +13,7 @@ const HistoricalData = () => { const handleShowHistory = async () => { try{ // It makes a petition to the api and store the response - const response = await axios.post(`${apiEndpoint}/getquestionshistory`, { }); + const response = await axios.get(`${apiEndpoint}/getquestionshistory`, { }); setQuestionsHistory(response.data); }catch (error){ console.error('Error:', error); @@ -26,9 +27,9 @@ const HistoricalData = () => { return ( - + -
+
@@ -41,11 +42,11 @@ const HistoricalData = () => { - - - - - + + + + + diff --git a/webapp/src/components/HistoricalUserData.js b/webapp/src/components/HistoricalUserData.js new file mode 100644 index 00000000..22e2695e --- /dev/null +++ b/webapp/src/components/HistoricalUserData.js @@ -0,0 +1,89 @@ +import React, { useState } from 'react'; +import axios from 'axios'; +import { useNavigate } from 'react-router-dom'; +import { Container, Button } from '@mui/material'; + +const HistoricalUserData = () => { + const navigate = useNavigate(); + const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; + + const [gameHistory, setGameHistory] = useState([]); + + + const handleLoadHistory = async () => { + try { + const username = localStorage.getItem('username'); + const response = await axios.get(`${apiEndpoint}/getgamehistory/${username}`); + + // Ordenar la lista de historial de partidas por fecha (de más reciente a más antigua) + const sortedHistory = response.data.sort((a, b) => new Date(b.date) - new Date(a.date)); + + setGameHistory(sortedHistory); + + console.log("el historial actual es "+gameHistory); + + } catch (error) { + console.error('Error:', error); + } + }; + + const handlePreviousPage = async () => { + let path= '/MainPage'; + navigate(path); + } + + + return ( + + + + +
+

Historial de Partidas:

+
PreguntaOpción correctaOpción incorrecta 1Opción incorrecta 2Opción incorrecta 3PreguntaOpción correctaOpción incorrecta 1Opción incorrecta 2Opción incorrecta 3
+ + + + + + + + + + + + {gameHistory.map((game) => ( + + + + + + + + + + {game.questions && game.questions.map((question, index) => ( + + + + ))} + + ))} + +
FechaTiempo de partida (s)Porcentaje de AciertosNúmero de PreguntasNúmero de AciertosNúmero de Fallos
{game.date}{game.duration}{game.percentage}%{game.totalQuestions}{game.correctAnswers}{game.incorrectAnswers}
+

Pregunta {index + 1}: {question.question}

+

Respuesta Correcta: {question.correctAnswer}

+

Respuesta del Usuario: {question.userAnswer}

+

La respuesta fue: {question.correctAnswer === question.userAnswer ? 'Correcta' : 'Incorrecta'}

+
+
+ + ); + +}; + +export default HistoricalUserData; diff --git a/webapp/src/components/Login.js b/webapp/src/components/Login.js index 0e39f9b6..478b48be 100644 --- a/webapp/src/components/Login.js +++ b/webapp/src/components/Login.js @@ -21,7 +21,7 @@ const Login = () => { const loginUser = async () => { try { await axios.post(`${apiEndpoint}/login`, { username, password }); - + localStorage.setItem('username',username); setLoginSuccess(true); setOpenSnackbar(true); diff --git a/webapp/src/components/MainPage.css b/webapp/src/components/MainPage.css new file mode 100644 index 00000000..c210f19c --- /dev/null +++ b/webapp/src/components/MainPage.css @@ -0,0 +1,12 @@ +div[title="main"]{ + display: grid; + grid-template-columns: 1fr; +} + +div[title="main"]>button{ + margin: 1em; + padding: 0.5em; + background-color: rgba(31, 60, 134, 0.764); + color: white; + font-size: 1em; +} \ No newline at end of file diff --git a/webapp/src/components/MainPage.js b/webapp/src/components/MainPage.js index 17f1dffe..757bf60a 100644 --- a/webapp/src/components/MainPage.js +++ b/webapp/src/components/MainPage.js @@ -1,5 +1,7 @@ -import { Container, Typography, Button} from '@mui/material'; -import { useNavigate} from 'react-router-dom'; +import React, { } from 'react'; +import { Container, Typography, Button } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import './MainPage.css'; const MainPage = () => { const navigate = useNavigate(); @@ -14,9 +16,15 @@ const MainPage = () => { navigate(path); }; + const handleShowHistoricalUserData = () => { + let path= '/HistoricalUserData'; + navigate(path); + }; + + return ( -
+
¡Bienvenido a WIQ 2024! @@ -25,17 +33,14 @@ const MainPage = () => { Puedes comenzar la partida o ver tu historial. - - {/* - Your account was created on {createdAt}. - */} - - {/* Se declaran los botones en los q al hacer click se ejecuta el metodo especificado en onClick*/} +
diff --git a/webapp/src/index.js b/webapp/src/index.js index 3923d310..9f794123 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -7,6 +7,7 @@ import App from './App'; import Game from './components/Game'; import HistoricalData from './components/HistoricalData'; import MainPage from './components/MainPage'; +import HistoricalUserData from './components/HistoricalUserData'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( @@ -17,6 +18,7 @@ root.render( }> }> }> + }>