diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c8a950..485cabf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: - run: npm --prefix webapp run test:e2e docker-push-webapp: name: Push webapp Docker Image to GitHub Packages - runs-on: ubuntu-latest + runs-on: ARM64 permissions: contents: read packages: write @@ -61,7 +61,7 @@ jobs: buildargs: API_URI docker-push-authservice: name: Push auth service Docker Image to GitHub Packages - runs-on: ubuntu-latest + runs-on: ARM64 permissions: contents: read packages: write @@ -78,7 +78,7 @@ jobs: workdir: users/authservice docker-push-userservice: name: Push user service Docker Image to GitHub Packages - runs-on: ubuntu-latest + runs-on: ARM64 permissions: contents: read packages: write @@ -95,7 +95,7 @@ jobs: workdir: users/userservice docker-push-gatewayservice: name: Push gateway service Docker Image to GitHub Packages - runs-on: ubuntu-latest + runs-on: ARM64 permissions: contents: read packages: write @@ -112,7 +112,7 @@ jobs: workdir: gatewayservice deploy: name: Deploy over SSH - runs-on: ubuntu-latest + runs-on: ARM64 needs: [docker-push-userservice,docker-push-authservice,docker-push-gatewayservice,docker-push-webapp] steps: - name: Deploy over SSH diff --git a/README.md b/README.md index f05f003..4f956f6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,28 @@ -# wiq_en1a +# WIQ [![Deploy on release](https://github.com/Arquisoft/wiq_en1a/actions/workflows/release.yml/badge.svg)](https://github.com/Arquisoft/wiq_en1a/actions/workflows/release.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Arquisoft_wiq_en1a&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Arquisoft_wiq_en1a) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Arquisoft_wiq_en1a&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Arquisoft_wiq_en1a) -### Members + +Welcome to WIQ, your gateway to a world of knowledge exploration! 🚀 + +WIQ is a dynamic web application that harnesses the power of Wikidata to create an engaging and educational experience. Dive into the vast ocean of information, challenge yourself with thought-provoking questions across various topics, and elevate your learning journey. + +Happy exploring! 🌐✨ +### Getting Started: +A quick start is using docker compose, so all containers will be created automatically. The web application is hosted on the port 3000 whereas the gateway service is in 8000. + +```bash +git clone https://github.com/Arquisoft/wiq_en1a.git +docker compose --profile dev up --build +``` + +## Meet our Team +We are students of Software Architecture in the University of Oviedo. This web application is the laboratory project of the subject. We hope you like our webapp and we are welcomed to proposals of new content by our Issues. + +You can check our Documentation [here](https://arquisoft.github.io/wiq_en1a/) where all the tecnical details are explained + +### Developers + | Name | Email | |-----------------------------|--------------------| | Andrés Cadenas Blanco | UO282276@uniovi.es | diff --git a/docker-compose.yml b/docker-compose.yml index 4a074de..ae4a9d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: - "27017:27017" networks: - mynetwork + # platform: linux/arm64 authservice: container_name: authservice-${teamname:-defaultASW} @@ -24,6 +25,7 @@ services: - mynetwork environment: MONGODB_URI: mongodb://mongodb:27017/userdb + # platform: linux/arm64 userservice: container_name: userservice-${teamname:-defaultASW} @@ -38,6 +40,7 @@ services: - mynetwork environment: MONGODB_URI: mongodb://mongodb:27017/userdb + # platform: linux/arm64 gatewayservice: container_name: gatewayservice-${teamname:-defaultASW} @@ -55,6 +58,7 @@ services: environment: AUTH_SERVICE_URL: http://authservice:8002 USER_SERVICE_URL: http://userservice:8001 + # platform: linux/arm64 webapp: container_name: webapp-${teamname:-defaultASW} @@ -65,6 +69,8 @@ services: - gatewayservice ports: - "3000:3000" + # platform: linux/arm64 + prometheus: image: prom/prometheus @@ -79,6 +85,7 @@ services: - "9090:9090" depends_on: - gatewayservice + grafana: image: grafana/grafana @@ -100,6 +107,7 @@ services: - prometheus + volumes: mongodb_data: prometheus_data: diff --git a/docs/src/03_system_scope_and_context.adoc b/docs/src/03_system_scope_and_context.adoc index 9c79942..3f24776 100644 --- a/docs/src/03_system_scope_and_context.adoc +++ b/docs/src/03_system_scope_and_context.adoc @@ -48,17 +48,15 @@ The title of the table is the name of your system, the three columns contain the **** -[plantuml,"Context diagram",png] +[plantuml,"Context Diagram",png] ---- actor Player -actor Client [Wikidata] <> [WIQ Game] <> [UsersAPI] <> [GeneratedQuestionsAPI] <> Player ..> (WIQ Game) : register/login -Client ..> (WIQ Game) : view data [GeneratedQuestionsAPI] ..> Wikidata [WIQ Game] ..> UsersAPI [WIQ Game] ..> GeneratedQuestionsAPI @@ -71,21 +69,18 @@ Client ..> (WIQ Game) : view data |Player |Plays the game and can consult past scores -|Client -|Can access the data about players and generated questions - |WIQ Game |Main system in which generated questions are shown and can be answered by players |Wikidata -|External data repository from which questions are generated using the WikidataAPI +|External data repository from which questions are generated |MongoDB -|Database for storing generated questions, players' info and scores +|Database for storing players' info and scores |Users Info API -|Allows clients to see the info about players stored in the database +|Manages data of users, both registration/login data and their past scores |Generated Questions API -|Allows clients to see the info about generated questions stored in the database +|Manages generation of questions from Wikidata |=== \ No newline at end of file diff --git a/docs/src/05_building_block_view.adoc b/docs/src/05_building_block_view.adoc index 036ba49..4f0573c 100644 --- a/docs/src/05_building_block_view.adoc +++ b/docs/src/05_building_block_view.adoc @@ -63,24 +63,19 @@ In the best case you will get away with examples or simple signatures. **** -[plantuml,"White box overall system",png] +[plantuml,"Whitebox overall system",png] ---- actor Player -actor Client [Wikidata] rectangle "WIQ Game (Level 1)"{ [WIQ Game GUI] -[Data View] [UsersAPI] [GeneratedQuestionsAPI] #BurlyWood Player ..> (WIQ Game GUI) -Client ..> (Data View) [GeneratedQuestionsAPI] ..> Wikidata [WIQ Game GUI] ..> UsersAPI [WIQ Game GUI] ..> GeneratedQuestionsAPI -[Data View] ..> UsersAPI -[Data View] ..> GeneratedQuestionsAPI } ---- @@ -97,11 +92,8 @@ Contained Black boxes:: |WIQ Game GUI |Main window in which questions are shown and can be answered by the player. The latter can also see past scores. -|Data View -|Access to data about users and generated questions for the client. - |Generated Questions API -|In charge of generating the questions and storing their data +|In charge of generating the questions |UsersAPI |In charge of keeping track of the data of the users (registration and scores) @@ -165,21 +157,18 @@ Leave out normal, simple, boring or standardized parts of your system ...describes the internal structure of _building block 1_. **** -[plantuml,"Generated Questions API (White Box)",png] +[plantuml,"Generated Questions API (WhiteBox)",png] ---- [Wikidata] -[wikidatanpm] <> +[wikibase-sdk] <> [WIQ Game GUI] -[Data View] database MongoDB rectangle "GenedQuestsAPI (Level 2)"{ - -[GenQuestsService] ..> wikidatanpm -[GenQuestsService] ..> Wikidata +[GenQuestsService] ..> [wikibase-sdk] +[GenQuestsService] ..> [Wikidata] [GenQuestsService] <--> MongoDB -[WIQ Game GUI] ..> GenQuestsService : new Question -[Data View] ..> GenQuestsService : get Questions +[WIQ Game GUI] ..> [GenQuestsService] : new Question } ---- @@ -194,23 +183,12 @@ Contained Black boxes:: |Name |Responsibility |GenQuestsService -|Receives different petitions regarding the generation of questions or their retrieval and responds accordingly. +|Receives different petitions regarding the generation of questions and responds accordingly. -|wikidatanpm +|wikibase-sdk |External library that facilitates and simplifies the use of wikidata for the generation of questions. |MongoDB -|The data of the generated questions is stored here for showing it later to the client. - -|=== - -Additional explanation of relationships:: - -The service is requested either the generation of a question or the retrieval of a subset (from WIQ Game GUI or -Data View respectively). - -For the first case, it makes use of the library to generate the question, retrieves it -for the interface to use and stores the pertinent data in the DB. +|Data about users and their scores is stored here -In the other case, it just makes the query according to the client choosings and retrieves the data of the -questions for the Data View to show. \ No newline at end of file +|=== \ No newline at end of file diff --git a/docs/src/08_concepts.adoc b/docs/src/08_concepts.adoc index 591ccf1..5ea68b2 100644 --- a/docs/src/08_concepts.adoc +++ b/docs/src/08_concepts.adoc @@ -55,19 +55,26 @@ image::08-Crosscutting-Concepts-Structure-EN.png["Possible topics for crosscutti See https://docs.arc42.org/section-8/[Concepts] in the arc42 documentation. **** +=== _Domain model and terminology_ -=== __ -__ +=== _Microservice based system_ -=== __ +Different business functionallities will be developed in different independent services. +This will ensure that if one of them fails, the rest are still working (For example, +if the rankings go down that will not affect the main game) as they have their own deployment +as well. +Other benefits are increased maintainability due to separation in small, more readable modules +and the possibillity of using different languages or technologies for each module if needed/prefered +without colliding with the rest. -__ -... +=== _Gateway service routing_ -=== __ - -__ +We will use a speciallized service that will route the requests to the corresponding service, acting as +a single entry point for the application. Requests to the services are much simpler as only the api base +endpoint and the action needs to be known and the gateway can also act as a filter to manage requests +conditionally if needed. This approach also favors security as we can control which requests are actually +sent based on its content or the context of the app. \ No newline at end of file diff --git a/docs/src/11_technical_risks.adoc b/docs/src/11_technical_risks.adoc index dc5575f..6297197 100644 --- a/docs/src/11_technical_risks.adoc +++ b/docs/src/11_technical_risks.adoc @@ -23,3 +23,14 @@ List of risks and/or technical debts, probably including suggested measures to m See https://docs.arc42.org/section-11/[Risks and Technical Debt] in the arc42 documentation. **** + +[options="header", cols="1,1,1,1"] +|=== +|Risk |Why it exists |Severity - Explanation |Possible solutions + +|Use of wikibase-sdk version 8 (not final) +|It is the last version that supports 'require', which is needed to use express in the same module +|Low - The difference is, a priori, merely functional, but retains the needed characteristics +|Upgrade only if a newer version supports 'require' + +|=== \ No newline at end of file diff --git a/docs/src/12_glossary.adoc b/docs/src/12_glossary.adoc index 192b235..9b0adb5 100644 --- a/docs/src/12_glossary.adoc +++ b/docs/src/12_glossary.adoc @@ -34,9 +34,9 @@ See https://docs.arc42.org/section-12/[Glossary] in the arc42 documentation. |=== |Term |Definition -| -| +|Microservice +|Small and independent component that performs a specific business function -| -| +|API +|Set of endpoints exposed by the backend server whose purpose is interacting with the client-side |=== diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index 88b84c8..62fe881 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -8,6 +8,7 @@ const port = 8000; const authServiceUrl = process.env.AUTH_SERVICE_URL || 'http://localhost:8002'; const userServiceUrl = process.env.USER_SERVICE_URL || 'http://localhost:8001'; +const questionServiceUrl = process.env.QUESTION_SERVICE_URL || 'http://localhost:8010'; app.use(cors()); app.use(express.json()); @@ -31,6 +32,7 @@ app.post('/login', async (req, res) => { } }); + app.post('/adduser', async (req, res) => { try { // Forward the add user request to the user service @@ -41,6 +43,52 @@ app.post('/adduser', async (req, res) => { } }); +app.get('/flags/question', async (req, res) => { + try { + // Forward the request to the question service + const questionResponse = await axios.get(questionServiceUrl+'/flags/question', req.body); + res.json(questionResponse.data); + } catch (error) { + res.status(error.response.status).json({ error: error.response.data.error }); + } +}); + +app.get('/flags/answer', async (req, res) => { + try { + // Forward the request to the question service + const questionResponse = await axios.get(questionServiceUrl+'/flags/answer', req.body); + res.json(questionResponse.data); + } catch (error) { + res.status(error.response.status).json({ error: error.response.data.error }); + } +}); + + +app.get('/self', async (req, res) => { + try { + // Forward the self request to the user service + + const userResponse = await axios.get(authServiceUrl+'/self', { + headers: { + Authorization: req.headers.authorization, + }, + }); + + res.status(200).json(userResponse.data); + } catch (error) { +res.status(error.response.status).json({ error: error.response.data.error }); + } +}); +app.get('/rankings', async (req, res) => { + try { + // Forward the request to the user service + const userResponse = await axios.get(userServiceUrl+'/rankings', req.body); + res.json(userResponse.data); + } catch (error) { + res.status(error.response.status).json({ error: error.response.data.error }); + } +}); + // Start the gateway service const server = app.listen(port, () => { console.log(`Gateway Service listening at http://localhost:${port}`); diff --git a/gatewayservice/package-lock.json b/gatewayservice/package-lock.json index fc5f2d6..5ef04ef 100644 --- a/gatewayservice/package-lock.json +++ b/gatewayservice/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "axios": "^1.6.5", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "express": "^4.18.2", "express-prom-bundle": "^7.0.0" @@ -1787,6 +1788,26 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/gatewayservice/package.json b/gatewayservice/package.json index f712722..313b29f 100644 --- a/gatewayservice/package.json +++ b/gatewayservice/package.json @@ -19,6 +19,7 @@ "homepage": "https://github.com/arquisoft/wiq_en1a#readme", "dependencies": { "axios": "^1.6.5", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "express": "^4.18.2", "express-prom-bundle": "^7.0.0" diff --git a/questionservice/package-lock.json b/questionservice/package-lock.json new file mode 100644 index 0000000..6ea2cf7 --- /dev/null +++ b/questionservice/package-lock.json @@ -0,0 +1,699 @@ +{ + "name": "questionservice", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "express": "^4.18.3", + "wikibase-sdk": "^8.1.1" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", + "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wikibase-sdk": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/wikibase-sdk/-/wikibase-sdk-8.1.1.tgz", + "integrity": "sha512-1NjMnfNQ4OaLh0dFAeTMvV3vGAq6HXsNKGfYUJYOVyBPGBDMunlY3QZ8+72hLV5FiKmc6Bzg1xbI0jCHfHmIew==", + "engines": { + "node": ">= 10.0.0" + } + } + } +} diff --git a/questionservice/package.json b/questionservice/package.json new file mode 100644 index 0000000..b6071ef --- /dev/null +++ b/questionservice/package.json @@ -0,0 +1,10 @@ +{ + "scripts": { + "start": "node question-service.js", + "test": "jest" + }, + "dependencies": { + "express": "^4.18.3", + "wikibase-sdk": "^8.1.1" + } +} diff --git a/questionservice/question-service.js b/questionservice/question-service.js new file mode 100644 index 0000000..9c7d276 --- /dev/null +++ b/questionservice/question-service.js @@ -0,0 +1,126 @@ +const WBK = require('wikibase-sdk') +const wbk = WBK({ + instance: 'https://www.wikidata.org', + sparqlEndpoint: 'https://query.wikidata.org/sparql' // Required to use `sparqlQuery` and `getReverseClaims` functions, optional otherwise +}) +const express = require('express'); +const app = express(); +const port = 8010; + +app.use(express.static('public')); + +app.use(express.text()); + +//Correct image +var correctAnswerFlag +//Associates flags with their countries +var flagToCountryMap = new Map() + +class WIQ_API{ + /** + * + * @returns JSON with the question and the flags + */ + async getQuestionAndCountryFlags() { + //Reset the map for the new question + flagToCountryMap = new Map() + + //Num of fetched countries + const countriesNum = 100 + + //Required by wikidata to accept the request + const headers = new Headers(); + headers.append('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + +' AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'); + + const sparql = `SELECT ?país ?paísLabel ?imagen_de_la_bandera WHERE { + SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } + ?país wdt:P31 wd:Q6256. + OPTIONAL { ?país wdt:P41 ?imagen_de_la_bandera. } + } + LIMIT ${countriesNum}` + + //Constructing the url for the wikidata request + var url = wbk.sparqlQuery(sparql); + + const response = await fetch(url, { headers }); + const data = await response.json() + + var chosenNums = []; + const numOfChosen = 4 + // Generate n random numbers + for (let i = 0; i < numOfChosen; i++) { + this.#getRandomNumNotInSetAndUpdate(countriesNum, chosenNums) + } + + const countries = [] + const imgs = [] + for(var i=0;i { + const question = await wiq.getQuestionAndCountryFlags() + res.json(question); +}); + +/** + * Gets a response indicating if the chosen flag img was correct or not + * @param {string} req - Flag img url selected by the player + * @param {Object} res - JSON containing whether the answer was correct "true" + * or not "false". In case it was incorrect, the chosen + * country will be returned as well +*/ +app.get('/flags/answer', (req, res) => { + const answeredFlag = req.body + if(correctAnswerFlag==answeredFlag){ + res.json({ + correct: "true" + }) + } else { + res.json({ + correct: "false", + country: `${flagToCountryMap.get(answeredFlag)}` + }) + } +}); + +app.listen(port, () => { + console.log(`Questions service listening on http://localhost:${port}`); +}); \ No newline at end of file diff --git a/users/authservice/auth-service.js b/users/authservice/auth-service.js index 9764f08..05673de 100644 --- a/users/authservice/auth-service.js +++ b/users/authservice/auth-service.js @@ -2,7 +2,7 @@ const express = require('express'); const mongoose = require('mongoose'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); -const User = require('./auth-model') +const User = require('./auth-model'); const app = express(); const port = 8002; @@ -39,7 +39,7 @@ app.post('/login', async (req, res) => { // Generate a JWT token const token = jwt.sign({ userId: user._id }, 'your-secret-key', { expiresIn: '1h' }); // Respond with the token and user information - res.json({ token: token, username: username, createdAt: user.createdAt }); + res.status(200).json({ token: token }); } else { res.status(401).json({ error: 'Invalid credentials' }); } @@ -53,6 +53,30 @@ const server = app.listen(port, () => { console.log(`Auth Service listening at http://localhost:${port}`); }); +app.get('/self', async (req, res) => { + try { + + const token = req.headers.authorization; + let id = {}; + if (!token) { + return res.status(403).json({ error: 'No token provided' }); + } + jwt.verify(token, 'your-secret-key', (err, decoded) => { + if (err) { + return res.status(500).json({ error: 'Failed to authenticate token' }); + } + + // If everything is good, save the user id to request for use in other routes + id = decoded.userId; + + }); + const user = await User.findById(id); + res.status(200).json(user); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + server.on('close', () => { // Close the Mongoose connection mongoose.connection.close(); diff --git a/users/authservice/package-lock.json b/users/authservice/package-lock.json index e0ceb0b..978a817 100644 --- a/users/authservice/package-lock.json +++ b/users/authservice/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "bcrypt": "^5.1.1", "body-parser": "^1.20.2", + "cookie-parser": "^1.4.6", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.4" @@ -1898,6 +1899,26 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/users/authservice/package.json b/users/authservice/package.json index 23f1c02..e9d7694 100644 --- a/users/authservice/package.json +++ b/users/authservice/package.json @@ -20,6 +20,7 @@ "dependencies": { "bcrypt": "^5.1.1", "body-parser": "^1.20.2", + "cookie-parser": "^1.4.6", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.4" diff --git a/users/userservice/user-model.js b/users/userservice/user-model.js index 71d81b5..328601b 100644 --- a/users/userservice/user-model.js +++ b/users/userservice/user-model.js @@ -13,6 +13,13 @@ const userSchema = new mongoose.Schema({ type: Date, default: Date.now, }, + points: { + type: Number, + default: function() { + // Generate a random integer between 0 and 100 + return Math.floor(Math.random() * 101); + } + } }); const User = mongoose.model('User', userSchema); diff --git a/users/userservice/user-service.js b/users/userservice/user-service.js index be95842..c169099 100644 --- a/users/userservice/user-service.js +++ b/users/userservice/user-service.js @@ -3,6 +3,7 @@ const express = require('express'); const mongoose = require('mongoose'); const bcrypt = require('bcrypt'); const bodyParser = require('body-parser'); + const User = require('./user-model') const app = express(); @@ -26,6 +27,35 @@ function validateRequiredFields(req, requiredFields) { } } +// Function to get the user's ranking data +async function getRankingFor(loggedUser) { + const users = await User.find().sort({points: -1}) + const ranking = users.indexOf( (user) => user._id == loggedUser._id) + + return { ranking: ranking, points: loggedUser.points, user: loggedUser.username } +} + +app.get('/rankings', async (req, res) => { + try { + /* const { token } = req.cookies + const decoded = jwt.verify(token, 'your-secret-key') + const userId = decoded.userId + const loggedUser = await User.findById(userId) + const userRanking = getRankingFor(loggedUser) */ + const usersRanking = (await User.find().sort({points: -1})).map( (user, index) => { + return { + ranking: index+1, + points: user.points, + user: user.username } + }) + + //res.json(userRanking, usersRanking) + res.json(usersRanking) + } catch (error) { + res.status(400).json({ error: error.message }); + } +}) + app.post('/adduser', async (req, res) => { try { // Check if required fields are present in the request body @@ -49,6 +79,8 @@ const server = app.listen(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 diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 27466ae..380820a 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -14,10 +14,14 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.2", + "autoprefixer": "^10.4.18", "axios": "^1.6.5", + "postcss": "^8.4.35", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.22.3", "react-scripts": "5.0.1", + "tailwindcss": "^3.4.1", "web-vitals": "^3.5.1" }, "devDependencies": { @@ -5005,6 +5009,14 @@ "node": ">=12" } }, + "node_modules/@remix-run/router": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", + "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -6931,9 +6943,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", "funding": [ { "type": "opencollective", @@ -6949,9 +6961,9 @@ } ], "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001591", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -7633,9 +7645,9 @@ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" }, "node_modules/browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "funding": [ { "type": "opencollective", @@ -7651,8 +7663,8 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, @@ -7818,9 +7830,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001576", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz", - "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==", + "version": "1.0.30001596", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001596.tgz", + "integrity": "sha512-zpkZ+kEr6We7w63ORkoJ2pOfBwBkY/bJrG/UZ90qNb45Isblu8wzDgevEOrRL1r9dWayHjYiiyCMEXPn4DweGQ==", "funding": [ { "type": "opencollective", @@ -9544,9 +9556,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.623", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.623.tgz", - "integrity": "sha512-lKoz10iCYlP1WtRYdh5MvocQPWVRoI7ysp6qf18bmeBgR8abE6+I2CsfyNKztRDZvhdWc+krKT6wS7Neg8sw3A==" + "version": "1.4.699", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.699.tgz", + "integrity": "sha512-I7q3BbQi6e4tJJN5CRcyvxhK0iJb34TV8eJQcgh+fR2fQ8miMgZcEInckCo1U9exDHbfz7DLDnFn8oqH/VcRKw==" }, "node_modules/emittery": { "version": "0.13.1", @@ -20153,9 +20165,9 @@ } }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "funding": [ { "type": "opencollective", @@ -22009,6 +22021,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", + "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "dependencies": { + "@remix-run/router": "1.15.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", + "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "dependencies": { + "@remix-run/router": "1.15.3", + "react-router": "6.22.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", diff --git a/webapp/package.json b/webapp/package.json index 74e31be..ca6979f 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -9,10 +9,14 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.2", + "autoprefixer": "^10.4.18", "axios": "^1.6.5", + "postcss": "^8.4.35", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.22.3", "react-scripts": "5.0.1", + "tailwindcss": "^3.4.1", "web-vitals": "^3.5.1" }, "scripts": { diff --git a/webapp/src/App.js b/webapp/src/App.js index d932005..c8e8a08 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -1,23 +1,54 @@ import React, { useState } from 'react'; +import {BrowserRouter, Routes, Route} from 'react-router-dom' import AddUser from './components/AddUser'; +import Navbar from './components/Navbar'; import Login from './components/Login'; +import Rankings from './components/Rankings'; +import Game from './components/Game'; import CssBaseline from '@mui/material/CssBaseline'; import Container from '@mui/material/Container'; import Typography from '@mui/material/Typography'; import Link from '@mui/material/Link'; - +import { useEffect } from 'react'; +import axios from 'axios'; function App() { const [showLogin, setShowLogin] = useState(true); - + const [user, setUser] = useState({}); + const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; const handleToggleView = () => { setShowLogin(!showLogin); }; - + const checkUser = () => { + const token = localStorage.getItem('uToken'); + if (token) { + axios.get(`${apiEndpoint}/self`, { + headers: { + Authorization: token, + }, + }).then((response) => { + setUser(response.data); + }); + } + }; return ( - + + + + Home page}/> + } /> + } /> + } /> + } /> + + + + + + + /* - Welcome to the 2024 edition of the Software Architecture course + Welcome to the 2024 edition of the Software Architecture course hola {showLogin ? : } @@ -31,8 +62,8 @@ function App() { )} - - ); + */ + ) } export default App; diff --git a/webapp/src/components/Game.js b/webapp/src/components/Game.js new file mode 100644 index 0000000..ca8ab32 --- /dev/null +++ b/webapp/src/components/Game.js @@ -0,0 +1,43 @@ +// src/components/Game.js +import React, { useState } from 'react'; +import axios from 'axios'; +import { Container, Button} from '@mui/material'; + +const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; + +const Game = () => { + const [error, setError] = useState(''); + const [question, setQuestion] = useState(''); + const [isPlaying, setIsPlaying] = useState(false); + + const updateQuestion = async (chosenFlag) => { + try { + const question = await axios.get(`${apiEndpoint}/flags/question`, {}); + setQuestion(question.data); + } catch (error) { + setError(error.response.data.error); + } + }; + + return ( + + {!isPlaying?( + + ) : ( + <> +

{question.question}

+
+ + + + +
+ + )} +
+ ); +}; + +export default Game; \ No newline at end of file diff --git a/webapp/src/components/Login.js b/webapp/src/components/Login.js index 0ad6268..80ff0cd 100644 --- a/webapp/src/components/Login.js +++ b/webapp/src/components/Login.js @@ -3,12 +3,13 @@ import React, { useState } from 'react'; import axios from 'axios'; import { Container, Typography, TextField, Button, Snackbar } from '@mui/material'; + const Login = () => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [loginSuccess, setLoginSuccess] = useState(false); - const [createdAt, setCreatedAt] = useState(''); + const [openSnackbar, setOpenSnackbar] = useState(false); const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; @@ -18,11 +19,9 @@ const Login = () => { const response = await axios.post(`${apiEndpoint}/login`, { username, password }); // Extract data from the response - const { createdAt: userCreatedAt } = response.data; - - setCreatedAt(userCreatedAt); + + localStorage.setItem('uToken', response.data.token); setLoginSuccess(true); - setOpenSnackbar(true); } catch (error) { setError(error.response.data.error); @@ -38,11 +37,9 @@ const Login = () => { {loginSuccess ? (
- Hello {username}! - - - Your account was created on {new Date(createdAt).toLocaleDateString()}. + {localStorage.getItem('user')} +
) : (
diff --git a/webapp/src/components/Navbar.jsx b/webapp/src/components/Navbar.jsx new file mode 100644 index 0000000..19e0a91 --- /dev/null +++ b/webapp/src/components/Navbar.jsx @@ -0,0 +1,21 @@ +function Navbar() { + + return ( +
+
+ WIQ +
+ +
+ ) +} + +export default Navbar \ No newline at end of file diff --git a/webapp/src/components/Rankings.jsx b/webapp/src/components/Rankings.jsx new file mode 100644 index 0000000..4e3ee4c --- /dev/null +++ b/webapp/src/components/Rankings.jsx @@ -0,0 +1,42 @@ +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; + +const apiEndpoint = 'http://localhost:8000'; + +const Rankings = () => { + const [users, setUsers] = useState([]); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await axios.get(`${apiEndpoint}/rankings`); + setUsers(response.data); + } catch (error) { + console.error('Error fetching data:', error); + } + }; + + fetchData(); + }, []); + + return ( +
+
+

Rankings

+
    + {users.map(user => ( +
  • +
    + {user.ranking} + {user.user} +
    + {user.points} points +
  • + ))} +
+
+
+ ) +} + +export default Rankings \ No newline at end of file diff --git a/webapp/src/index.css b/webapp/src/index.css index ec2585e..17df0e7 100644 --- a/webapp/src/index.css +++ b/webapp/src/index.css @@ -1,3 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', diff --git a/webapp/src/index.js b/webapp/src/index.js index d563c0f..ec6a487 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -5,6 +5,8 @@ import App from './App'; import reportWebVitals from './reportWebVitals'; const root = ReactDOM.createRoot(document.getElementById('root')); + + root.render( diff --git a/webapp/tailwind.config.js b/webapp/tailwind.config.js new file mode 100644 index 0000000..4049afe --- /dev/null +++ b/webapp/tailwind.config.js @@ -0,0 +1,10 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./index.html", + "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +} +