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/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index 317c55ae..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,103 +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/:amount/:type', async (req, res) => { - try { - const lang = req.params.lang.toString(); - const amount = req.params.amount.toString(); - const type = 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); + +app.get('/questions/:lang/:amount/:type', verifyToken, async (req, res) => { + try { + 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.status(error.response.status).json({ error: error.response.data.error }); + manageError(error) } }); -app.get('/questions/:lang/:amount', async (req, res) => { +app.get('/questions/:lang/:amount', verifyToken, async (req, res) => { try { - const lang = req.params.lang.toString(); - const amount = 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); + 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) { - - res.status(error.response.status).json({ error: error.response.data.error }); + manageError(error) } }); -app.get('/questions/:lang', async (req, res) => { +app.get('/questions/:lang', verifyToken, async (req, res) => { try { - const lang = req.params.lang.toString(); - // Forward the question request to the quetion service - const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang); - - res.json(questionResponse.data); + 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) { - res.status(error.response.status).json({ error: error.response.data.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/ranking/top10', async(req, res)=>{ +app.get('/record/ranking/top10', verifyToken, async(req, res)=>{ try { // Forward the record request to the record service 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', async(req, res)=>{ +app.get('/record/ranking/:user', 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/ranking/' + user); - res.json(recordResponse.data); + 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) { - res.send(error); + manageError(error) } }); -app.get('/record/:user', async(req, res)=>{ +app.get('/record/:user', 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); - res.json(recordResponse.data); + 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) { - res.send(error); + manageError(error) } }); @@ -159,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 971fead9..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')) { @@ -45,6 +52,14 @@ describe('Gateway Service', () => { } }); + + + // 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) @@ -69,7 +84,7 @@ 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'); checkQuestion(response); }); @@ -77,7 +92,7 @@ describe('Gateway Service', () => { // 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'); checkQuestion(response); }); @@ -85,7 +100,7 @@ describe('Gateway Service', () => { // Test /questions/:lang/:amount endpoint it('should forward questions request to question service', async () => { const response = await request(app) - .get('/questions/es/1'); + .get('/questions/es/1').set('token', 'valorDelToken'); checkQuestion(response); }); @@ -93,7 +108,7 @@ describe('Gateway Service', () => { // 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'); + .get('/questions/es/1/CAPITAL').set('token', 'valorDelToken'); checkQuestion(response); }); @@ -101,7 +116,7 @@ describe('Gateway Service', () => { // 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'); @@ -110,7 +125,7 @@ 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'); checkRecord(response); }); @@ -118,7 +133,7 @@ describe('Gateway Service', () => { // Test /record/ranking/:user endpoint it('should forward record request to record service', async () => { const response = await request(app) - .get('/record/ranking/testuser'); + .get('/record/ranking/testuser').set('token', 'valorDelToken'); checkRecord(response); }); @@ -126,10 +141,11 @@ describe('Gateway Service', () => { // Test /record/ranking/top10 endpoint it('should forward record request to record service', async () => { const response = await request(app) - .get('/record/ranking/top10'); + .get('/record/ranking/top10').set('token', 'valorDelToken'); checkRecord(response); }); + }); function checkRecord(response){ 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 effc5ac7..77377c74 100644 --- a/questionservice/question-service.js +++ b/questionservice/question-service.js @@ -89,7 +89,7 @@ app.get('/questions/:lang', async (req, res) => { const lang = req.params.lang; const questions = await Question.aggregate([ - {$match: {language : lang}}, //Condition + {$match: {language : lang.toString()}}, //Condition {$sample: {size:5}} //5 random from the ones that fullfil the condition ]); 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-service.js b/users/recordservice/record-service.js index cf94544a..3bca1fa8 100644 --- a/users/recordservice/record-service.js +++ b/users/recordservice/record-service.js @@ -22,7 +22,7 @@ app.post('/record', async (req, res) => { const user = req.body.user; const game = req.body.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); } 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/steps/gameMenu.steps.js b/webapp/e2e/steps/gameMenu.steps.js index 215af2f7..9d00a32d 100644 --- a/webapp/e2e/steps/gameMenu.steps.js +++ b/webapp/e2e/steps/gameMenu.steps.js @@ -4,22 +4,36 @@ 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({ - slowMo: 20, - defaultViewport: { width: 1920, height: 1080 }, - args: ['--window-size=1920,1080'] + headless: "new", + slowMo: 40, + defaultViewport: { width: 1920, height: 1080 }, + args: ['--window-size=1920,1080'] }); page = await browser.newPage(); - setDefaultOptions({ timeout: 10000 }); + 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'); @@ -29,7 +43,7 @@ defineFeature(feature, test => { then('three buttons should be visible', async () => { //await expect(page).toMatchElement('.linkButton'); const elements = await page.$$('.linkButton'); - expect(elements.length).toBeGreaterThan(0); // At least one element with class 'linkButton' + expect(elements.length).toBe(3); }); }); test('New Game should go to game configurator', ({ given, when, then }) => { diff --git a/webapp/e2e/steps/home.steps.js b/webapp/e2e/steps/home.steps.js index 770b2a0a..3bae6848 100644 --- a/webapp/e2e/steps/home.steps.js +++ b/webapp/e2e/steps/home.steps.js @@ -11,9 +11,10 @@ defineFeature(feature, test => { beforeAll(async () => { browser = await puppeteer.launch({ - slowMo: 20, - defaultViewport: { width: 1920, height: 1080 }, - args: ['--window-size=1920,1080'] + headless: "new", + slowMo: 20, + defaultViewport: { width: 1920, height: 1080 }, + args: ['--window-size=1920,1080'] }); page = await browser.newPage(); 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 9ecc289d..2a9213a2 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", @@ -18518,6 +18519,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", diff --git a/webapp/package.json b/webapp/package.json index e843dbb5..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", diff --git a/webapp/src/App.js b/webapp/src/App.js index b79ab18b..69c168cf 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -11,7 +11,7 @@ import Container from '@mui/material/Container'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import './custom.css'; import HistoricalView from './components/HistoricalData/HistoricalView'; -import { UserContextProvider } from './components/loginAndRegistration/UserContext'; +import Cookies from 'js-cookie'; import GameConfigurator from './components/GameConfigurator/GameConfigurator'; import RankingView from './components/ranking/RankingView'; @@ -20,28 +20,30 @@ function App() { useEffect(() => { document.title = 'WIQ'; }, []); + + //The double !! converts an expression that can be a boolean into an actual boolean + const isLoggedIn = !!Cookies.get('user'); + return ( -
} /> - } /> - } /> + : } /> + : } /> } /> - } /> - } /> - } /> - } /> - }/> - } /> + : } /> + : } /> + : } /> + : } /> + : }/> + : } /> } />
-
); } diff --git a/webapp/src/App.test.js b/webapp/src/App.test.js index 0d9927ca..3756fd6f 100644 --- a/webapp/src/App.test.js +++ b/webapp/src/App.test.js @@ -1,4 +1,4 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render} from '@testing-library/react'; import App from './App'; diff --git a/webapp/src/components/GameMenu/GameMenu.js b/webapp/src/components/GameMenu/GameMenu.js index 7a1f94ed..70ef4d0e 100644 --- a/webapp/src/components/GameMenu/GameMenu.js +++ b/webapp/src/components/GameMenu/GameMenu.js @@ -3,6 +3,7 @@ import { Link } from "react-router-dom"; import {useTranslation} from "react-i18next"; import ButtonHistoricalData from "../HistoricalData/ButtonHistoricalData"; + export default function GameMenu() { const[t] = useTranslation("global"); diff --git a/webapp/src/components/HistoricalData/HistoricalView.js b/webapp/src/components/HistoricalData/HistoricalView.js index 66dc8a2d..2a4d336b 100644 --- a/webapp/src/components/HistoricalData/HistoricalView.js +++ b/webapp/src/components/HistoricalData/HistoricalView.js @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import {useTranslation} from "react-i18next"; import HistoryRecordRetriever from './HistoryRecordRetriever'; -import { useUserContext } from '../loginAndRegistration/UserContext'; +import Cookies from 'js-cookie' import BackButtonToGameMenu from '../fragments/BackButtonToGameMenu'; import RecordList from './RecordList'; @@ -12,13 +12,13 @@ const retriever = new HistoryRecordRetriever(); export default function HistoricalView() { const [records, setRecords]= useState(null); const[t] = useTranslation("global"); - const {user} = useUserContext(); const getRecords = async ()=>{ try { - var jsonRecords = await retriever.getRecords(user.username); - var recordsArray = jsonRecords.games; - setRecords(recordsArray); + let cookie = JSON.parse(Cookies.get('user')) + var jsonRecords = await retriever.getRecords(cookie.username, cookie.token); + var recordsArray = jsonRecords.games; + setRecords(recordsArray); } catch (error) { console.log(error); } diff --git a/webapp/src/components/HistoricalData/HistoricalView.test.js b/webapp/src/components/HistoricalData/HistoricalView.test.js index e224a4ce..e2a5f39d 100644 --- a/webapp/src/components/HistoricalData/HistoricalView.test.js +++ b/webapp/src/components/HistoricalData/HistoricalView.test.js @@ -4,9 +4,9 @@ import i18en from 'i18next'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { MemoryRouter } from 'react-router-dom'; -import { UserContextProvider} from '../loginAndRegistration/UserContext'; import HistoricalView from './HistoricalView'; import { act } from 'react-dom/test-utils'; +import Cookies from 'js-cookie'; const mockAxios = new MockAdapter(axios); i18en.use(initReactI18next).init({ resources: {}, @@ -16,29 +16,27 @@ i18en.use(initReactI18next).init({ } }); global.i18en = i18en; +Cookies.set('user', JSON.stringify({username:"dummy", token:"fasfda"})) describe('Historical View component', () => { beforeEach(() => { mockAxios.reset(); }); it('renders no games if the api is down', () => { - const user = { username: 'dummy' }; - render(); + render(); const text = screen.getByText(i18en.t('historicalView.no_games_played')); expect(text).toBeInTheDocument(); }); it('renders no games if the user has no games', async () => { - const user = { username: 'dummy' }; mockAxios.onGet('http://localhost:8000/record/dummy').reply(200, "undefined"); await act(async()=> - await render() + await render() ) const text = screen.getByText(i18en.t('historicalView.no_games_played')); expect(text).toBeInTheDocument(); }); it('renders Game Record Buttons', async () => { - const user = { username: 'dummy' }; mockAxios.onGet('http://localhost:8000/record/dummy').reply(200, {record : {user: "dummy", games: [{ @@ -53,7 +51,7 @@ describe('Historical View component', () => { }]}}); await act(async()=> - await render() + await render() ) const regex = /\d{1,2}\/\d{1,2}\/\d{4}/; // Expresión regular para el formato de fecha "MM/DD/YYYY" o "M/D/YYYY" const textRegex = new RegExp(regex); diff --git a/webapp/src/components/HistoricalData/HistoryRecordRetriever.js b/webapp/src/components/HistoricalData/HistoryRecordRetriever.js index 0e65d6ff..0f554460 100644 --- a/webapp/src/components/HistoricalData/HistoryRecordRetriever.js +++ b/webapp/src/components/HistoricalData/HistoryRecordRetriever.js @@ -7,9 +7,9 @@ class HistoryRecordRetriever{ } - async getRecords(user) { + async getRecords(user, token) { try { - const response = await axios.get(this.apiUrl + '/' + user); + const response = await axios.get(this.apiUrl + '/' + user, { headers : {'token':token}}); const receivedRecords = await response.data; return receivedRecords.record; } catch (error) { diff --git a/webapp/src/components/fragments/NavBar.js b/webapp/src/components/fragments/NavBar.js index d6938b13..5ce411cd 100644 --- a/webapp/src/components/fragments/NavBar.js +++ b/webapp/src/components/fragments/NavBar.js @@ -4,21 +4,36 @@ import MenuItem from '@mui/material/MenuItem'; import Menu from '@mui/material/Menu'; import "../../custom.css"; import { useTranslation } from "react-i18next"; -import { useUserContext } from '../loginAndRegistration/UserContext'; +import { useNavigate } from 'react-router-dom'; +import Cookies from 'js-cookie'; function Navbar() { - + const navigate = useNavigate(); const [t, i18n] = useTranslation("global"); - const [anchorEl, setAnchorEl] = useState(null); - const { user } = useUserContext(); + const [anchorLanguage, setAnchorLanguage] = useState(null); + const [anchorUser, setAnchorUser] = useState(null); const handleLanguageMenuOpen = (event) => { - setAnchorEl(event.currentTarget); + setAnchorLanguage(event.currentTarget); }; const handleLanguageMenuClose = () => { - setAnchorEl(null); + setAnchorLanguage(null); + }; + + const handleUserMenuOpen = (event) => { + setAnchorUser(event.currentTarget); + }; + + const handleUserMenuClose = () => { + setAnchorUser(null); + }; + + const removeCookie = () => { + Cookies.remove('user'); + navigate('/home'); + window.location.reload(); }; const changeLanguage = (language) => { @@ -37,18 +52,31 @@ function Navbar() {
changeLanguage("en")}> {t("navBar.en")} changeLanguage("es")}> {t("navBar.es")} changeLanguage("tk")}> {t("navBar.tk")} - {user != null ? ( -

{user.username}

- ) : null} + + {Cookies.get('user') ? ( + <> + + + removeCookie()}> {t("navBar.logout")} + + + ) : null} +
); diff --git a/webapp/src/components/fragments/NavBar.test.js b/webapp/src/components/fragments/NavBar.test.js index 29ceb23f..7fe0137f 100644 --- a/webapp/src/components/fragments/NavBar.test.js +++ b/webapp/src/components/fragments/NavBar.test.js @@ -2,12 +2,10 @@ import { render , screen, fireEvent } from '@testing-library/react'; import { initReactI18next } from 'react-i18next'; import i18en from 'i18next'; import { MemoryRouter } from 'react-router-dom'; - -import { UserContextProvider} from '../loginAndRegistration/UserContext'; import Navbar from './NavBar' +import Cookies from 'js-cookie' import App from '../../App' - i18en.use(initReactI18next).init({ resources: {}, lng: 'en', @@ -16,12 +14,12 @@ i18en.use(initReactI18next).init({ } }); global.i18en = i18en; +Cookies.set('user', JSON.stringify({ username: 'dummy' })) describe('NavBar fragment', () => { it('shows the user name',async () => { - const user = { username: 'dummy' }; - render(); + render(); const text2 = await screen.findByText('dummy') expect(text2).toBeInTheDocument(); // Wait for questions to load diff --git a/webapp/src/components/loginAndRegistration/AddUser.js b/webapp/src/components/loginAndRegistration/AddUser.js index 629f9c40..66683cc6 100644 --- a/webapp/src/components/loginAndRegistration/AddUser.js +++ b/webapp/src/components/loginAndRegistration/AddUser.js @@ -6,67 +6,82 @@ import axios from 'axios'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import zxcvbn from "zxcvbn"; +import Cookies from 'js-cookie'; +import { manageError } from "../../utils/manageError"; const AddUser = () => { const navigate = useNavigate(); const apiUrl = (process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000') + "/adduser"; const { t } = useTranslation("global"); + + const [email, setEmail] = useState(''); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [repeatPassword, setRepeatPassword] = useState(''); - const [passwordStrength, setPasswordStrength] = useState(undefined); + const [passwordStrength, setPasswordStrength] = useState(''); const [passwordStrengthText, setPasswordStrengthText] = useState(''); - const [submitError, setSubmitError] = useState(''); + const [submitErrors, setSubmitErrors] = useState([]); + 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,}))$/); + }; const handleSubmit = async (event) => { event.preventDefault(); - try { - //Validations - //TODO: email validation - if(password !== repeatPassword){ - //User put the same password - setSubmitError("addUser.error_passwords_no_match"); - } else if(/\s/.test(password)){ - //User put spaces in password - setSubmitError("addUser.error_password_spaces"); - } else if(password.length < 8){ - //Password too short - setSubmitError("addUser.error_password_minimum_length"); - } else if(password.length > 64){ - //Password too long - setSubmitError("addUser.error_password_maximum_length"); - } else if(/\s/.test(username)){ - //Spaces in username - setSubmitError("addUser.error_username_spaces"); - } else{ - //Continue - setSubmitError(''); - const response = await axios.post(apiUrl, { username, password }); - console.log("Registered user: " + response.data.username); - navigate('/login'); - } - } catch (error) { - if(error.response.data.error === "Username already in use"){ //TODO: Improve - setSubmitError("addUser.error_username_in_use"); + const newSubmitErrors = []; + + //Validations + + if(!validateEmail(email)){ + //User put a wrong format email + newSubmitErrors.push("addUser.error_wrong_email_format") + } + + if(password !== repeatPassword){ + //User put the same password + newSubmitErrors.push("addUser.error_passwords_no_match"); + } + if(/\s/.test(password)){ + //User put spaces in password + newSubmitErrors.push("addUser.error_password_spaces"); + } + if(password.length < 8){ + //Password too short + newSubmitErrors.push("addUser.error_password_minimum_length"); + } + + if(password.length > 64){ + //Password too long + newSubmitErrors.push("addUser.error_password_maximum_length"); + } + + if(/\s/.test(username)){ + //Spaces in username + newSubmitErrors.push("addUser.error_username_spaces"); + } + + setSubmitErrors(newSubmitErrors); + + if (newSubmitErrors.length === 0) { + try { + const response = await axios.post(apiUrl, { email, username, password, repeatPassword }); + let oneHourAfter = new Date().getTime() + (1 * 60 * 60 * 1000) + Cookies.set('user', JSON.stringify({username : response.data.username, token : response.data.token}) + , {expires:oneHourAfter}); + navigate('/menu'); + window.location.reload(); + } catch (error) { + let processedError = manageError(error); + if(processedError.status !== 500) + setSubmitErrors(['addUser.error_username_in_use']); } - console.error('Error adding user:', error); } }; - //Possible email validation - /** - 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,}))$/ - ); - }; - */ - const handlePasswordChange = (e) => { const newPassword = e.target.value; setPassword(newPassword); @@ -96,63 +111,79 @@ const AddUser = () => { setPasswordStrength(newStrength); }; + const showErrors = () => { + if (submitErrors.length > 0) { + return submitErrors.map((error, index) => ( +

{t(error)}

+ )); + } + return null; + }; + return (
-
-
-
-

{t("addUser.title")}

-
-

{t("addUser.username_placeholder")}:

- +
+ +

{t("addUser.title")}

+ {showErrors()} +
+

{t("addUser.email_placeholder")}:

+ setEmail(e.target.value)} + /> +
+
+

{t("addUser.username_placeholder")}:

+ setUsername(e.target.value)} /> -
-
-

{t("addUser.password_placeholder")}:

- +
+

{t("addUser.password_placeholder")}:

+ -
-
- - {t(passwordStrengthText.toString())} - - -
-
-

{t("addUser.repeat_password_placeholder")}:

- setRepeatPassword(e.target.value)} - /> -
- {submitError &&

{t(submitError)}

} - - - - - - -
-
+
+
+ {t(passwordStrengthText)} + +
+
+

{t("addUser.repeat_password_placeholder")}:

+ setRepeatPassword(e.target.value)} + /> +
+ + + + +
+
); }; @@ -160,61 +191,10 @@ const AddUser = () => { function LinkLogin() { const { t } = useTranslation("global"); return ( - + {t("addUser.login_link")} ); } export default AddUser; - -// const [username, setUsername] = useState(''); -// const [password, setPassword] = useState(''); -// const [error, setError] = useState(''); -// const [openSnackbar, setOpenSnackbar] = useState(false); - -// const addUser = async () => { -// try { -// await axios.post(`${apiEndpoint}/adduser`, { username, password }); -// setOpenSnackbar(true); -// } catch (error) { -// setError(error.response.data.error); -// } -// }; - -// const handleCloseSnackbar = () => { -// setOpenSnackbar(false); -// }; - -// return ( -// -// -// Add User -// -// setUsername(e.target.value)} -// /> -// setPassword(e.target.value)} -// /> -// -// -// {error && ( -// setError('')} message={`Error: ${error}`} /> -// )} -// -// ); -// }; diff --git a/webapp/src/components/loginAndRegistration/AddUser.test.js b/webapp/src/components/loginAndRegistration/AddUser.test.js index 70493840..f95b52ef 100644 --- a/webapp/src/components/loginAndRegistration/AddUser.test.js +++ b/webapp/src/components/loginAndRegistration/AddUser.test.js @@ -33,7 +33,10 @@ describe('', () => { }); - const fillFormAndSubmit = (username, password, repeatPassword) => { + const fillFormAndSubmit = (email, username, password, repeatPassword) => { + const emailInput = screen.getByPlaceholderText('addUser.email_placeholder'); + fireEvent.change(emailInput, { target: { value: email } }); + const usernameInput = screen.getByPlaceholderText('addUser.username_placeholder'); fireEvent.change(usernameInput, { target: { value: username } }); @@ -48,28 +51,40 @@ describe('', () => { }; test('displays correct error messages', async () => { + //Wrong email format lacks @ + fillFormAndSubmit('userexample.com', 'username', '12345678', '123456789'); + expect(screen.getByText('addUser.error_wrong_email_format')).toBeInTheDocument(); + //Wrong email format lacks domain + fillFormAndSubmit('user@example', 'username', '12345678', '123456789'); + expect(screen.getByText('addUser.error_wrong_email_format')).toBeInTheDocument(); //Passwords do not match - fillFormAndSubmit('username', '12345678', '123456789'); + fillFormAndSubmit('user@example.com', 'username', '12345678', '123456789'); expect(screen.getByText('addUser.error_passwords_no_match')).toBeInTheDocument(); //Password with spaces - fillFormAndSubmit('username', '1234 5678', '1234 5678'); + fillFormAndSubmit('user@example.com', 'username', '1234 5678', '1234 5678'); expect(screen.getByText('addUser.error_password_spaces')).toBeInTheDocument(); //Password too short - fillFormAndSubmit('username', '1234567', '1234567'); + fillFormAndSubmit('user@example.com', 'username', '1234567', '1234567'); expect(screen.getByText('addUser.error_password_minimum_length')).toBeInTheDocument(); //Password too long - fillFormAndSubmit('username', '01234567890123456789012345678901234567890123456789012345678901234', '01234567890123456789012345678901234567890123456789012345678901234'); + fillFormAndSubmit('user@example.com', 'username', '01234567890123456789012345678901234567890123456789012345678901234', '01234567890123456789012345678901234567890123456789012345678901234'); expect(screen.getByText('addUser.error_password_maximum_length')).toBeInTheDocument(); //Username with spaces - fillFormAndSubmit('user name', '12345678', '12345678'); + fillFormAndSubmit('user@example.com', 'user name', '12345678', '12345678'); + expect(screen.getByText('addUser.error_username_spaces')).toBeInTheDocument(); + + //Show various errors + fillFormAndSubmit('userexample.com', 'user name', '12345678', '12345678'); expect(screen.getByText('addUser.error_username_spaces')).toBeInTheDocument(); + expect(screen.getByText('addUser.error_wrong_email_format')).toBeInTheDocument(); + //Username in use axios.post.mockRejectedValue({ response: { data: { error: 'Username already in use' } } }); - fillFormAndSubmit('existing_user', '12345678', '12345678'); + fillFormAndSubmit('user@example.com', 'existing_user', '12345678', '12345678'); await waitFor(() => { expect(screen.getByText('addUser.error_username_in_use')).toBeInTheDocument(); }); - expect(axios.post).toHaveBeenCalledWith(expect.any(String), { username: 'existing_user', password: '12345678' }); + expect(axios.post).toHaveBeenCalledWith(expect.any(String), { email: 'user@example.com' ,username: 'existing_user', password: '12345678', repeatPassword: "12345678" }); }); }); diff --git a/webapp/src/components/loginAndRegistration/Login.js b/webapp/src/components/loginAndRegistration/Login.js index ae209ddd..bc7fd00c 100644 --- a/webapp/src/components/loginAndRegistration/Login.js +++ b/webapp/src/components/loginAndRegistration/Login.js @@ -5,7 +5,7 @@ import "../../custom.css"; import axios from 'axios'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useUserContext } from "./UserContext"; +import Cookies from 'js-cookie'; const Login = () => { const navigate = useNavigate(); @@ -13,15 +13,16 @@ const Login = () => { const { t } = useTranslation("global"); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); - const { setUser } = useUserContext(); const handleSubmit = async (event) => { event.preventDefault(); try { const response = await axios.post(apiUrl, { username, password }); - setUser({username : response.data.username, token : response.data.token}) - //Used to redirect to the menu to start playing + let oneHourAfter = new Date().getTime() + (1 * 60 * 60 * 1000) + Cookies.set('user', JSON.stringify({username : response.data.username, token : response.data.token}) + , {expires:oneHourAfter}); navigate('/menu'); + window.location.reload(); } catch (error) { console.error('Error adding user:', error); } @@ -56,13 +57,7 @@ const Login = () => { onChange={(e) => setPassword(e.target.value)} /> - {//TODO: Study this option and see if it is viable - } -
- -
+ diff --git a/webapp/src/components/loginAndRegistration/Login.test.js b/webapp/src/components/loginAndRegistration/Login.test.js index edd34e82..d58571a1 100644 --- a/webapp/src/components/loginAndRegistration/Login.test.js +++ b/webapp/src/components/loginAndRegistration/Login.test.js @@ -1,22 +1,23 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import Login from './Login'; import { BrowserRouter as Router } from 'react-router-dom'; -import { UserContextProvider } from './UserContext'; +import MockAdapter from 'axios-mock-adapter'; +import axios from 'axios'; // Mocking useTranslation hook jest.mock('react-i18next', () => ({ useTranslation: () => ({ t: key => key }), })); +const mockAxios = new MockAdapter(axios); + describe('', () => { - test('renders the Login component', () => { + it('renders the Login component', () => { render( - - ); expect(screen.getByText('login.title')).toBeInTheDocument(); @@ -25,4 +26,24 @@ describe('', () => { expect(screen.getByText('login.login_button')).toBeInTheDocument(); expect(screen.getByText('login.register_link')).toBeInTheDocument(); }); + + it('sends a correct log in petition', async () => { + mockAxios.onPost('http://localhost:8000/login').reply(200, { token: "token", username: "username"}); + + render( + + + + ); + + fireEvent.change(screen.getByPlaceholderText('login.username_placeholder'), { target: { value: 'user' } }); + fireEvent.change(screen.getByPlaceholderText('login.password_placeholder'), { target: { value: 'j' } }); + fireEvent.click(screen.getByText('login.login_button')); + + // Wait for redirection to happen + await waitFor(() => { + expect(window.location.pathname).toBe('/menu'); + }); + + }); }); diff --git a/webapp/src/components/loginAndRegistration/UserContext.js b/webapp/src/components/loginAndRegistration/UserContext.js deleted file mode 100644 index 95764078..00000000 --- a/webapp/src/components/loginAndRegistration/UserContext.js +++ /dev/null @@ -1,19 +0,0 @@ -import React, { createContext, useContext, useState } from 'react'; - -const UserContext = createContext(); - -export function UserContextProvider({ children, baseUser = null }) { - const [user, setUser] = useState(baseUser); - - return ( - - {children} - - ); -} - -export function useUserContext() { - return useContext(UserContext); -} - - diff --git a/webapp/src/components/questionView/CreationHistoricalRecord.js b/webapp/src/components/questionView/CreationHistoricalRecord.js index 3bdb6370..b10f0690 100644 --- a/webapp/src/components/questionView/CreationHistoricalRecord.js +++ b/webapp/src/components/questionView/CreationHistoricalRecord.js @@ -38,7 +38,7 @@ class CreationHistoricalRecord{ } - async sendRecord(user) { + async sendRecord(user, token) { const apiUrl = (process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000') + "/record"; const body = { @@ -49,7 +49,8 @@ class CreationHistoricalRecord{ try { const response = await axios.post(apiUrl, body, { headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'token':token } }); diff --git a/webapp/src/components/questionView/QuestionGenerator.js b/webapp/src/components/questionView/QuestionGenerator.js index 9d204004..ec7a1b6d 100644 --- a/webapp/src/components/questionView/QuestionGenerator.js +++ b/webapp/src/components/questionView/QuestionGenerator.js @@ -8,7 +8,7 @@ class QuestionGenerator{ } - async generateQuestions(lang, type, amount) { + async generateQuestions(lang, type, amount, token) { /* try { //const response = await fetch(this.apiUrl); @@ -40,10 +40,11 @@ class QuestionGenerator{ try { let response; if(type==="COMPETITIVE"){ - response = await axios.get(this.apiUrl + '/' + lang); + response = await axios.get(this.apiUrl + '/' + lang, {headers : {'token':token}}); }else{ - response = await axios.get(this.apiUrl + '/' + lang + '/' +amount + '/' + type); + response = await axios.get(this.apiUrl + '/' + lang + '/' +amount + '/' + type, {headers : {'token':token}}); } + console.log(response) const receivedQuestions = await response.data; let i = 0; var questions = []; diff --git a/webapp/src/components/questionView/QuestionView.js b/webapp/src/components/questionView/QuestionView.js index b84d4c4a..7b757576 100644 --- a/webapp/src/components/questionView/QuestionView.js +++ b/webapp/src/components/questionView/QuestionView.js @@ -8,7 +8,7 @@ import {useTranslation} from "react-i18next"; import $ from 'jquery'; import RecordList from '../HistoricalData/RecordList'; import ButtonHistoricalData from "../HistoricalData/ButtonHistoricalData"; -import { useUserContext } from '../loginAndRegistration/UserContext'; +import Cookies from 'js-cookie' import BackButtonToGameMenu from '../fragments/BackButtonToGameMenu'; const creationHistoricalRecord = new CreationHistoricalRecord(); @@ -18,14 +18,15 @@ function QuestionView({type= "COMPETITIVE", amount=5}){ const [numQuestion, setnumQuestion] = useState(-1); const [questions, setQuestions] = useState(null); const[t, i18n] = useTranslation("global"); - const {user} = useUserContext(); + const cookie = JSON.parse(Cookies.get('user')) const [audio] = useState(new Audio('/tictac.mp3')); const generateQuestions = async (numQuestion) => { if (numQuestion < 0) { try { - var generatedQuestions = await questionGenerator.generateQuestions(i18n.language, type, amount); + + var generatedQuestions = await questionGenerator.generateQuestions(i18n.language, type, amount, cookie.token); setQuestions(generatedQuestions); setnumQuestion(0); } catch (error) { @@ -109,7 +110,7 @@ function QuestionView({type= "COMPETITIVE", amount=5}){ creationHistoricalRecord.setCompetitive(type === 'COMPETITIVE'); creationHistoricalRecord.setDate(Date.now()); creationHistoricalRecord.setPoints(points); - creationHistoricalRecord.sendRecord(user.username); + creationHistoricalRecord.sendRecord(cookie.username, cookie.token); } }, 1000); diff --git a/webapp/src/components/questionView/QuestionView.test.js b/webapp/src/components/questionView/QuestionView.test.js index 90248453..d00a36e9 100644 --- a/webapp/src/components/questionView/QuestionView.test.js +++ b/webapp/src/components/questionView/QuestionView.test.js @@ -4,11 +4,10 @@ import i18en from 'i18next'; import QuestionView from './QuestionView'; import { MemoryRouter } from 'react-router-dom'; import { act } from 'react-dom/test-utils'; -import { UserContextProvider} from '../loginAndRegistration/UserContext'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import {configure} from '@testing-library/dom'; - +import Cookies from 'js-cookie' // Función para configurar el mock de global.Audio const setupAudioMock = () => { @@ -48,6 +47,7 @@ i18en.use(initReactI18next).init({ } }); global.i18en = i18en; +Cookies.set('user', JSON.stringify({username:"dummy", token:"fasfda"})) describe('Question View component', () => { @@ -56,7 +56,7 @@ describe('Question View component', () => { answers: ["225089","272357","267855","231841"]}]); it('shows the no_questions_message as the endpoint does not exist',async () => { - render(); + render(); const text = screen.getByText(i18en.t('questionView.no_questions_message')); expect(text).toBeInTheDocument(); // Wait for questions to load @@ -67,7 +67,7 @@ describe('Question View component', () => { it('speaks the question when the speaker button is clicked', async () => { await act(async () => { - render(); + render(); }); fireEvent.click(screen.getByText('🔊')); @@ -82,7 +82,7 @@ describe('Question View component', () => { //It gives an error as we are not wrapping it by act, however by doing this we simulate a no questions situation await act(async () =>{ - await render(); + await render(); }) await waitFor(() => expect(screen.getByText('What is the population of Oviedo?')).toBeInTheDocument()); @@ -97,7 +97,7 @@ describe('Question View component', () => { it('shows colors to reveal correct answer and it sounds', async () => { setupAudioMock(); await act(async () =>{ - await render(); + await render(); }) await waitFor(() => expect(screen.getByText('What is the population of Oviedo?')).toBeInTheDocument()); @@ -115,7 +115,7 @@ describe('Question View component', () => { it('shows colors to reveal false answer and it sounds', async () => { setupAudioMock() await act(async () =>{ - await render(); + await render(); }) await waitFor(() => expect(screen.getByText('What is the population of Oviedo?')).toBeInTheDocument()); @@ -133,7 +133,7 @@ describe('Question View component', () => { it('shows timer and tiktak sound', async () => { setupAudioMock() await act(async () =>{ - await render(); + await render(); }) await waitFor(() => expect(screen.getByText('What is the population of Oviedo?')).toBeInTheDocument()); @@ -147,11 +147,9 @@ describe('Question View component', () => { mockAxios.onGet('http://localhost:8000/questions/en').reply(200, []); mockAxios.onPost('http://localhost:8000/record').reply(200, {user:'myUser'}); - const user = { username: 'myUser' }; - //It gives an error as we are not wrapping it by act, however by doing this we simulate a no questions situation await act(async () =>{ - await render(); + await render(); }) await waitFor(() => expect(screen.getByText(i18en.t('questionView.finished_game'))).toBeInTheDocument()); @@ -168,7 +166,7 @@ describe('Question View component', () => { // [{question: "What is the population of Oviedo?", // answers: ["225089","272357","267855","231841"]}]); // await act(async () =>{ - // await render(); + // await render(); // }) // await waitFor(() => expect(screen.getByText('What is the population of Oviedo?')).toBeInTheDocument()); diff --git a/webapp/src/components/ranking/RankingRetriever.js b/webapp/src/components/ranking/RankingRetriever.js index a84de7e6..03ebcd6c 100644 --- a/webapp/src/components/ranking/RankingRetriever.js +++ b/webapp/src/components/ranking/RankingRetriever.js @@ -7,10 +7,10 @@ class RankingRetriever{ } - async getTopTen() { + async getTopTen(token) { try { - const response = await axios.get(this.apiUrl + '/top10');//finding the top ten + const response = await axios.get(this.apiUrl + '/top10', {headers : {'token':token}});//finding the top ten const receivedTopTenRanking = await response.data; return receivedTopTenRanking; } catch (error) { @@ -74,10 +74,10 @@ class RankingRetriever{ ] }*/ } - async getUser(user){ + async getUser(user, token){ try { - const response = await axios.get(this.apiUrl + '/'+user);//finding the top ten + const response = await axios.get(this.apiUrl + '/'+user, {headers : {'token':token}});//finding the top ten const receivedMyRanking = await response.data; return receivedMyRanking.userCompetitiveStats; } catch (error) { diff --git a/webapp/src/components/ranking/RankingView.js b/webapp/src/components/ranking/RankingView.js index f2aad16e..60fd368d 100644 --- a/webapp/src/components/ranking/RankingView.js +++ b/webapp/src/components/ranking/RankingView.js @@ -3,25 +3,25 @@ import RankingRetriever from './RankingRetriever'; import {useTranslation} from "react-i18next"; import Loader from "../fragments/Loader" import BackButton from '../fragments/BackButtonToGameMenu'; -import { useUserContext } from '../loginAndRegistration/UserContext'; +import Cookies from 'js-cookie' const retriever = new RankingRetriever(); const RankingView = () => { const[t] = useTranslation("global"); - const {user} = useUserContext(); + const cookie = JSON.parse(Cookies.get('user')) const [rankingData, setRankingData] = useState(null); const [myRankingData, setMyRankingData] = useState(null); - const [searchTerm, setSearchTerm] = useState(user.username); + const [searchTerm, setSearchTerm] = useState(cookie.username); const getRanking = async () => { try { - var ranking = await retriever.getTopTen(); + var ranking = await retriever.getTopTen(cookie.token); setRankingData(ranking.usersCompetitiveStats); - var myrank = await retriever.getUser(user.username); + var myrank = await retriever.getUser(cookie.username, cookie.token); setMyRankingData(myrank); } catch (error) { console.log(error); @@ -31,7 +31,7 @@ const RankingView = () => { e.preventDefault(); if(searchTerm.length!==0){ try { - const rank = await retriever.getUser(searchTerm); + const rank = await retriever.getUser(searchTerm, cookie.token); setMyRankingData(rank); } catch (error) { console.log(error); diff --git a/webapp/src/components/ranking/RankingView.test.js b/webapp/src/components/ranking/RankingView.test.js index 76c0506a..d8640b31 100644 --- a/webapp/src/components/ranking/RankingView.test.js +++ b/webapp/src/components/ranking/RankingView.test.js @@ -6,7 +6,8 @@ import i18en from 'i18next'; import RankingView from './RankingView'; import MockAdapter from 'axios-mock-adapter'; import { act } from 'react-dom/test-utils'; -import { UserContextProvider} from '../loginAndRegistration/UserContext'; +import Cookies from 'js-cookie' + i18en.use(initReactI18next).init({ resources: {}, lng: 'en', @@ -15,21 +16,20 @@ i18en.use(initReactI18next).init({ } }); global.i18en = i18en; - +Cookies.set('user', JSON.stringify({username:"myUser", token:"fasfda"})) const mockAxios = new MockAdapter(axios); describe('RankingView component', () => { - const user = { username: 'myUser' }; it('renders title', () => { act(()=>{ - render(); + render(); }) const text = screen.getByText(i18en.t('ranking.ranking')); expect(text).toBeInTheDocument(); }); it('renders Loading if the call to the gateway has not been done', () => { act(()=>{ - render(); + render(); }) const text = screen.getByText('Loading...'); expect(text).toBeInTheDocument(); @@ -102,12 +102,11 @@ describe('RankingView component', () => { } } ); - const user = { username: 'myUser' }; it('renders position all headers in the table',async ()=>{ await act(async () =>{ - await render(); + await render(); }) await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument()); @@ -118,7 +117,7 @@ describe('RankingView component', () => { it('renders position all users usernames',async ()=>{ await act(async () =>{ - await render(); + await render(); }) await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument()); @@ -136,7 +135,7 @@ describe('RankingView component', () => { it('renders position all users totalPoints',async ()=>{ await act(async () =>{ - await render(); + await render(); }) await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument()); @@ -153,7 +152,7 @@ describe('RankingView component', () => { it('renders position all users competitive games',async ()=>{ await act(async () =>{ - await render(); + await render(); }) await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument()); @@ -166,7 +165,7 @@ describe('RankingView component', () => { it('renders position all users competitive games',async ()=>{ await act(async () =>{ - await render(); + await render(); }) await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument()); diff --git a/webapp/src/translations/en/global.json b/webapp/src/translations/en/global.json index c076edbd..54f087cf 100644 --- a/webapp/src/translations/en/global.json +++ b/webapp/src/translations/en/global.json @@ -16,7 +16,8 @@ "en": "English", "es": "Spanish", "tk": "Turkish", - "language": "Language" + "language": "Language", + "logout":"Log Out" }, "instructions": { "title": "WIQ Instructions", @@ -37,7 +38,7 @@ }, "login": { "title": "Login", - "username_placeholder": "Username", + "username_placeholder": "Username or Email", "password_placeholder": "Password", "remember_me": "Remember me", "forgot_password": "Forgot password?", @@ -51,6 +52,7 @@ "repeat_password_placeholder": "Repeat password", "register_button": "Register", "login_link": "Do you have an account? Login here.", + "email_placeholder" : "Email", "very_weak_password": "Very weak password", "weak_password": "Weak password", "good_password": "Good password", @@ -60,7 +62,8 @@ "error_username_spaces": "Username cannot contain spaces", "error_password_minimum_length": "Password must be at least 8 characters long", "error_password_maximum_length": "Password cannot be over 64 characters long", - "error_username_in_use": "Username already in use" + "error_username_in_use": "Username already in use", + "error_wrong_email_format": "Wrong email format (user@example.com)" }, "gameMenu":{ "history_button":"View Historical Data", diff --git a/webapp/src/translations/es/global.json b/webapp/src/translations/es/global.json index 898263d1..092ecc74 100644 --- a/webapp/src/translations/es/global.json +++ b/webapp/src/translations/es/global.json @@ -17,7 +17,8 @@ "en": "Inglés", "es": "Español", "tk": "Turco", - "language": "Idioma" + "language": "Idioma", + "logout":"Desconectarse" }, "instructions": { @@ -40,7 +41,7 @@ }, "login": { "title": "Inicio de sesión", - "username_placeholder": "Nombre de usuario", + "username_placeholder": "Nombre de usuario o Correo Electrónico", "password_placeholder": "Contraseña", "remember_me": "Recordarme", "forgot_password": "¿Olvidaste tu contraseña?", @@ -55,6 +56,7 @@ "repeat_password_placeholder": "Repetir contraseña", "register_button": "Registrarse", "login_link": "¿Ya tienes una cuenta? Inicia sesión aquí.", + "email_placeholder" : "Correo electrónico", "very_weak_password": "Contraseña muy débil", "weak_password": "Contraseña débil", "good_password": "Contraseña buena", diff --git a/webapp/src/translations/tk/global.json b/webapp/src/translations/tk/global.json index 3c822aaf..8fa408ff 100644 --- a/webapp/src/translations/tk/global.json +++ b/webapp/src/translations/tk/global.json @@ -16,7 +16,8 @@ "en": "İngilizce", "es": "İspanyolca", "tk": "Türkçe", - "language": "Dil" + "language": "Dil", + "logout":"Log Out" }, "instructions": { "title": "WIQ Talimatları", diff --git a/webapp/src/utils/manageError.js b/webapp/src/utils/manageError.js new file mode 100644 index 00000000..ea56f89c --- /dev/null +++ b/webapp/src/utils/manageError.js @@ -0,0 +1,17 @@ +/** + * Receives an error found in a catch after doing some axios petition in the try + * returns {status: "4xx", error: "error message"} + * or {status: "500", error: "Internal server error"} + * @param {*} error + */ + +function manageError(error){ + if(error.response) + return {status : error.response.status, error : error.response.data.error}; + else //Some other error + return {status : 500, error : "Interanl server error"}; +} + +module.exports = { + manageError +}; \ No newline at end of file