diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 74f4a99e..cef2a143 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -76,6 +76,8 @@ jobs:
- uses: actions/checkout@v4
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@v5
+ env:
+ JWT_KEY: ${{secrets.JWT_KEY}}
with:
name: arquisoft/wiq_en1b/authservice
username: ${{ github.actor }}
diff --git a/README.md b/README.md
index 5575e13d..3d262ac9 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ docker compose --profile dev up --build
```
### Deployed in Cloud
-In order to view the application deploy in the cloud click [here](http://172.203.216.60:3000)
+In order to view the application deploy in the cloud click [here](http://wiqen1b.serveminecraft.net:3000)
### Members
- Lucía Ruiz Núñez uo289267@uniovi.es
diff --git a/docker-compose.yml b/docker-compose.yml
index 26949792..89ffbe46 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -113,7 +113,7 @@
prometheus:
image: prom/prometheus
container_name: prometheus-${teamname:-defaultASW}
- profiles: ["dev"]
+ profiles: ["dev", "prod"]
networks:
- mynetwork
volumes:
@@ -127,7 +127,7 @@
grafana:
image: grafana/grafana
container_name: grafana-${teamname:-defaultASW}
- profiles: ["dev"]
+ profiles: ["dev", "prod"]
networks:
- mynetwork
volumes:
diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js
index 009db707..c08d0e37 100644
--- a/gatewayservice/gateway-service.js
+++ b/gatewayservice/gateway-service.js
@@ -6,7 +6,7 @@ const promBundle = require('express-prom-bundle');
const swaggerUi = require('swagger-ui-express');
const fs = require("fs")
const YAML = require('yaml')
-
+const jwt = require('jsonwebtoken');
const app = express();
const port = 8000;
@@ -33,7 +33,7 @@ app.post('/login', async (req, res) => {
const authResponse = await axios.post(authServiceUrl+'/login', req.body);
res.json(authResponse.data);
} catch (error) {
- res.status(error.response.status).json({ error: error.response.data.error });
+ manageError(error)
}
});
@@ -43,52 +43,130 @@ app.post('/adduser', async (req, res) => {
const userResponse = await axios.post(userServiceUrl+'/adduser', req.body);
res.json(userResponse.data);
} catch (error) {
- res.status(error.response.status).json({ error: error.response.data.error });
+ manageError(error);
+
}
});
-app.get('/questions', async (req, res) => {
+app.get('/questions', verifyToken, async (req, res) => {
try {
// Forward the question request to the quetion service
const questionResponse = await axios.get(questionServiceUrl+'/questions');
res.json(questionResponse.data);
} catch (error) {
- res.status(error.response.status).json({ error: error.response.data.error });
+ manageError(error)
}
});
-app.get('/questions/:lang', async (req, res) => {
+
+
+app.get('/questions/:lang/:amount/:type', verifyToken, async (req, res) => {
try {
- const lang = req.params.lang;
- // Forward the question request to the quetion service
- const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang);
+ if(!validateLang(req.params.lang.toString()) ||
+ !validateAmount(req.params.amount.toString()) ||
+ !validateType(req.params.type.toString()))
+ res.status(400).json({ error: 'Wrong values given' });
+ else {
+ const lang = encodeURIComponent(req.params.lang.toString());
+ const amount = encodeURIComponent(req.params.amount.toString());
+ const type = encodeURIComponent(req.params.type.toString());
+ // Forward the question request to the quetion service
+ const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang + '/' + amount + '/' + type);
+
+ res.json(questionResponse.data);
+ }
+ } catch (error) {
- res.json(questionResponse.data);
+ manageError(error)
+ }
+});
+
+
+app.get('/questions/:lang/:amount', verifyToken, async (req, res) => {
+ try {
+ if(!validateLang(req.params.lang.toString()) ||
+ !validateAmount(req.params.amount.toString()))
+ res.status(400).json({ error: 'Wrong values given' });
+ else{
+ const lang = encodeURIComponent(req.params.lang.toString());
+ const amount = encodeURIComponent(req.params.amount.toString());
+ // Forward the question request to the quetion service
+ const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang + '/' + amount);
+
+ res.json(questionResponse.data);
+ }
} catch (error) {
+ manageError(error)
+ }
+});
- res.status(error.response.status).json({ error: error.response.data.error });
+app.get('/questions/:lang', verifyToken, async (req, res) => {
+ try {
+ if(!validateLang(req.params.lang.toString()))
+ res.status(400).json({ error: 'Wrong values given' });
+ else{
+ const lang = encodeURIComponent(req.params.lang.toString());
+ // Forward the question request to the quetion service
+ const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang.toString());
+
+ res.json(questionResponse.data);
+ }
+
+ } catch (error) {
+
+ manageError(error)
}
});
-app.post('/record', async(req, res) => {
+app.post('/record', verifyToken, async(req, res) => {
+
try {
// Forward the record request to the record service
const recordResponse = await axios.post(recordServiceUrl+'/record', req.body);
res.json(recordResponse.data);
} catch (error) {
- res.send(error);
+ manageError(error)
}
});
-app.get('/record/:user', async(req, res)=>{
+app.get('/record/ranking/top10', verifyToken, async(req, res)=>{
try {
- const user = req.params.user;
// Forward the record request to the record service
- const recordResponse = await axios.get(recordServiceUrl + '/record/' + user);
+ const recordResponse = await axios.get(recordServiceUrl + '/record/ranking/top10');
res.json(recordResponse.data);
} catch (error) {
- res.send(error);
+ manageError(error)
+ }
+});
+
+app.get('/record/ranking/:user', verifyToken, async(req, res)=>{
+ try {
+ if(!validateUser(req.params.user.toString()))
+ res.status(400).json({ error: 'Wrong values given' });
+ else{
+ const user = encodeURIComponent(req.params.user.toString());
+ // Forward the record request to the record service
+ const recordResponse = await axios.get(recordServiceUrl + '/record/ranking/' + user);
+ res.json(recordResponse.data);
+ }
+ } catch (error) {
+ manageError(error)
+ }
+});
+
+app.get('/record/:user', verifyToken, async(req, res)=>{
+ try {
+ if(!validateUser(req.params.user.toString()))
+ res.status(400).json({ error: 'Wrong values given' });
+ else{
+ const user = encodeURIComponent(req.params.user.toString());
+ // Forward the record request to the record service
+ const recordResponse = await axios.get(recordServiceUrl + '/record/' + user);
+ res.json(recordResponse.data);
+ }
+ } catch (error) {
+ manageError(error)
}
});
@@ -108,4 +186,48 @@ const server = app.listen(port, () => {
console.log(`Gateway Service listening at http://localhost:${port}`);
});
+function verifyToken(req, res, next) {
+ // Get the token from the request headers
+ const token = req.headers['token'] || req.body.token || req.query.token;
+
+ // Verify if the token is valid
+ jwt.verify(token, (process.env.JWT_KEY??'my-key'), (err, decoded) => {
+ if (err) {
+ // Token is not valid
+ res.status(403).json({authorized: false,
+ error: 'Invalid token or outdated'});
+ } else {
+ // Token is valid
+ req.decodedToken = decoded;
+ // Call next() to proceed to the next middleware or route handler
+ next();
+ }
+ });
+}
+
+function validateLang(lang){
+ return ['en', 'es', 'tk'].includes(lang);
+}
+
+function validateAmount(amount) {
+ const parsed = parseInt(amount, 10);
+ // We only accept integers and positive ones
+ return !isNaN(parsed) && parsed > 0;
+}
+
+function validateType(type){
+ return ['POPULATION', 'CAPITAL', 'LANGUAGE', 'SIZE'].includes(type);
+}
+
+function validateUser(user){
+ return !(/\s/.test(user)) //True if there are no spaces
+}
+
+function manageError(error){
+ if(error.response) //Some microservice responded with an error
+ res.status(error.response.status).json({ error: error.response.data.error });
+ else //Some other error
+ res.status(500).json({error : "Interanl server error"})
+}
+
module.exports = server
diff --git a/gatewayservice/gateway-service.test.js b/gatewayservice/gateway-service.test.js
index bfed664f..550c4024 100644
--- a/gatewayservice/gateway-service.test.js
+++ b/gatewayservice/gateway-service.test.js
@@ -1,14 +1,21 @@
const request = require('supertest');
const axios = require('axios');
+const jwt = require('jsonwebtoken');
const app = require('./gateway-service');
afterAll(async () => {
app.close();
});
+
+jest.mock('jsonwebtoken');
+
jest.mock('axios');
-describe('Gateway Service', () => {
+
+
+describe('Gateway Service with token mock', () => {
+
// Mock responses from external services
axios.post.mockImplementation((url, data) => {
if (url.endsWith('/login')) {
@@ -20,19 +27,39 @@ describe('Gateway Service', () => {
}
});
+ const question = { data: [{question: "¿Cuál es la población de Oviedo?",
+ answers: ["225089","272357","267855","231841"]}] };
+
+ //Dont need to check a good record just that it redirects the call
+ const record = {data : {record:'undefined'}};
+
axios.get.mockImplementation((url, data) => {
if (url.endsWith('/questions')){
- return Promise.resolve({ data: [{question: "¿Cuál es la población de Oviedo?",
- answers: ["225089","272357","267855","231841"]}] });
+ return Promise.resolve(question);
+ } else if (url.endsWith('/questions/es/1/CAPITAL')){
+ return Promise.resolve(question);
+ } else if (url.endsWith('/questions/es/1')){
+ return Promise.resolve(question);
} else if (url.endsWith('/questions/es')){
- return Promise.resolve({ data: [{question: "¿Cuál es la población de Oviedo?",
- answers: ["225089","272357","267855","231841"]}] });
+ return Promise.resolve(question);
+
} else if(url.endsWith('/record/testuser')){
- //Dont need to check a good record just that it redirects the call
- return Promise.resolve({data : {record:'undefined'}})
+ return Promise.resolve(record)
+ } else if(url.endsWith('/record/ranking/top10')){
+ return Promise.resolve(record)
+ } else if(url.endsWith('/record/ranking/testuser')){
+ return Promise.resolve(record)
}
});
+
+
+ // Mock the `verify` function of JWT
+ jwt.verify.mockImplementation((token, secretOrPublicKey, callback) => {
+ // Assume the token is valid and return the payload
+ callback(null, "decoded");
+ });
+
// Test /login endpoint
it('should forward login request to auth service', async () => {
const response = await request(app)
@@ -57,25 +84,39 @@ describe('Gateway Service', () => {
// Test /questions endpoint
it('should forward questions request to question service', async () => {
const response = await request(app)
- .get('/questions');
+ .get('/questions').set('token', 'valorDelToken');
- expect(response.statusCode).toBe(200);
- expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ checkQuestion(response);
});
// Test /questions/:lang endpoint
it('should forward questions request to question service', async () => {
const response = await request(app)
- .get('/questions/es');
+ .get('/questions/es').set('token', 'valorDelToken');
- expect(response.statusCode).toBe(200);
- expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ checkQuestion(response);
+ });
+
+ // Test /questions/:lang/:amount endpoint
+ it('should forward questions request to question service', async () => {
+ const response = await request(app)
+ .get('/questions/es/1').set('token', 'valorDelToken');
+
+ checkQuestion(response);
+ });
+
+ // Test /questions/:lang/:amount/:type endpoint
+ it('should forward questions request to question service', async () => {
+ const response = await request(app)
+ .get('/questions/es/1/CAPITAL').set('token', 'valorDelToken');
+
+ checkQuestion(response);
});
// Test /record endpoint
it('should forward record request to record service', async () => {
const response = await request(app)
- .post('/record');
+ .post('/record').set('token', 'valorDelToken');
expect(response.statusCode).toBe(200);
expect(response.body.user).toBe('testuser');
@@ -84,9 +125,35 @@ describe('Gateway Service', () => {
// Test /record/:user endpoint
it('should forward record request to record service', async () => {
const response = await request(app)
- .get('/record/testuser');
+ .get('/record/testuser').set('token', 'valorDelToken');
- expect(response.statusCode).toBe(200);
- expect(response.body).toHaveProperty('record', "undefined");
+ checkRecord(response);
+ });
+
+ // Test /record/ranking/:user endpoint
+ it('should forward record request to record service', async () => {
+ const response = await request(app)
+ .get('/record/ranking/testuser').set('token', 'valorDelToken');
+
+ checkRecord(response);
});
-});
\ No newline at end of file
+
+ // Test /record/ranking/top10 endpoint
+ it('should forward record request to record service', async () => {
+ const response = await request(app)
+ .get('/record/ranking/top10').set('token', 'valorDelToken');
+ checkRecord(response);
+
+ });
+
+});
+
+function checkRecord(response){
+ expect(response.statusCode).toBe(200);
+ expect(response.body).toHaveProperty('record', "undefined");
+}
+
+function checkQuestion(response){
+ expect(response.statusCode).toBe(200);
+ expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+}
\ No newline at end of file
diff --git a/gatewayservice/openapi.yaml b/gatewayservice/openapi.yaml
index 7129380e..47615d2a 100644
--- a/gatewayservice/openapi.yaml
+++ b/gatewayservice/openapi.yaml
@@ -9,7 +9,7 @@ info:
servers:
- url: http://localhost:8000
description: Development server
- - url: http://172.203.216.60:8000
+ - url: http://wiqen1b.serveminecraft.net:8000
description: Production server
paths:
/adduser:
diff --git a/gatewayservice/package-lock.json b/gatewayservice/package-lock.json
index ca71293c..48729129 100644
--- a/gatewayservice/package-lock.json
+++ b/gatewayservice/package-lock.json
@@ -14,6 +14,7 @@
"express": "^4.18.2",
"express-openapi": "^12.1.3",
"express-prom-bundle": "^7.0.0",
+ "jsonwebtoken": "^9.0.2",
"swagger-ui-express": "^5.0.0",
"yaml": "^2.4.1"
},
@@ -1600,6 +1601,11 @@
"node-int64": "^0.4.0"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -1980,6 +1986,14 @@
"esprima": "^4.0.0"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -3438,6 +3452,81 @@
"node": ">=6"
}
},
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jsonwebtoken/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/jsonwebtoken/node_modules/semver": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -3474,11 +3563,46 @@
"node": ">=8"
}
},
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
+ },
"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/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
diff --git a/gatewayservice/package.json b/gatewayservice/package.json
index 93a9914e..0757ca8f 100644
--- a/gatewayservice/package.json
+++ b/gatewayservice/package.json
@@ -22,6 +22,7 @@
"cors": "^2.8.5",
"express": "^4.18.2",
"express-openapi": "^12.1.3",
+ "jsonwebtoken": "^9.0.2",
"express-prom-bundle": "^7.0.0",
"swagger-ui-express": "^5.0.0",
"yaml": "^2.4.1"
diff --git a/questionservice/question-service.js b/questionservice/question-service.js
index 026feb5c..77377c74 100644
--- a/questionservice/question-service.js
+++ b/questionservice/question-service.js
@@ -30,12 +30,66 @@ app.get('/questions', async (req, res) => {
}
});
-app.get('/questions/:lang', async (req, res) => {
+app.get('/questions/:lang/:amount/:type', async (req, res) => {
+ try {
+ const lang = req.params.lang.toString();
+ let amount = checkAmount(parseInt(req.params.amount));
+ const type = req.params.type.toString();
+
+ if(amount > 20 || amount < 1)
+ amount = 5;
+
+ const questions = await Question.aggregate([
+ {$match: {language : lang, type: type}}, //Condition
+ {$sample: {size:amount}}
+ ]);
+
+ let jsonResult = {};
+ for (let i = 0; i < questions.length; i++) {
+ const question = questions[i];
+ jsonResult[i] = {
+ question : question.question,
+ answers : question.answers
+ }
+ }
+ res.json(jsonResult);
+ } catch (error) {
+ res.status(500).json({ error: 'Internal Server Error' });
+ }
+});
+
+app.get('/questions/:lang/:amount', async (req, res) => {
try {
const lang = req.params.lang;
+ let amount = checkAmount(parseInt(req.params.amount));
+
+
const questions = await Question.aggregate([
{$match: {language : lang}}, //Condition
+ {$sample: {size:amount}}
+ ]);
+
+ let jsonResult = {};
+ for (let i = 0; i < questions.length; i++) {
+ const question = questions[i];
+ jsonResult[i] = {
+ question : question.question,
+ answers : question.answers
+ }
+ }
+ res.json(jsonResult);
+ } catch (error) {
+ res.status(500).json({ error: 'Internal Server Error' });
+ }
+});
+
+app.get('/questions/:lang', async (req, res) => {
+ try {
+ const lang = req.params.lang;
+
+ const questions = await Question.aggregate([
+ {$match: {language : lang.toString()}}, //Condition
{$sample: {size:5}} //5 random from the ones that fullfil the condition
]);
@@ -53,6 +107,11 @@ app.get('/questions/:lang', async (req, res) => {
}
});
+function checkAmount(amount){
+ if(amount > 20 || amount < 1)
+ return 5;
+ return amount;
+}
const server = app.listen(port, () => {
console.log(`Question Service listening at http://localhost:${port}`);
diff --git a/questionservice/question-service.test.js b/questionservice/question-service.test.js
index 67b5ca66..35b70eae 100644
--- a/questionservice/question-service.test.js
+++ b/questionservice/question-service.test.js
@@ -12,7 +12,7 @@ beforeAll(async () => {
app = require('./question-service');
//Populate db
- for(let i = 0; i < 6 ; i++){
+ for(let i = 0; i < 21 ; i++){
const question = new Question( {
question: "¿Cuál es la población de Oviedo?",
answers: [
@@ -60,4 +60,52 @@ describe('Question Service', () => {
expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
expect(Object.keys(response.body).length).toBe(5);
});
+
+
+ it('Should give 20 questions /questions/es/20', async () => {
+
+ let response = await request(app).get('/questions/es/20');
+ expect(response.status).toBe(200);
+ expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ expect(Object.keys(response.body).length).toBe(20);
+ });
+
+ it('Should give 1 questions /questions/es/1', async () => {
+
+ let response = await request(app).get('/questions/es/20');
+ expect(response.status).toBe(200);
+ expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ expect(Object.keys(response.body).length).toBe(20);
+ });
+
+ it('Should give 5 questions as the max is 20 /questions/es/21', async () => {
+
+ let response = await request(app).get('/questions/es/21');
+ expect(response.status).toBe(200);
+ expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ expect(Object.keys(response.body).length).toBe(5);
+ });
+
+ it('Should give 5 questions as the min is 1 /questions/es/0', async () => {
+
+ let response = await request(app).get('/questions/es/0');
+ expect(response.status).toBe(200);
+ expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ expect(Object.keys(response.body).length).toBe(5);
+ });
+
+ it('Should give 10 questions /questions/es/10/POPULATION', async () => {
+
+ let response = await request(app).get('/questions/es/10/POPULATION');
+ expect(response.status).toBe(200);
+ expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ expect(Object.keys(response.body).length).toBe(10);
+ });
+
+ it('Should give 0 questions /questions/es/10/CAPITAL', async () => {
+
+ let response = await request(app).get('/questions/es/10/CAPITAL');
+ expect(response.status).toBe(200);
+ expect(Object.keys(response.body).length).toBe(0);
+ });
});
\ No newline at end of file
diff --git a/sonar-project.properties b/sonar-project.properties
index 6db6137a..fad83281 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -14,4 +14,5 @@ sonar.coverage.exclusions=**/*.test.js
sonar.sources=webapp/src/components,users/authservice,users/userservice,gatewayservice
sonar.sourceEncoding=UTF-8
sonar.exclusions=node_modules/**
-sonar.javascript.lcov.reportPaths=**/coverage/lcov.info
\ No newline at end of file
+sonar.javascript.lcov.reportPaths=**/coverage/lcov.info
+sonar.cpd.exclusions=**/*.test.js,**/*steps.js,**/*Tests.java
diff --git a/users/authservice/auth-model.js b/users/authservice/auth-model.js
index 7763b51e..5cdbaf69 100644
--- a/users/authservice/auth-model.js
+++ b/users/authservice/auth-model.js
@@ -1,6 +1,7 @@
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
+ email: String,
username: String,
password: String,
createdAt: Date,
diff --git a/users/authservice/auth-service.js b/users/authservice/auth-service.js
index 98779572..924dc512 100644
--- a/users/authservice/auth-service.js
+++ b/users/authservice/auth-service.js
@@ -35,17 +35,20 @@ app.post('/login', async (req, res) => {
return
}
- const { username, password } = req.body;
+ const email = req.body.username.toString();
+ const username = req.body.username.toString();
+ const password = req.body.password.toString();
- // Find the user by username in the database
- const user = await User.findOne({ username });
+ let user = await User.findOne({ username })
+ if(!user) //There is no user by that username we may have received an email
+ user = await User.findOne({ email })
// Check if the user exists and verify the password
if (user && await bcrypt.compare(password, user.password)) {
// Generate a JWT token
- const token = jwt.sign({ userId: user._id }, 'your-secret-key', { expiresIn: '1h' });
+ const token = jwt.sign({ userId: user._id }, (process.env.JWT_KEY??'my-key'), { expiresIn: '1h' });
// Respond with the token and user information
- res.json({ token: token, username: username});
+ res.json({ token: token, username: user.username, email: user.email});
} else {
res.status(400).json({ error: 'Invalid credentials' });
}
diff --git a/users/authservice/auth-service.test.js b/users/authservice/auth-service.test.js
index f4a5a82e..7f258f5c 100644
--- a/users/authservice/auth-service.test.js
+++ b/users/authservice/auth-service.test.js
@@ -7,7 +7,7 @@ let mongoServer;
let app;
//test user
-const user = {
+let user = {
username: 'testuser',
password: 'testpassword',
};
@@ -15,6 +15,7 @@ const user = {
async function addUser(user){
const hashedPassword = await bcrypt.hash(user.password, 10);
const newUser = new User({
+ email: "user@gmail.com",
username: user.username,
password: hashedPassword,
createdAt: new Date()
@@ -44,7 +45,14 @@ describe('Auth Service', () => {
expect(response.body).toHaveProperty('username', 'testuser');
});
- it('Should show missing field user /login', async () => {
+ it('Should perform a login operation with email /login', async () => {
+ user.username = "user@gmail.com";
+ const response = await request(app).post('/login').send(user);
+ expect(response.status).toBe(200);
+ expect(response.body).toHaveProperty('username', 'testuser');
+ });
+
+ it('Should show missing field username /login', async () => {
const response = await request(app).post('/login').send();
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('error', 'Missing required field: username');
diff --git a/users/recordservice/record-model.js b/users/recordservice/record-model.js
index a7a30d76..a302df52 100644
--- a/users/recordservice/record-model.js
+++ b/users/recordservice/record-model.js
@@ -1,16 +1,18 @@
const mongoose = require('mongoose');
+const { Schema } = mongoose;
-const recordSchema = new mongoose.Schema({
- user: String,
+const recordSchema = new Schema({
+ user: { type: String, required: true },
games: [{
questions: [{
- question: String,
- answers: [String],
- answerGiven: String,
- correctAnswer: String
+ question: { type: String, required: true },
+ answers: { type: [String], required: true },
+ answerGiven: { type: String, required: true },
+ correctAnswer: { type: String, required: true }
}],
- points: Number,
- date: String
+ points: { type: Number, required: true },
+ date: { type: String, required: true },
+ competitive: { type: Boolean, required: true }
}]
});
const Record = mongoose.model('Record', recordSchema);
diff --git a/users/recordservice/record-service.js b/users/recordservice/record-service.js
index dd383984..3bca1fa8 100644
--- a/users/recordservice/record-service.js
+++ b/users/recordservice/record-service.js
@@ -1,8 +1,7 @@
const express = require('express');
const mongoose = require('mongoose');
-const bcrypt = require('bcrypt');
const bodyParser = require('body-parser');
-const Record = require('./record-model')
+const Record = require('./record-model');
const app = express();
const port = 8004;
@@ -10,6 +9,10 @@ const port = 8004;
// Middleware to parse JSON in request body
app.use(bodyParser.json());
+var ranking = [];
+var lastTime = new Date();
+const minTimeDifferenceInMiliseconds = 120_000;
+
// Connect to MongoDB
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/userdb';
mongoose.connect(mongoUri);
@@ -18,14 +21,12 @@ mongoose.connect(mongoUri);
app.post('/record', async (req, res) => {
const user = req.body.user;
const game = req.body.game;
- console.log(user)
- console.log(game)
if(user && game){
- let record = await Record.findOne({ user : user });
+ let record = await Record.findOne({ user : user.toString() });
if(record){ //If it exits
record.games.push(game);
}
- else{ //Lo creamos
+ else{ //We make it
record = new Record({
user:user,
games:[game]
@@ -45,9 +46,34 @@ app.post('/record', async (req, res) => {
});
+app.get('/record/ranking/top10', async (req, res) => {
+ try {
+ let usersRanking = await getRanking();
+ let usersCompetitiveStats = usersRanking.slice(0, 10);
+
+ res.json({usersCompetitiveStats: usersCompetitiveStats });
+ } catch (err) {
+ res.status(500).send();
+ }
+});
+
+app.get('/record/ranking/:user', async (req, res) => {
+ try {
+ const user = req.params.user.toString();
+
+ let usersRanking = await getRanking();
+ let userCompetitiveStats = usersRanking.filter(userData => userData._id === user);
+
+ res.json({userCompetitiveStats: userCompetitiveStats[0] });
+ } catch (err) {
+ console.log(err)
+ res.status(500).send();
+ }
+});
+
app.get('/record/:user', async (req, res) => {
try {
- const recordFound = await Record.findOne({ user: req.params.user }, 'games');
+ const recordFound = await Record.findOne({ user: req.params.user.toString() }, 'games');
if (!recordFound) {
res.json({record: "undefined" });
} else {
@@ -58,6 +84,37 @@ app.get('/record/:user', async (req, res) => {
}
});
+
+async function getRanking(){
+ const nowTime = new Date();
+ let timeDifferenceInMiliseconds = nowTime - lastTime;
+ if(ranking.length == 0 || timeDifferenceInMiliseconds > minTimeDifferenceInMiliseconds){
+ ranking = await Record.aggregate([
+ // Unwind the games array to work with each game separately
+ { $unwind: "$games" },
+ // Match only competitive games
+ { $match: { "games.competitive": true } },
+ // Group by user and calculate total points and total competitive games per user
+ {
+ $group: {
+ _id: "$user",
+ totalPoints: { $sum: "$games.points" },
+ totalCompetitiveGames: { $sum: 1 } // Count the number of competitive games
+ }
+ },
+ // Sort by total points in descending order (top 1 will have the highest points)
+ { $sort: { totalPoints: -1 } }
+ ]);
+
+ //The operator ... dumps the user json making it {_id , totalPoints, totalCompetitiveGames, position}
+ ranking = ranking.map((user, index) => ({ ...user, position: index + 1 }));
+ lastTime = new Date();
+ }
+
+ return ranking;
+
+}
+
const server = app.listen(port, () => {
console.log(`Record Service listening at http://localhost:${port}`);
});
diff --git a/users/recordservice/record-service.test.js b/users/recordservice/record-service.test.js
index d2c2ca6e..5594f781 100644
--- a/users/recordservice/record-service.test.js
+++ b/users/recordservice/record-service.test.js
@@ -1,5 +1,6 @@
const request = require('supertest');
const { MongoMemoryServer } = require('mongodb-memory-server');
+const Record = require('./record-model')
let mongoServer;
let app;
@@ -9,6 +10,8 @@ beforeAll(async () => {
const mongoUri = mongoServer.getUri();
process.env.MONGODB_URI = mongoUri;
app = require('./record-service');
+
+ await populateDatabase();
});
afterAll(async () => {
@@ -99,6 +102,33 @@ describe('Record Service', () => {
response = await request(app).post('/record').send(newUser);
expect(response.status).toBe(400);
+ //Data lacks competitive field
+ newUser = {
+ user:"testuser",
+ game:
+ {
+ "questions": [
+ {
+ "question": "¿Cuál es el río más largo del mundo?",
+ "answers": ["Nilo", "Amazonas", "Yangtsé", "Misisipi"],
+ "answerGiven": "Amazonas",
+ "correctAnswer": "Amazonas"
+ },
+ {
+ "question": "¿Cuál es el elemento más abundante en la corteza terrestre?",
+ "answers": ["Hierro", "Oxígeno", "Silicio", "Aluminio"],
+ "answerGiven": "Oxígeno",
+ "correctAnswer": "Oxígeno"
+ }
+ ],
+ "points": 2500,
+ "date": "02/02/24"
+ }
+ };
+
+ response = await request(app).post('/record').send(newUser);
+ expect(response.status).toBe(500);
+
});
it('should add a new record on POST /record', async () => {
const newUser = {
@@ -120,7 +150,8 @@ describe('Record Service', () => {
}
],
"points": 2500,
- "date": "02/02/24"
+ "date": "02/02/24",
+ "competitive": false
}
};
@@ -161,7 +192,8 @@ describe('Record Service', () => {
}
],
"points": 3000,
- "date": "03/03/24"
+ "date": "03/03/24",
+ "competitive": false
}
};
@@ -174,4 +206,78 @@ describe('Record Service', () => {
expect(responseGet.body.record.games[1]).toHaveProperty('date', '03/03/24');
});
-});
\ No newline at end of file
+
+ it('should get back on GET /record/testuser', async () => {
+ const responseGet = await request(app).get('/record/testuser');
+ expect(responseGet.status).toBe(200);
+ expect(responseGet.body.record.games[0]).toHaveProperty('date', '02/02/24');
+ });
+
+ it('should get back on GET /record/testuser', async () => {
+ const responseGet = await request(app).get('/record/testuser');
+ expect(responseGet.status).toBe(200);
+ expect(responseGet.body.record.games[0]).toHaveProperty('date', '02/02/24');
+ });
+
+ it('should get back on GET /record/ranking/top10', async () => {
+ const responseGet = await request(app).get('/record/ranking/top10');
+ expect(responseGet.status).toBe(200);
+ const usersStats = responseGet.body.usersCompetitiveStats;
+ expect(usersStats.length).toBe(10); //Only top 10
+
+ //Ordered by points
+ expect(usersStats[0]).toHaveProperty('_id', 'user10');
+ expect(usersStats[9]).toHaveProperty('_id', 'user1');
+
+ expect(usersStats[0]).toHaveProperty('totalCompetitiveGames', 2);
+ expect(usersStats[0]).toHaveProperty('totalPoints', 200);
+ });
+
+ it('should get back on GET /record/ranking/user1', async () => {
+ const responseGet = await request(app).get('/record/ranking/user1');
+ expect(responseGet.status).toBe(200);
+ const userStats = responseGet.body.userCompetitiveStats;
+
+ expect(userStats).toHaveProperty('_id', 'user1');
+ expect(userStats).toHaveProperty('position', 10);
+ expect(userStats).toHaveProperty('totalCompetitiveGames', 2);
+ expect(userStats).toHaveProperty('totalPoints', 20); //i * 10 * totalCompetitiveGames , i = 2
+ });
+});
+
+
+
+async function populateDatabase() {
+ try {
+ // Generate 10 users
+ for (let i = 1; i <= 10; i++) {
+ const user = `user${i}`;
+ const games = [];
+
+ // Generate 3 games for each user
+ for (let j = 1; j <= 3; j++) {
+ const game = {
+ questions: [
+ {
+ question: `Question ${j} for ${user}`,
+ answers: ["Answer 1", "Answer 2", "Answer 3", "Answer 4"],
+ answerGiven: "Answer 1",
+ correctAnswer: "Answer 1"
+ }
+ ],
+ points: i * 10,
+ date: "04/01/2024",
+ competitive: j <= 2 ? true : false // Only 2 games are competitive
+ };
+ games.push(game);
+ }
+
+ // Guardar el usuario en la base de datos
+ await Record.create({ user, games });
+ }
+
+ console.log('Database populated successfully');
+ } catch (error) {
+ console.error('Error populating database:', error);
+ }
+}
\ No newline at end of file
diff --git a/users/userservice/package-lock.json b/users/userservice/package-lock.json
index f21b26cb..e2ccf4a2 100644
--- a/users/userservice/package-lock.json
+++ b/users/userservice/package-lock.json
@@ -12,6 +12,7 @@
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"express": "^4.18.2",
+ "jsonwebtoken": "^9.0.2",
"mongoose": "^8.0.4"
},
"devDependencies": {
@@ -1656,6 +1657,11 @@
"node": "*"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -2061,6 +2067,14 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -3602,6 +3616,51 @@
"node": ">=6"
}
},
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken/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/jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/kareem": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz",
@@ -3646,6 +3705,41 @@
"node": ">=8"
}
},
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
+ },
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
diff --git a/users/userservice/package.json b/users/userservice/package.json
index 71cc1cdb..a2efb649 100644
--- a/users/userservice/package.json
+++ b/users/userservice/package.json
@@ -21,6 +21,7 @@
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"express": "^4.18.2",
+ "jsonwebtoken": "^9.0.2",
"mongoose": "^8.0.4"
},
"devDependencies": {
diff --git a/users/userservice/user-model.js b/users/userservice/user-model.js
index 71d81b5f..e6643ff2 100644
--- a/users/userservice/user-model.js
+++ b/users/userservice/user-model.js
@@ -1,6 +1,10 @@
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
+ email: {
+ type: String,
+ required: true,
+ },
username: {
type: String,
required: true,
diff --git a/users/userservice/user-service.js b/users/userservice/user-service.js
index 69899c29..a6ea8a09 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 jwt = require('jsonwebtoken');
const User = require('./user-model')
const app = express();
@@ -16,6 +17,11 @@ const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/userdb';
mongoose.connect(mongoUri);
+const validateEmail = (email) => {
+ return String(email)
+ .toLowerCase()
+ .match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/);
+};
// Function to validate required fields in the request body
function validateRequiredFields(req, requiredFields) {
@@ -24,35 +30,80 @@ function validateRequiredFields(req, requiredFields) {
throw new Error(`Missing required field: ${field}`);
}
}
+
+ let email = req.body.email.toString();
+ let username = req.body.username.toString();
+ let password = req.body.password.toString();
+ let repeatPassword = req.body.repeatPassword.toString();
+
+ if(!validateEmail(email)){
+ //User put a wrong format email
+ throw new Error("Wrong email format (example@example.com)")
+ }
+
+ if(password !== repeatPassword){
+ //User put the same password
+ throw new Error("Passwords dont match");
+ }
+ if(/\s/.test(password)){
+ //User put spaces in password
+ throw new Error("Password cannot have spaces");
+ }
+ if(password.length < 8){
+ //Password too short
+ throw new Error("Password must be at least 8 characters long");
+ }
+
+ if(password.length > 64){
+ //Password too long
+ throw new Error("Password must less than 64 characters long");
+ }
+
+ if(/\s/.test(username)){
+ //Spaces in username
+ throw new Error("Username cannot have spaces");
+ }
+
}
app.post('/adduser', async (req, res) => {
try {
// Check if required fields are present in the request body
try{
- validateRequiredFields(req, ['username', 'password']);
+ validateRequiredFields(req, ['email', 'username', 'password', 'repeatPassword']);
}
catch(error){
res.status(400).json({ error : error.message });
+ console.log(res)
return
}
//Check there is not a user with the same name
- const user = await User.findOne({username: req.body.username});
+ const userUsername = await User.findOne({username: req.body.username.toString()});
+
+ //Check there is not a user with the same name
+ const userEmail = await User.findOne({email: req.body.email.toString()});
- if(user)
+ if(userUsername)
return res.status(400).json({error : "Username already in use"})
+ if(userEmail)
+ return res.status(400).json({error : "Email already in use"})
+
// Encrypt the password before saving it
const hashedPassword = await bcrypt.hash(req.body.password, 10);
const newUser = new User({
+ email: req.body.email,
username: req.body.username,
password: hashedPassword,
});
- await newUser.save();
- res.json({username: newUser.username});
+ const savedUser = await newUser.save();
+
+ const token = jwt.sign({ userId: savedUser._id }, (process.env.JWT_KEY??'my-key'), { expiresIn: '1h' });
+
+ res.json({ token: token, username: savedUser.username, email: savedUser.email});
} catch (error) {
res.status(400).json({ error: error.message });
}});
diff --git a/users/userservice/user-service.test.js b/users/userservice/user-service.test.js
index 7b9e1b4f..ab9fa21e 100644
--- a/users/userservice/user-service.test.js
+++ b/users/userservice/user-service.test.js
@@ -4,7 +4,12 @@ const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
let app;
-
+let newUser = {
+ email: 'example@example.com',
+ username: 'testuser',
+ password: 'testpassword',
+ repeatPassword: 'testpassword'
+};
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
@@ -18,35 +23,91 @@ afterAll(async () => {
await mongoServer.stop();
});
+afterEach(async () => {
+ newUser = {
+ email: 'example@example.com',
+ username: 'testuser',
+ password: 'testpassword',
+ repeatPassword: 'testpassword'
+ };
+})
+
describe('User Service', () => {
it('should add a new user on POST /adduser', async () => {
- const newUser = {
- username: 'testuser',
- password: 'testpassword'
- };
-
const response = await request(app).post('/adduser').send(newUser);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('username', 'testuser');
});
- it('Should show missing field user /adduser', async () => {
+ it('Should show missing field email /adduser', async () => {
const response = await request(app).post('/adduser').send();
expect(response.status).toBe(400);
- expect(response.body).toHaveProperty('error', 'Missing required field: username');
+ expect(response.body).toHaveProperty('error', 'Missing required field: email');
});
it('Should not register user /adduser', async () => {
- const newUser = {
- username: 'testuser',
- password: 'testpassword'
- };
+ newUser.email = 'example2@example.com';
const response = await request(app).post('/adduser').send(newUser);
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('error', 'Username already in use');
});
-
+ it('Should not register user /adduser', async () => {
+ newUser.username = 'testuser2';
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Email already in use');
+ });
+
+});
+
+describe('User service validations', () => {
+ it('shows error message on wrong formed email', async () => {
+ newUser.email = "test"
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Wrong email format (example@example.com)');
+ });
+
+ it('shows error message on not equal passwords', async () => {
+ newUser.repeatPassword = newUser.repeatPassword + "n";
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Passwords dont match');
+ });
+
+ it('shows error message on password have spaces', async () => {
+ setPassword("1234 56789")
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Password cannot have spaces');
+ });
+
+ it('shows error message on password length is less than 8', async () => {
+ setPassword("12")
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Password must be at least 8 characters long');
+ });
+
+ it('shows error message on password length is more than 64', async () => {
+ setPassword("01234567890123456789012345678901234567890123456789012345678901234")
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Password must less than 64 characters long');
+ });
+
+ it('shows error message on username has spaces', async () => {
+ newUser.username = newUser.username + " yes"
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Username cannot have spaces');
+ });
});
+
+function setPassword(newPassword){
+ newUser.password = newPassword;
+ newUser.repeatPassword = newPassword;
+}
\ No newline at end of file
diff --git a/webapp/e2e/features/gameMenu.feature b/webapp/e2e/features/gameMenu.feature
new file mode 100644
index 00000000..a156fbbc
--- /dev/null
+++ b/webapp/e2e/features/gameMenu.feature
@@ -0,0 +1,8 @@
+Feature: Game Menu page functionality
+ Scenario: There should be visible three links
+ Given I am on the game menu
+ Then three buttons should be visible
+ Scenario: New Game should go to game configurator
+ Given I am on the game menu
+ When I click on New Game
+ Then I should be in the game configurator
diff --git a/webapp/e2e/steps/gameMenu.steps.js b/webapp/e2e/steps/gameMenu.steps.js
new file mode 100644
index 00000000..9d00a32d
--- /dev/null
+++ b/webapp/e2e/steps/gameMenu.steps.js
@@ -0,0 +1,62 @@
+const puppeteer = require('puppeteer');
+const { defineFeature, loadFeature } = require('jest-cucumber');
+const setDefaultOptions = require('expect-puppeteer').setDefaultOptions;
+
+const feature = loadFeature('./features/gameMenu.feature');
+
+const { register, login, logout } = require("../utils");
+
+let page;
+let browser;
+
+const email = "testUser@example.com";
+const username = "testUser"
+const password = "testUserPassword"
+
+defineFeature(feature, test => {
+
+ beforeAll(async () => {
+ browser = await puppeteer.launch({
+ headless: "new",
+ slowMo: 40,
+ defaultViewport: { width: 1920, height: 1080 },
+ args: ['--window-size=1920,1080']
+ });
+
+ page = await browser.newPage();
+ setDefaultOptions({ timeout: 30000 });
+
+ await register(page, email, username, password);
+ });
+
+ beforeEach(async () => {
+ await logout(page);
+ await login(page, username, password);
+ })
+
+ test('There should be visible three links', ({ given, then }) => {
+ given('I am on the game menu', async () => {
+ await page.goto('http://localhost:3000/menu');
+ await page.waitForSelector('.divMenu');
+ });
+
+ then('three buttons should be visible', async () => {
+ //await expect(page).toMatchElement('.linkButton');
+ const elements = await page.$$('.linkButton');
+ expect(elements.length).toBe(3);
+ });
+ });
+ test('New Game should go to game configurator', ({ given, when, then }) => {
+ given('I am on the game menu', async () => {
+ await page.goto('http://localhost:3000/menu');
+ await page.waitForSelector('.divMenu');
+ });
+ when('I click on New Game', async () => {
+ await page.click('.linkButton');
+ });
+ then('I should be in the game configurator', async () => {
+ await expect(page).toMatchElement('.GameConfiguratorDiv');
+ });
+ });
+
+});
diff --git a/webapp/e2e/steps/home.steps.js b/webapp/e2e/steps/home.steps.js
index 22d6dec1..eeb89d7e 100644
--- a/webapp/e2e/steps/home.steps.js
+++ b/webapp/e2e/steps/home.steps.js
@@ -10,17 +10,17 @@ let browser;
defineFeature(feature, test => {
beforeAll(async () => {
- browser = await puppeteer.launch({
- headless: "new", // Cambiado a "new" para evitar la advertencia de deprecación
- slowMo: 20,
- defaultViewport: { width: 1920, height: 1080 },
- args: ['--window-size=1920,1080']
+ browser = await puppeteer.launch({
+ headless: "new",
+ slowMo: 20,
+ defaultViewport: { width: 1920, height: 1080 },
+ args: ['--window-size=1920,1080']
+ });
+
+ page = await browser.newPage();
+ setDefaultOptions({ timeout: 10000 });
});
- page = await browser.newPage();
- setDefaultOptions({ timeout: 10000 });
-});
-
// test('The text container is initially visible', ({ given, then }) => {
// given('I am on the home page', async () => {
// await page.goto('http://localhost:3000/home');
diff --git a/webapp/e2e/utils.js b/webapp/e2e/utils.js
new file mode 100644
index 00000000..b25f6aaa
--- /dev/null
+++ b/webapp/e2e/utils.js
@@ -0,0 +1,37 @@
+async function register(page, email, username, password) {
+ await page.goto('http://localhost:3000/addUser');
+ await page.waitForSelector('.general');
+
+ await page.type('input[name="email"]', email);
+ await page.type('input[name="username"]', username);
+ await page.type('input[name="password"]', password);
+ await page.type('input[name="repeat_password"]', password);
+ await page.click('button[type="submit"]');
+ //Wait for menu to load
+ await page.waitForSelector('.divMenu');
+}
+
+async function login(page, username, password) {
+ await page.goto('http://localhost:3000/login');
+ await page.waitForSelector('.general');
+
+ await page.type('input[type="text"]', username);
+ await page.type('input[type="password"]', password);
+ await page.click('button[type="submit"]');
+ //Wait for menu to load
+ await page.waitForSelector('.divMenu');
+}
+
+async function logout(page){
+ await page.click('.user-button');
+ await page.waitForSelector('.MuiMenu-paper', { visible: true });
+ await page.click('text=Log Out');
+ //Wait for home to load
+ await page.waitForSelector('.general');
+}
+
+module.exports = {
+ register,
+ login,
+ logout
+ };
\ No newline at end of file
diff --git a/webapp/package-lock.json b/webapp/package-lock.json
index 01a7cfde..e71a4dd5 100644
--- a/webapp/package-lock.json
+++ b/webapp/package-lock.json
@@ -19,6 +19,7 @@
"express": "^4.19.2",
"i18n": "^0.15.1",
"jquery": "^3.7.1",
+ "js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.3.0",
"react": "^18.2.0",
@@ -28,7 +29,8 @@
"react-icons": "^5.0.1",
"react-router-dom": "^6.22.3",
"react-scripts": "^5.0.1",
- "web-vitals": "^3.5.1"
+ "web-vitals": "^3.5.1",
+ "zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
@@ -18530,6 +18532,14 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
},
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -28486,6 +28496,11 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zxcvbn": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
+ "integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ=="
}
}
}
diff --git a/webapp/package.json b/webapp/package.json
index 53a5c05f..c258e1f2 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -14,6 +14,7 @@
"express": "^4.19.2",
"i18n": "^0.15.1",
"jquery": "^3.7.1",
+ "js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.3.0",
"react": "^18.2.0",
@@ -23,7 +24,8 @@
"react-icons": "^5.0.1",
"react-router-dom": "^6.22.3",
"react-scripts": "^5.0.1",
- "web-vitals": "^3.5.1"
+ "web-vitals": "^3.5.1",
+ "zxcvbn": "^4.4.2"
},
"scripts": {
"start": "react-scripts start",
diff --git a/webapp/public/favicon.ico b/webapp/public/favicon.ico
index a11777cc..5019a7d6 100644
Binary files a/webapp/public/favicon.ico and b/webapp/public/favicon.ico differ
diff --git a/webapp/public/index.html b/webapp/public/index.html
index aa069f27..a9a639d1 100644
--- a/webapp/public/index.html
+++ b/webapp/public/index.html
@@ -3,6 +3,7 @@
+
{
+ document.title = 'WIQ';
+ }, []);
+
+ //The double !! converts an expression that can be a boolean into an actual boolean
+ const isLoggedIn = !!Cookies.get('user');
+
return (
-