diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cef2a143..bbf44fd1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -184,4 +184,4 @@ jobs: wget https://raw.githubusercontent.com/arquisoft/wiq_en1b/master/docker-compose.yml -O docker-compose.yml wget https://raw.githubusercontent.com/arquisoft/wiq_en1b/master/.env docker compose down - docker compose --profile prod up -d + docker compose --profile prod up -d --pull always diff --git a/docker-compose.yml b/docker-compose.yml index 89ffbe46..43d9de66 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -108,7 +108,7 @@ depends_on: - gatewayservice ports: - - "3000:3000" + - "80:80" prometheus: image: prom/prometheus diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index 49b59c0f..bab68145 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -9,12 +9,25 @@ const YAML = require('yaml') const jwt = require('jsonwebtoken'); const app = express(); const port = 8000; +//Setting up the email +const nodemailer = require('nodemailer'); + +const transporter = nodemailer.createTransport({ + service: 'Gmail', + auth: { + user: "wiqen1b@gmail.com", + pass: "akskfqgakjvcswyg", + }, +}); const authServiceUrl = process.env.AUTH_SERVICE_URL || 'http://localhost:8002'; const userServiceUrl = process.env.USER_SERVICE_URL || 'http://localhost:8001'; const questionServiceUrl = process.env.QUESTION_SERVICE_URL || 'http://localhost:8003'; const recordServiceUrl = process.env.RECORD_SERVICE_URL || 'http://localhost:8004'; + +var forgetPasswords = new Map() + app.use(cors()); app.use(express.json()); @@ -41,7 +54,50 @@ app.post('/adduser', async (req, res) => { try { // Forward the add user request to the user service const userResponse = await axios.post(userServiceUrl+'/adduser', req.body); - console.log(userResponse) + res.json(userResponse.data); + } catch (error) { + manageError(res, error); + + } +}); + +app.post('/forgetPassword', async (req, res) => { + try { + // Forward the forget password request to the user service + const userResponse = await axios.post(userServiceUrl+'/forgetPassword', req.body); + + let sixNumbers = getRandomSixDigitNumber(); + while(forgetPasswords.has(sixNumbers)) + sixNumbers = getRandomSixDigitNumber(); + + forgetPasswords.set(sixNumbers, userResponse.data.token) + await sendEmail(res, userResponse.data.email, userResponse.data.username, sixNumbers) + } catch (error) { + manageError(res, error); + + } +}); + +app.get('/tokenFromCode/:code', async (req, res) => { + try { + var code = parseInt(req.params.code); + if(forgetPasswords.has(code)){ + var token = forgetPasswords.get(code) + forgetPasswords.delete(code) + res.json({token: token}); + } + else + res.status(400).json({ error : "Invalid code" }); + } catch (error) { + manageError(res, error); + + } +}); + +app.post('/changePassword', verifyToken, async (req, res) => { + try { + // Forward the forget password request to the user service + const userResponse = await axios.post(userServiceUrl+'/changePassword', req.body); res.json(userResponse.data); } catch (error) { manageError(res, error); @@ -235,4 +291,30 @@ function manageError(res, error){ res.status(500).json({error : "Internal server error"}) } +function getRandomSixDigitNumber() { + const now = Date.now(); // Gets the current timestamp + const lastSixDigits = now.toString().slice(-6); // Gets the last 6 digits as a string + return parseInt(lastSixDigits, 10); // Converts it back to an integer +} + +async function sendEmail(res, email, username, numbers) { + // Configuración del correo + const mailOptions = { + from: process.env.EMAIL_USER, + to: email, + subject: 'Hello ' + username + ' this is the wiqen1b team', + text: 'We see that you have requested a password change.\n' + + 'Please introduce the code: ' + numbers + '. You have around 10 minutes to change your password \n' + + 'In case you have not requested a password change forget this email existance', + }; + + try { + // Envía el correo + await transporter.sendMail(mailOptions); + res.send('Email sent successfully'); + } catch (error) { + res.status(500).send('Error sending email'); + } +} + module.exports = server diff --git a/gatewayservice/gateway-service.test.js b/gatewayservice/gateway-service.test.js index a51f5f18..0517d123 100644 --- a/gatewayservice/gateway-service.test.js +++ b/gatewayservice/gateway-service.test.js @@ -2,6 +2,7 @@ const request = require('supertest'); const axios = require('axios'); const jwt = require('jsonwebtoken'); const app = require('./gateway-service'); +const nodemailer = require('nodemailer'); afterAll(async () => { app.close(); @@ -12,8 +13,6 @@ jest.mock('jsonwebtoken'); jest.mock('axios'); - - describe('Gateway Service with mocked micro services', () => { // Mock responses from external services @@ -24,6 +23,10 @@ describe('Gateway Service with mocked micro services', () => { return Promise.resolve({ data: { username: 'newuser' } }); } else if(url.endsWith('/record')){ return Promise.resolve({data : {user:'testuser'}}) + } else if(url.endsWith('/forgetPassword')){ + return Promise.resolve({data : { token: 'mockedToken', username : 'testuser', email:"example@example.com"}}) + } else if(url.endsWith('/changePassword')){ + return Promise.resolve({data : {token: 'mockedToken', username : 'testuser', email:"example@example.com"}}) } }); @@ -60,6 +63,14 @@ describe('Gateway Service with mocked micro services', () => { callback(null, "decoded"); }); + + //Mock nodemailer + jest.mock('nodemailer', () => ({ + createTransport: jest.fn().mockReturnValue({ + sendMail: jest.fn(), + }), + })); + // Test /login endpoint it('should forward login request to auth service', async () => { const response = await request(app) @@ -146,6 +157,42 @@ describe('Gateway Service with mocked micro services', () => { }); + //Test /forgetPassword + it('should forward the request and send an email', async () => { + const response = await request(app) + .post('/forgetPassword') + .send({ email: 'example@example.com', username: 'testuser'}); + expect(response.statusCode).toBe(200); + expect(response.text).toBe('Email sent successfully'); + }) + + //Test tokenFromCode/:code + it('should find a token', async () => { + //First generate the code:token + + const fixedTimestamp = 1683078000000; + jest.spyOn(Date, 'now').mockReturnValue(fixedTimestamp); + + await request(app) + .post('/forgetPassword') + .send({ email: 'example@example.com', username: 'testuser'}); + const response = await request(app).get('/tokenFromCode/000000'); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty('token', "mockedToken"); + }) + + //Test /changePassword + it('should forward the request', async () => { + const response = await request(app) + .post('/changePassword') + .send({ username: 'testuser', password: 'newpassword' }) + .set('token', 'valorDelToken'); + + expect(response.statusCode).toBe(200); + expect(response.body.username).toBe('testuser'); + }) + }); function checkRecord(response){ diff --git a/gatewayservice/openapi.yaml b/gatewayservice/openapi.yaml index 6755daa9..65cfed8f 100644 --- a/gatewayservice/openapi.yaml +++ b/gatewayservice/openapi.yaml @@ -84,6 +84,153 @@ paths: type: string description: Error information. example: Internal Server Error + /forgetPassword: + post: + summary: Sends a forget password alert to the server + operationId: forgetPassword + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + description: User email + example: user@example.com + username: + type: string + description: User name + example: student + responses: + '200': + description: Email sent successfully + content: + application/json: + schema: + type: string + example: "Email sent successfully" + '400': + description: Failed to find user + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error information. + example: No user found, review credentials + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error information. + example: Internal Server Error + /tokenFromCode{code}: + get: + summary: Get a token from a 6 digit code + parameters: + - name: code + in: path + required: true + schema: + type: string + responses: + '200': + description: Code found returns token + content: + application/json: + schema: + type: object + properties: + token: + type: string + '400': + description: Invalid code + content: + application/json: + schema: + type: string + example: "Invalid code" + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error information. + example: Internal Server Error + /changePassword: + post: + summary: Changes the password of the authorized user + parameters: + - name: token + in: header + schema: + type: string + operationId: changePassword + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + description: User email + example: user@example.com + username: + type: string + description: User name + example: student + password: + type: string + description: User password + example: password123 + repeatPassword: + type: string + description: Userpassword + example: password123 + responses: + '200': + description: Email sent successfully + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + '400': + description: Failed to find user + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error information. + example: No user found, review credentials + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error information. + example: Internal Server Error /login: post: summary: Log in to the system. diff --git a/gatewayservice/package-lock.json b/gatewayservice/package-lock.json index 48729129..8748d7db 100644 --- a/gatewayservice/package-lock.json +++ b/gatewayservice/package-lock.json @@ -15,6 +15,7 @@ "express-openapi": "^12.1.3", "express-prom-bundle": "^7.0.0", "jsonwebtoken": "^9.0.2", + "nodemailer": "^6.9.13", "swagger-ui-express": "^5.0.0", "yaml": "^2.4.1" }, @@ -3790,6 +3791,14 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, + "node_modules/nodemailer": { + "version": "6.9.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", + "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/gatewayservice/package.json b/gatewayservice/package.json index 0757ca8f..756bc677 100644 --- a/gatewayservice/package.json +++ b/gatewayservice/package.json @@ -22,8 +22,9 @@ "cors": "^2.8.5", "express": "^4.18.2", "express-openapi": "^12.1.3", - "jsonwebtoken": "^9.0.2", "express-prom-bundle": "^7.0.0", + "jsonwebtoken": "^9.0.2", + "nodemailer": "^6.9.13", "swagger-ui-express": "^5.0.0", "yaml": "^2.4.1" }, diff --git a/questionGenerator/src/main/java/Main.java b/questionGenerator/src/main/java/Main.java index e5c144aa..cd0480f8 100644 --- a/questionGenerator/src/main/java/Main.java +++ b/questionGenerator/src/main/java/Main.java @@ -1,6 +1,7 @@ package main.java; import java.util.ArrayList; +import java.util.Date; import java.util.List; import main.java.questionGenerator.QuestionGenerator; @@ -23,15 +24,16 @@ public class Main { QuestionType.SIZE, QuestionType.HEAD_OF_GOVERMENT}; //, QuestionType.VIDEOGAME_DEVELOPER, QuestionType.VIDEOGAME_PUBLISHER, QuestionType.VIDEOGAME_GENRE, QuestionType.VIDEOGAME_COUNTRY}; - //private static final int NUMBER_OF_QUESTIONS = 50; - private static final int NUMBER_OF_QUESTIONS = 100; + private static final int NUMBER_OF_QUESTIONS = 20; + //private static final int NUMBER_OF_QUESTIONS = 75; //private static final int NUMBER_OF_QUESTIONS = 3; //private static final int NUMBER_OF_QUESTIONS = 1; public static void main(String[] args) { + List questions = generate().stream().map(q -> q.getJSON().toString()).toList(); while(true) { - List questions = generate().stream().map(q -> q.getJSON().toString()).toList(); QuestionRepository.getInstance().populate(questions); + questions = generate().stream().map(q -> q.getJSON().toString()).toList(); try { Thread.sleep(TIME_SKIP); } catch (InterruptedException e) { @@ -44,18 +46,18 @@ private static List generate() { List questions = new ArrayList(); for(String lang : languages) { qg.setLanguageCode(lang); - for(QuestionType type: types) + for(QuestionType type: types){ + System.out.println(String.format("Starting Type: %s Language: %s at: %s", type, lang, new Date())); questions.addAll(run(qg, type, NUMBER_OF_QUESTIONS)); + System.out.println(String.format("Type: %s Language: %s Finished", type, lang)); + } + } return questions; } private static List run(QuestionGenerator qg, QuestionType type, int numberOfQuestions) { List questions = qg.generateQuestions(type, numberOfQuestions); - for(int i=0; i questions = qg.generateQuestions(type, numberOfQuestions); - for(int i=0; i q.getJSON().toString()).toList()); } } diff --git a/questionGenerator/src/main/java/questionGenerator/QuestionGenerator.java b/questionGenerator/src/main/java/questionGenerator/QuestionGenerator.java index 1fd27702..f8e4717b 100644 --- a/questionGenerator/src/main/java/questionGenerator/QuestionGenerator.java +++ b/questionGenerator/src/main/java/questionGenerator/QuestionGenerator.java @@ -129,9 +129,9 @@ public void setLanguageCode(String languageCode) { } private long getSampleSize(int amount) { - long value = amount * 10; - if(value<1000) - return 1000; + long value = amount * 2; + if(value>500) + return 500; return value; } diff --git a/questionGenerator/src/main/java/questionGenerator/generator/answersAreEntites/AbstractAnswersAreEntites.java b/questionGenerator/src/main/java/questionGenerator/generator/answersAreEntites/AbstractAnswersAreEntites.java index 963104e7..49cf2ea0 100644 --- a/questionGenerator/src/main/java/questionGenerator/generator/answersAreEntites/AbstractAnswersAreEntites.java +++ b/questionGenerator/src/main/java/questionGenerator/generator/answersAreEntites/AbstractAnswersAreEntites.java @@ -47,7 +47,7 @@ private String getAnswer(String id) throws Exception{ public List getWrongAnswers(String rightAnswer) throws Exception { List entites = new ArrayList<>(); try { - entites = EntityGenerator.getEntities(type, getSampleSize(), getPropertyId()); + entites = EntityGenerator.getEntities(type, 20, getPropertyId()); } catch (IOException e) { e.printStackTrace(); } diff --git a/questionGenerator/src/main/java/questionGenerator/question/Question.java b/questionGenerator/src/main/java/questionGenerator/question/Question.java index 7068cbd6..7e9a33f3 100644 --- a/questionGenerator/src/main/java/questionGenerator/question/Question.java +++ b/questionGenerator/src/main/java/questionGenerator/question/Question.java @@ -9,7 +9,6 @@ public class Question { private String question; private List answers; - private int number = -1; private String language; private QuestionType type; @@ -43,17 +42,12 @@ public void setAnswers(List answers) { public void addRightAnswer(String answer) { answers.add(0, answer); } - - public void setNumber(int number){ - this.number = number; - } public JSONObject getJSON() { JSONObject json = new JSONObject(); json.accumulate("question", question); for(String s : answers) json.accumulate("answers", s); - if(number != -1) json.accumulate("number", number); //Para que los tests pasen json.accumulate("language", language); json.accumulate("type", type); return json; diff --git a/questionGenerator/src/test/java/questionGenerator/QuestionGeneratorTests.java b/questionGenerator/src/tests/java/questionGenerator/QuestionGeneratorTests.java similarity index 97% rename from questionGenerator/src/test/java/questionGenerator/QuestionGeneratorTests.java rename to questionGenerator/src/tests/java/questionGenerator/QuestionGeneratorTests.java index fcceef44..3421d15e 100644 --- a/questionGenerator/src/test/java/questionGenerator/QuestionGeneratorTests.java +++ b/questionGenerator/src/tests/java/questionGenerator/QuestionGeneratorTests.java @@ -1,4 +1,4 @@ -package test.java.questionGenerator; +package tests.java.questionGenerator; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -22,7 +22,8 @@ public class QuestionGeneratorTests { private QuestionGenerator qg = QuestionGenerator.getInstance(); - private static QuestionType[] types = {QuestionType.POPULATION, QuestionType.CAPITAL, QuestionType.SIZE, QuestionType.LANGUAGE}; + private static QuestionType[] types = {QuestionType.POPULATION, QuestionType.CAPITAL, QuestionType.POPULATION, + QuestionType.SIZE, QuestionType.HEAD_OF_GOVERMENT}; @Test void testGenerateQuestionsEnglish() { diff --git a/questionGenerator/src/test/java/questionGenerator/generator/CapitalGeneratorTests.java b/questionGenerator/src/tests/java/questionGenerator/generator/CapitalGeneratorTests.java similarity index 98% rename from questionGenerator/src/test/java/questionGenerator/generator/CapitalGeneratorTests.java rename to questionGenerator/src/tests/java/questionGenerator/generator/CapitalGeneratorTests.java index 9403bed4..1bcb01d3 100644 --- a/questionGenerator/src/test/java/questionGenerator/generator/CapitalGeneratorTests.java +++ b/questionGenerator/src/tests/java/questionGenerator/generator/CapitalGeneratorTests.java @@ -1,4 +1,4 @@ -package test.java.questionGenerator.generator; +package tests.java.questionGenerator.generator; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/questionGenerator/src/test/java/questionGenerator/generator/DirectorGeneratorTests.java b/questionGenerator/src/tests/java/questionGenerator/generator/DirectorGeneratorTests.java similarity index 98% rename from questionGenerator/src/test/java/questionGenerator/generator/DirectorGeneratorTests.java rename to questionGenerator/src/tests/java/questionGenerator/generator/DirectorGeneratorTests.java index 0fe21eff..51de4a8b 100644 --- a/questionGenerator/src/test/java/questionGenerator/generator/DirectorGeneratorTests.java +++ b/questionGenerator/src/tests/java/questionGenerator/generator/DirectorGeneratorTests.java @@ -1,4 +1,4 @@ -package test.java.questionGenerator.generator; +package tests.java.questionGenerator.generator; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/questionGenerator/src/test/java/questionGenerator/generator/HeadOfGovernmentGeneratorTest.java b/questionGenerator/src/tests/java/questionGenerator/generator/HeadOfGovernmentGeneratorTest.java similarity index 98% rename from questionGenerator/src/test/java/questionGenerator/generator/HeadOfGovernmentGeneratorTest.java rename to questionGenerator/src/tests/java/questionGenerator/generator/HeadOfGovernmentGeneratorTest.java index e432648a..f78b05a5 100644 --- a/questionGenerator/src/test/java/questionGenerator/generator/HeadOfGovernmentGeneratorTest.java +++ b/questionGenerator/src/tests/java/questionGenerator/generator/HeadOfGovernmentGeneratorTest.java @@ -1,4 +1,4 @@ -package test.java.questionGenerator.generator; +package tests.java.questionGenerator.generator; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/questionGenerator/src/test/java/questionGenerator/generator/LanguageGeneratorTests.java b/questionGenerator/src/tests/java/questionGenerator/generator/LanguageGeneratorTests.java similarity index 98% rename from questionGenerator/src/test/java/questionGenerator/generator/LanguageGeneratorTests.java rename to questionGenerator/src/tests/java/questionGenerator/generator/LanguageGeneratorTests.java index ecc7f620..6a7dc251 100644 --- a/questionGenerator/src/test/java/questionGenerator/generator/LanguageGeneratorTests.java +++ b/questionGenerator/src/tests/java/questionGenerator/generator/LanguageGeneratorTests.java @@ -1,4 +1,4 @@ -package test.java.questionGenerator.generator; +package tests.java.questionGenerator.generator; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/questionGenerator/src/test/java/questionGenerator/generator/PopulationGeneratorTests.java b/questionGenerator/src/tests/java/questionGenerator/generator/PopulationGeneratorTests.java similarity index 98% rename from questionGenerator/src/test/java/questionGenerator/generator/PopulationGeneratorTests.java rename to questionGenerator/src/tests/java/questionGenerator/generator/PopulationGeneratorTests.java index 7084ea0e..114c0c25 100644 --- a/questionGenerator/src/test/java/questionGenerator/generator/PopulationGeneratorTests.java +++ b/questionGenerator/src/tests/java/questionGenerator/generator/PopulationGeneratorTests.java @@ -1,4 +1,4 @@ -package test.java.questionGenerator.generator; +package tests.java.questionGenerator.generator; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/questionGenerator/src/test/java/questionGenerator/generator/SizeGeneratorTests.java b/questionGenerator/src/tests/java/questionGenerator/generator/SizeGeneratorTests.java similarity index 98% rename from questionGenerator/src/test/java/questionGenerator/generator/SizeGeneratorTests.java rename to questionGenerator/src/tests/java/questionGenerator/generator/SizeGeneratorTests.java index 824dc59b..1d1ade18 100644 --- a/questionGenerator/src/test/java/questionGenerator/generator/SizeGeneratorTests.java +++ b/questionGenerator/src/tests/java/questionGenerator/generator/SizeGeneratorTests.java @@ -1,4 +1,4 @@ -package test.java.questionGenerator.generator; +package tests.java.questionGenerator.generator; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/questionGenerator/src/test/java/questionGenerator/generator/VideogameCountryGeneratorTests.java b/questionGenerator/src/tests/java/questionGenerator/generator/VideogameCountryGeneratorTests.java similarity index 98% rename from questionGenerator/src/test/java/questionGenerator/generator/VideogameCountryGeneratorTests.java rename to questionGenerator/src/tests/java/questionGenerator/generator/VideogameCountryGeneratorTests.java index aae764bd..b71acf02 100644 --- a/questionGenerator/src/test/java/questionGenerator/generator/VideogameCountryGeneratorTests.java +++ b/questionGenerator/src/tests/java/questionGenerator/generator/VideogameCountryGeneratorTests.java @@ -1,4 +1,4 @@ -package test.java.questionGenerator.generator; +package tests.java.questionGenerator.generator; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/questionGenerator/src/test/java/questionGenerator/generator/VideogameDeveloperGeneratorTests.java b/questionGenerator/src/tests/java/questionGenerator/generator/VideogameDeveloperGeneratorTests.java similarity index 98% rename from questionGenerator/src/test/java/questionGenerator/generator/VideogameDeveloperGeneratorTests.java rename to questionGenerator/src/tests/java/questionGenerator/generator/VideogameDeveloperGeneratorTests.java index 99ac8c8b..4909bdc0 100644 --- a/questionGenerator/src/test/java/questionGenerator/generator/VideogameDeveloperGeneratorTests.java +++ b/questionGenerator/src/tests/java/questionGenerator/generator/VideogameDeveloperGeneratorTests.java @@ -1,4 +1,4 @@ -package test.java.questionGenerator.generator; +package tests.java.questionGenerator.generator; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/questionGenerator/src/test/java/questionGenerator/generator/VideogameGenreGeneratorTests.java b/questionGenerator/src/tests/java/questionGenerator/generator/VideogameGenreGeneratorTests.java similarity index 98% rename from questionGenerator/src/test/java/questionGenerator/generator/VideogameGenreGeneratorTests.java rename to questionGenerator/src/tests/java/questionGenerator/generator/VideogameGenreGeneratorTests.java index b3701fde..29e59362 100644 --- a/questionGenerator/src/test/java/questionGenerator/generator/VideogameGenreGeneratorTests.java +++ b/questionGenerator/src/tests/java/questionGenerator/generator/VideogameGenreGeneratorTests.java @@ -1,4 +1,4 @@ -package test.java.questionGenerator.generator; +package tests.java.questionGenerator.generator; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/questionGenerator/src/test/java/questionGenerator/generator/VideogamePublisherGeneratorTests.java b/questionGenerator/src/tests/java/questionGenerator/generator/VideogamePublisherGeneratorTests.java similarity index 98% rename from questionGenerator/src/test/java/questionGenerator/generator/VideogamePublisherGeneratorTests.java rename to questionGenerator/src/tests/java/questionGenerator/generator/VideogamePublisherGeneratorTests.java index 8531651d..8576535a 100644 --- a/questionGenerator/src/test/java/questionGenerator/generator/VideogamePublisherGeneratorTests.java +++ b/questionGenerator/src/tests/java/questionGenerator/generator/VideogamePublisherGeneratorTests.java @@ -1,4 +1,4 @@ -package test.java.questionGenerator.generator; +package tests.java.questionGenerator.generator; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/questionGenerator/src/test/java/questionGenerator/question/QuestionTests.java b/questionGenerator/src/tests/java/questionGenerator/question/QuestionTests.java similarity index 97% rename from questionGenerator/src/test/java/questionGenerator/question/QuestionTests.java rename to questionGenerator/src/tests/java/questionGenerator/question/QuestionTests.java index fc5253fd..6453d09f 100644 --- a/questionGenerator/src/test/java/questionGenerator/question/QuestionTests.java +++ b/questionGenerator/src/tests/java/questionGenerator/question/QuestionTests.java @@ -1,4 +1,4 @@ -package test.java.questionGenerator.question; +package tests.java.questionGenerator.question; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; diff --git a/users/userservice/user-service.js b/users/userservice/user-service.js index a6ea8a09..7c230074 100644 --- a/users/userservice/user-service.js +++ b/users/userservice/user-service.js @@ -31,10 +31,11 @@ function validateRequiredFields(req, requiredFields) { } } - 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 there are not here is because they dont need to be, it has being check before which need to be here + let email = req.body.email ? req.body.email.toString() : "example@example.com"; + let username = req.body.username ? req.body.username.toString() : "example"; + let password = req.body.password ? req.body.password.toString() : "123456789"; + let repeatPassword = req.body.repeatPassword ? req.body.repeatPassword.toString() : "123456789"; if(!validateEmail(email)){ //User put a wrong format email @@ -74,7 +75,6 @@ app.post('/adduser', async (req, res) => { } catch(error){ res.status(400).json({ error : error.message }); - console.log(res) return } @@ -106,7 +106,72 @@ app.post('/adduser', async (req, res) => { res.json({ token: token, username: savedUser.username, email: savedUser.email}); } catch (error) { res.status(400).json({ error: error.message }); - }}); + }} +); + +app.post('/forgetPassword', async (req, res) => { + try { + // Check if required fields are present in the request body + try{ + validateRequiredFields(req, ['email', 'username']); + } + catch(error){ + res.status(400).json({ error : error.message }); + return + } + //Check there is a user with that name + const userUsername = await User.findOne({username: req.body.username.toString()}); + + if(!userUsername || userUsername.email !== req.body.email) + return res.status(400).json({error : "No user found, review credentials"}) + + + const token = jwt.sign({ userId: userUsername._id }, (process.env.JWT_KEY??'my-key'), { expiresIn: '15m' }); + + res.json({ token: token, username: userUsername.username, email: userUsername.email}); + } catch (error) { + res.status(400).json({ error: error.message }); + }} +); + +app.post('/changePassword', async (req, res) => { + try { + // Check if required fields are present in the request body + try{ + validateRequiredFields(req, ['email', 'username', 'password', 'repeatPassword']); + } + catch(error){ + res.status(400).json({ error : error.message }); + return + } + + + //Check there is a user with that name + const userUsername = await User.findOne({username: req.body.username.toString()}); + + if(!userUsername || userUsername.email !== req.body.email) + return res.status(400).json({error : "No user found, review credentials"}) + + // Encrypt the password before saving it + const hashedPassword = await bcrypt.hash(req.body.password, 10); + + const result = await User.updateOne( + { _id: userUsername._id }, + { $set: { password: hashedPassword } } + ); + + if (result.nModified === 0) { + res.status(404).send('User not found or no change'); + } else { + const token = jwt.sign({ userId: userUsername._id }, (process.env.JWT_KEY??'my-key'), { expiresIn: '1h' }); + res.json({ token: token, username: userUsername.username, email: userUsername.email}); + } + + + } catch (error) { + res.status(400).json({ error: error.message }); + }} +); const server = app.listen(port, () => { console.log(`User Service listening at http://localhost:${port}`); diff --git a/users/userservice/user-service.test.js b/users/userservice/user-service.test.js index ab9fa21e..19cff1da 100644 --- a/users/userservice/user-service.test.js +++ b/users/userservice/user-service.test.js @@ -1,5 +1,13 @@ const request = require('supertest'); const { MongoMemoryServer } = require('mongodb-memory-server'); +const jwt = require('jsonwebtoken'); + +// Mock the `jsonwebtoken` module +jest.mock('jsonwebtoken', () => ({ + sign: jest.fn(), +})); +const expectedToken = "mockedToken" +jwt.sign.mockReturnValue(expectedToken); let mongoServer; let app; @@ -32,7 +40,7 @@ afterEach(async () => { }; }) -describe('User Service', () => { +describe('User Service /adduser', () => { it('should add a new user on POST /adduser', async () => { const response = await request(app).post('/adduser').send(newUser); expect(response.status).toBe(200); @@ -46,11 +54,11 @@ describe('User Service', () => { }); it('Should not register user /adduser', async () => { - newUser.email = 'example2@example.com'; + 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'); + 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 () => { @@ -59,7 +67,7 @@ describe('User Service', () => { const response = await request(app).post('/adduser').send(newUser); expect(response.status).toBe(400); expect(response.body).toHaveProperty('error', 'Email already in use'); - }); + }); }); @@ -110,4 +118,47 @@ describe('User service validations', () => { function setPassword(newPassword){ newUser.password = newPassword; newUser.repeatPassword = newPassword; -} \ No newline at end of file +} + + +describe('User service /forgetPassword', () => { + it('should return a token when given good credentials', async () => { + const response = await request(app).post('/forgetPassword').send({email:newUser.email, username:newUser.username}); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('token', expectedToken); + }) + + it('should show missing field email', async () => { + const response = await request(app).post('/forgetPassword').send({username:newUser.username}); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('error', 'Missing required field: email'); + }) + + it('should show no user found', async () => { + const response = await request(app).post('/forgetPassword').send({email: "add" + newUser.email, username:newUser.username}); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('error', 'No user found, review credentials'); + }) +}) + +describe('User service /changePassword', () => { + it('should return a token when given good credentials', async () => { + setPassword('123456789') + const response = await request(app).post('/changePassword').send(newUser); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('token', expectedToken); + }) + + it('should show missing field email', async () => { + const response = await request(app).post('/changePassword').send({username:newUser.username}); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('error', 'Missing required field: email'); + }) + + it('should show no user found', async () => { + newUser.email = "add" + newUser.email; + const response = await request(app).post('/changePassword').send(newUser); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('error', 'No user found, review credentials'); + }) +}) \ No newline at end of file diff --git a/webapp/Dockerfile b/webapp/Dockerfile index 68c3b59f..ce09edc7 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -8,6 +8,7 @@ RUN npm install ARG API_URI="http://localhost:8000" ENV REACT_APP_API_ENDPOINT=$API_URI +ENV PORT=80 #Create an optimized version of the webapp RUN npm run build diff --git a/webapp/src/App.js b/webapp/src/App.js index dd6ec5bd..07b0c74e 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -17,6 +17,7 @@ import HistoricalView from './components/HistoricalData/HistoricalView'; import Cookies from 'js-cookie'; import GameConfigurator from './components/GameConfigurator/GameConfigurator'; import RankingView from './components/ranking/RankingView'; +import ForgetPassword from './components/ForgetPassword/ForgetPassword' function App() { @@ -45,6 +46,7 @@ function App() { : }/> : } /> } /> + } /> } /> diff --git a/webapp/src/components/ForgetPassword/ForgetPassword.js b/webapp/src/components/ForgetPassword/ForgetPassword.js new file mode 100644 index 00000000..c00bc349 --- /dev/null +++ b/webapp/src/components/ForgetPassword/ForgetPassword.js @@ -0,0 +1,355 @@ +import "../../custom.css"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; +import ForgetPasswordFunctions from "./ForgetPasswordFunctions"; +import Cookies from "js-cookie"; +import zxcvbn from "zxcvbn"; +import { useNavigate } from "react-router-dom"; + +import { validateEmail, validatePasswords, validateUsername } from "../../utils/utils"; + +const forgetPasswordFunctions = new ForgetPasswordFunctions(); +export default function ForgotPassword() { + const navigate = useNavigate(); + const [t] = useTranslation("global"); + + // Component state variables + const [emailStatus, setEmailStatus] = useState("ASK_EMAIL"); + const [email, setEmail] = useState(""); + const [username, setUsername] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [repeatPassword, setRepeatPassword] = useState(""); + const [passwordStrength, setPasswordStrength] = useState({}); + const [passwordStrengthText, setPasswordStrengthText] = useState(""); + const [submitErrors, setSubmitErrors] = useState([]); + const [errors, setErrors] = useState([]); + + // Function to handle state transition and send email + const handleSubmitStateToPending = async (event) => { + event.preventDefault(); + + const newSubmitErrors = []; + + validateEmail(newSubmitErrors, email); + validateUsername(newSubmitErrors, username); + + setSubmitErrors(newSubmitErrors); + + if (newSubmitErrors.length === 0) { + + try { + setEmailStatus("PENDING"); + await sendEmail(); + setSubmitErrors([]) + } catch (error) { + setErrors([error.message]) + } + + } + }; + + // Function to send the email + const sendEmail = async () => { + try{ + const received = await forgetPasswordFunctions.sendEmail(email, username); + if (received) { + setEmailStatus("RECEIVED"); + } else { + console.log("Email not received"); + } + } + catch(error){ + setErrors([error.message]) + } + }; + + // Function to obtain the code from input fields + async function obtenerCodigo(event) { + event.preventDefault(); + let inputs = document.querySelectorAll(".codeGrid input"); + let codigo = ""; + let inputsNotEmpty = false; + inputs.forEach((input) => { + codigo += input.value; + inputsNotEmpty = inputsNotEmpty || input.value === "" + }); + + if(!inputsNotEmpty){ + try { + const codigoNumerico = parseInt(codigo, 10); + await forgetPasswordFunctions.tokenFromCode(codigoNumerico); + setEmailStatus("RESTORE_PASSWORD"); + } catch (error) { + setErrors([error.message]) + } + } + + } + + // Function to restore the password + async function restorePassword(event) { + event.preventDefault(); + + const newSubmitErrors = []; + + validatePasswords(newSubmitErrors, newPassword, repeatPassword); + + setSubmitErrors(newSubmitErrors); + + if (newSubmitErrors.length === 0) { + + const response = await forgetPasswordFunctions.changePassword( + email, + username, + newPassword, + repeatPassword + ); + + const 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(); + } + } + + // Function to show errors + const showErrors = () => { + if (submitErrors.length > 0) { + return submitErrors.map((error, index) => ( +

+ {t(error)} +

+ )); + } + else{ + if(errors.length > 0){ + return errors.map((error, index) => ( +

+ {t(error)} +

+ )); + } else + return null; + } + }; + + // Function to handle password change and determine strength + const handlePasswordChange = (e) => { + const newPassword = e.target.value; + setNewPassword(newPassword); + + const newStrength = zxcvbn(newPassword); + + switch (newStrength.score) { + case 0: + case 1: + setPasswordStrengthText("addUser.very_weak_password"); + break; + case 2: + setPasswordStrengthText("addUser.weak_password"); + break; + case 3: + setPasswordStrengthText("addUser.good_password"); + break; + case 4: + setPasswordStrengthText("addUser.strong_password"); + break; + default: + setPasswordStrengthText("addUser.very_weak_password"); + break; + } + setPasswordStrength(newStrength); + }; + + // Main component return block with conditional rendering + return ( +
+
+
+
+ {(() => { + switch (emailStatus) { + case "ASK_EMAIL": + return ( + + ); + case "PENDING": + return (); + case "RECEIVED": + return (); + case "RESTORE_PASSWORD": + return ( + + ); + default: + return

There has been an error

; + } + })()} +
+
+
+
+ ); +} + +// Sub-components used in the main component +function AskEmailUsername({ email, setEmail, username, setUsername, t, handleSubmit, showErrors }) { + return ( +
+

{t("forgotPassword.enter_email")}

+ {showErrors()} +
+

{t("addUser.email_placeholder")}:

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

{t("addUser.username_placeholder")}:

+ setUsername(e.target.value)} + /> +
+ +
+
+ ); +} + +function PendingComponent({ t }) { + return ( +
+

{t("forgotPassword.sending_title")}

+

{t("forgotPassword.sending_paragraph")}

+
+ ); +} + +function EnterCode({ t, obtainCode, showErrors }) { + return ( +
+
+

{t("forgotPassword.enter_code")}

+ {showErrors()} +
+ + + + + + +
+ +
+
+
+ ); +} + +function RestorePassword({ + email, + username, + passwordStrength, + passwordStrengthText, + newPassword, + handlePasswordChange, + repeatPassword, + setRepeatPassword, + handleSubmit, + t, + showErrors +}) { + return ( +
+

{t("forgotPassword.enter_password")}

+ {showErrors()} +
+

{t("addUser.email_placeholder")}:

+ +
+
+

{t("addUser.username_placeholder")}:

+ +
+
+

{t("addUser.password_placeholder")}:

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

{t("addUser.repeat_password_placeholder")}:

+ setRepeatPassword(e.target.value)} + /> +
+ +
+
+ ); +} diff --git a/webapp/src/components/ForgetPassword/ForgetPassword.test.js b/webapp/src/components/ForgetPassword/ForgetPassword.test.js new file mode 100644 index 00000000..b108e9a3 --- /dev/null +++ b/webapp/src/components/ForgetPassword/ForgetPassword.test.js @@ -0,0 +1,75 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import ForgetPassword from './ForgetPassword'; +import { MemoryRouter } from 'react-router-dom'; +import { act } from 'react-dom/test-utils'; +import { initReactI18next } from 'react-i18next'; +import i18en from 'i18next'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import userEvent from '@testing-library/user-event'; +import Cookies from 'js-cookie'; + +i18en.use(initReactI18next).init({ + resources: {}, + lng: 'en', + interpolation:{ + escapeValue: false, + } +}); +global.i18en = i18en; +const mockAxios = new MockAdapter(axios); +describe('ForgetPassword Component', () => { + test('renders Ask Email component', async () => { + act( () => { + render(); + }); + await waitFor(() => expect(screen.getByText(i18en.t("forgotPassword.enter_email"))).toBeInTheDocument()); + expect(screen.getByText(/addUser.email_placeholder/)).toBeInTheDocument(); + expect(screen.getByText(/addUser.username_placeholder/)).toBeInTheDocument(); + }); + test('renders AskForCode component', async () => { + var code = 111111 + var token = "mockedToken" + act( () => { + render(); + }); + await waitFor(() => expect(screen.getByText(i18en.t("forgotPassword.enter_email"))).toBeInTheDocument()); + const emailInput = screen.getByPlaceholderText(/addUser.email_placeholder/i); + const usernameInput = screen.getByPlaceholderText(/addUser.username_placeholder/i); + //introducimos email y username + expect(screen.getByText(/addUser.email_placeholder/)).toBeInTheDocument(); + userEvent.type(emailInput, 'test@example.com'); + userEvent.type(usernameInput, 'testuser'); + + // Hacer clic en el botón de enviar + const submitButton = screen.getByText(/forgotPassword.enter_email_button/i); // Ajusta el texto según el texto real del botón + userEvent.click(submitButton); + mockAxios.onPost('http://localhost:8000/forgetPassword').reply(200, "Email sent"); + act(async ()=>{ + await waitFor(async () => expect(screen.getByText(i18en.t("forgotPassword.enter_code")).toBeInTheDocument())); + const inputs = screen.getAllByPlaceholderText('X'); + // Introducir el mismo carácter en todos los inputs + userEvent.type(inputs[0], '1'); // Introducir el carácter '1', puedes cambiarlo al que desees + userEvent.type(inputs[1], '1'); + userEvent.type(inputs[2], '1'); + userEvent.type(inputs[3], '1'); + userEvent.type(inputs[4], '1'); + userEvent.type(inputs[5], '1'); + // Simula un clic en el botón de submit + fireEvent.click(screen.getByText(/"forgotPassword.send_code"/)); + mockAxios.onGet('http://localhost:8000/tokenFromCode/' + code).reply(200, + {token: token}); + //llegamos al replace + await waitFor(async () => expect(screen.getByText(i18en.t("forgotPassword.enter_password")).toBeInTheDocument())); + + mockAxios.onPost('http://localhost:8000/changePassword').reply(200, { token: token, username: username}); + //me redirigen a game Menu + await waitFor(async () => expect(screen.getByText(i18en.t('gameMenu.title')).toBeInTheDocument())); + //la cookie queda bien seteada + expect(Cookies.get('user')).toHaveProperty('token', token); + }); + }); + + +}); diff --git a/webapp/src/components/ForgetPassword/ForgetPasswordFunctions.js b/webapp/src/components/ForgetPassword/ForgetPasswordFunctions.js new file mode 100644 index 00000000..3ff1478d --- /dev/null +++ b/webapp/src/components/ForgetPassword/ForgetPasswordFunctions.js @@ -0,0 +1,51 @@ +import axios from 'axios'; + +class ForgetPasswordFunctions{ + + constructor(){ + this.apiUrl = (process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'); + this.token = ""; + } + + async sendEmail(email, username){ + try { + const response = await axios.post(this.apiUrl + '/forgetPassword', { email, username}); + return !!response.data; + } catch (error) { + throw new Error(error.message) + } + } + + async tokenFromCode(code){ + try{ + const response = await axios.get(this.apiUrl+'/tokenFromCode/'+code) + this.token=response.data.token; + return response.data.token; + }catch (error) { + throw new Error(error.message) + } + } + async changePassword(email, username, password, repeatPassword){ + try{ + const response = await axios.post(this.apiUrl+'/changePassword', + { + email, + username, + password, + repeatPassword + }, + { + headers: { + 'token': this.token + } + } + ) + return response; + }catch (error) { + throw new Error(error.message) + } + } + +} + +export default ForgetPasswordFunctions; diff --git a/webapp/src/components/GameConfigurator/GameConfigurator.js b/webapp/src/components/GameConfigurator/GameConfigurator.js index 4c008e80..3fe6d1cd 100644 --- a/webapp/src/components/GameConfigurator/GameConfigurator.js +++ b/webapp/src/components/GameConfigurator/GameConfigurator.js @@ -51,9 +51,7 @@ function GameConfigurator(){ />

-


-

{t("gameConfigurator.competi_game")}

{t("gameConfigurator.rules_competi")}

{/* Botones para jugar un juego personalizado o competitivo */} diff --git a/webapp/src/components/loginAndRegistration/AddUser.js b/webapp/src/components/loginAndRegistration/AddUser.js index 66683cc6..cabc827a 100644 --- a/webapp/src/components/loginAndRegistration/AddUser.js +++ b/webapp/src/components/loginAndRegistration/AddUser.js @@ -8,7 +8,7 @@ import { useNavigate } from 'react-router-dom'; import zxcvbn from "zxcvbn"; import Cookies from 'js-cookie'; -import { manageError } from "../../utils/manageError"; +import { manageError, validateEmail, validateUsername, validatePasswords } from "../../utils/utils"; const AddUser = () => { const navigate = useNavigate(); @@ -23,46 +23,16 @@ const AddUser = () => { const [passwordStrengthText, setPasswordStrengthText] = 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(); 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"); - } + validateEmail(newSubmitErrors, email); + validateUsername(newSubmitErrors, username); + validatePasswords(newSubmitErrors, password, repeatPassword); + setSubmitErrors(newSubmitErrors); diff --git a/webapp/src/components/loginAndRegistration/Login.js b/webapp/src/components/loginAndRegistration/Login.js index bc7fd00c..c2a4b301 100644 --- a/webapp/src/components/loginAndRegistration/Login.js +++ b/webapp/src/components/loginAndRegistration/Login.js @@ -59,7 +59,7 @@ const Login = () => { - + @@ -79,6 +79,15 @@ function LinkRegister() { ); } +function LinkForgetPassword(){ + const { t } = useTranslation("global"); + return ( + + {t("login.forget_pass")} + + ); +} + export default Login; diff --git a/webapp/src/components/questionView/QuestionView.test.js b/webapp/src/components/questionView/QuestionView.test.js index 11c2439a..7e818b50 100644 --- a/webapp/src/components/questionView/QuestionView.test.js +++ b/webapp/src/components/questionView/QuestionView.test.js @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import {configure} from '@testing-library/dom'; -import Cookies from 'js-cookie' +import Cookies from 'js-cookie'; // Función para configurar el mock de global.Audio const setupAudioMock = () => { diff --git a/webapp/src/custom.css b/webapp/src/custom.css index c9d36a80..5cc05a4d 100644 --- a/webapp/src/custom.css +++ b/webapp/src/custom.css @@ -1720,3 +1720,28 @@ input[type="text"] { } +/*Forget-Password*/ +.code { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} +.code input { + width: 30px; + height: 40px; + text-align: center; + background-color: transparent; + border: none; + border-bottom: solid 2px rgb(20, 181, 230); + font-size: 20px; + color: white; + outline: none; +} + +.codeGrid { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} \ No newline at end of file diff --git a/webapp/src/translations/en/global.json b/webapp/src/translations/en/global.json index 0eb924be..e3a25d89 100644 --- a/webapp/src/translations/en/global.json +++ b/webapp/src/translations/en/global.json @@ -58,6 +58,7 @@ "remember_me": "Remember me", "forgot_password": "Forgot password?", "login_button": "Login", + "forget_pass":"Have you forgotten your password? Let's restore it.", "register_link": "Don't you have an account? Register here." }, "addUser": { @@ -129,6 +130,16 @@ "error":{ "error":"Error", "sorry":"We're sorry, this page does not exist. Don't be angry, I'm just a little cat." + }, + "forgotPassword":{ + "sending_title":"Check your email", + "sending_paragraph":"In order to restore your password we are sending an email with a code of 6 digits", + "enter_code":"Enter code of 6 digits that was sent via email", + "enter_email":"Enter email and username", + "enter_password":"Enter new password", + "enter_email_button": "Send email", + "send_code": "Send code", + "enter_password_button":"Send password" }, "footer":{ "about": "About us", diff --git a/webapp/src/translations/es/global.json b/webapp/src/translations/es/global.json index b51b19ae..ade02ce4 100644 --- a/webapp/src/translations/es/global.json +++ b/webapp/src/translations/es/global.json @@ -60,6 +60,7 @@ "remember_me": "Recordarme", "forgot_password": "¿Olvidaste tu contraseña?", "login_button": "Iniciar sesión", + "forget_pass":"¿Has olvidado tu contraseña? Restaurémosla.", "register_link": "¿No tienes una cuenta? Regístrate aquí." }, @@ -130,6 +131,16 @@ "num_games":"Juegos Competitivos", "search":"Buscar", "enter_username":"Inserta usuario..." + } , + "forgotPassword":{ + "sending_title":"Revisa tu email", + "sending_paragraph":"Para poder reestablecer tu email hemos enviado al email con el que te registraste un código de 6 cifras", + "enter_code":"Introduce el código de 6 cifras enviado a tu email", + "enter_email":"Introduce Email y password", + "enter_password":"Introduce la nueva contraseña", + "enter_email_button": "Enviar Email", + "send_code": "Enviar Código", + "enter_password_button":"Enviar contraseña" }, "footer":{ "about": "Sobre nosotros", @@ -144,7 +155,7 @@ "name2": "Lucía Ruiz Núñez", "name3": "Daniel Sinne Argüelles", "name4": "Jorge Cano Martínez", - "name5": "Ahmet Erdem Yabacı", + "name5": "Ahmet Erdem Yabaci", "name6": "Laura Gómez Menéndez" } diff --git a/webapp/src/translations/tk/global.json b/webapp/src/translations/tk/global.json index 3ebebcb9..1cc1db56 100644 --- a/webapp/src/translations/tk/global.json +++ b/webapp/src/translations/tk/global.json @@ -1,134 +1,145 @@ - { - "home":{ - "welcome":"WIQ'a hoş geldiniz!", - "how_to_play":"Nasıl Oynanır?", - "login":"Giriş Yap", - "register":"Hesabınız yok mu? Buradan kaydolun.", - "language":"Dil İngilizce olarak değiştirildi", - "msg1":"Bilgi yarışmasına hoş geldiniz! Zihninizin sınanmasına ve becerilerinizi sergilemenize hazır olun. Bu heyecan verici oyunda, çeşitli konulardaki bilginizi test edecek bir dizi soruyla karşılaşacaksınız.", - "msg2":"Öyleyse ileri gidin, soruların ustası olduğunuzu kanıtlayın! Meydan okumayı kabul etmeye hazır mısınız?", - "clickOpen":"Devam etmek için tıklayın", - "clickClose":"Kapatmak için tıklayın", - "game":"Misafir olarak oyna" + "home": { + "welcome": "WIQ'a hoş geldiniz!", + "how_to_play": "Nasıl Oynanır?", + "login": "Giriş Yap", + "register": "Hesabınız yok mu? Buradan kaydolun.", + "language": "Dil İngilizce olarak değiştirildi", + "msg1": "Bilgi meydan okumasına hoş geldiniz! Zihninizin sınırlarını test etmeye ve becerilerinizi göstermeye hazırlanın. Bu heyecan verici oyun, çeşitli konulardaki bilginizi test eden bir dizi soruyla karşılaşmanızı sağlayacak.", + "msg2": "Öyleyse devam edin, soruların ustası olduğunuzu kanıtlayın! Meydana mı çıkmaya hazır mısınız?", + "clickOpen": "Devam etmek için tıklayın", + "clickClose": "Kapatmak için tıklayın", + "game":"Misafir olarak oyna" }, - "navBar":{ - "title":"Bil ve Kazan!", - "en":"İngilizce", - "es":"İspanyolca", - "tk":"Türkçe", - "language":"Dil", - "logout":"Çıkış Yap" + "navBar": { + "title": "Bil ve Kazan!", + "en": "İngilizce", + "es": "İspanyolca", + "tk": "Türkçe", + "language": "Dil", + "logout":"Çıkış Yap" }, - "instructions":{ - "title":"WIQ Talimatları", - "objective":"Hedef:", - "objective_p1":"Oyunun amacı, mümkün olduğunca çok soruyu doğru cevaplamaktır.", - "how_to_play":"Nasıl Oynanır:", - "how_to_play_p1":"Oyun bir dizi sorudan oluşur.", - "how_to_play_p2":"Her soruyu dikkatlice okuyun.", - "how_to_play_p3":"Sağlanan seçeneklerden doğru cevabı seçin.", - "how_to_play_p4":"Seçtiğiniz cevabı göndermek için üzerine tıklayın veya dokunun.", - "how_to_play_p5":"Uygulamaya kayıtlıysanız, 'Rekabetçi' modunun sıralamasında ve ilk 10 oyuncusunda pozisyonunuzu görebileceksiniz.", - "guest":"Misafir olarak oyna:", - "guest_p1":"'Misafir' modunda kayıt olmadan oynayabilirsiniz.", - "guest_p2":"Sonucunuzun herhangi bir sıralamada görünmeyeceğini ve bir geçmişinizin olmayacağını unutmayın.", - "guest_p3":"Bu özelliklerden yararlanmak istiyorsanız lütfen kaydolun.", - "personalization":"Oyunu Özelleştir:", - "personalization_p1":"Karşılaşacağınız soru türünü ve cevaplayacağınız soru sayısını seçebilirsiniz.", - "personalization_p2":"10 saniyeniz olacak ve farklı türlerden 5 sorunun olduğu rekabetçi modu oynayabilirsiniz.", - "scoring":"Puanlama:", - "scoring_p1":"Her doğru cevap size 100 puan kazandırır.", - "scoring_p2":"Yanlış cevaplar 50 puan düşürür.", - "time_limit":"Zaman Sınırı:", - "time_limit_p1":"Bazı oyun modlarında her soruya cevap vermek için bir zaman sınırı olabilir. Puanınızı maksimuma çıkarmak için hızlı ve doğru olun.", - "have_fun":"Keyfini Çıkarın!:", - "have_fun_p1":"Oyunun tadını çıkarın ve bilginizi test edin. İyi şanslar!", - "voice":"Ses ve Klavye Erişilebilirliği:", - "voice_p1":"Soruyu ve cevabı dinleme seçeneğiniz vardır. Bir düğmeye basarak soruyu ve ardından her birinin önünde bir numara olan cevapları duyabilirsiniz.", - "voice_p2":"Bir düğmeye basarak soruyu ve ardından her birinin önünde bir numara olan cevapları duyabilirsiniz.", - "voice_p3":"Bu numara, seçtiğiniz cevaba karşılık gelen klavyede ilgili numarayı basarak yanıt vermek için kullanılır.", - "voice_p4":"Ek olarak, 's' harfine basarak sesi etkinleştirebilirsiniz." + "instructions": { + "title": "WIQ Talimatları", + "objective": "Amaç:", + "objective_p1": "Oyunun amacı, mümkün olduğunca çok soruyu doğru cevaplamaktır.", + "how_to_play": "Nasıl Oynanır:", + "how_to_play_p1": "Oyun, bir dizi sorudan oluşur.", + "how_to_play_p2": "Her soruyu dikkatlice okuyun.", + "how_to_play_p3": "Sağlanan seçeneklerden doğru cevabı seçin.", + "how_to_play_p4": "Seçtiğiniz cevabı göndermek için tıklayın veya dokunun.", + "how_to_play_p5": "Uygulamaya kayıtlıysanız, 'Rekabetçi' moddaki sıranızı ve en iyi 10 oyuncuyu görebilirsiniz.", + "guest": "Misafir olarak oyna:", + "guest_p1": "Misafir olarak kaydolmadan oynayabilirsiniz.", + "guest_p2": "Sonuçlarınızın hiçbir sıralamada görünmeyeceğini ve oyun geçmişinizin olmayacağını unutmayın.", + "guest_p3": "Bu özelliklerin tadını çıkarmak istiyorsanız kaydolun.", + "personalization": "Oyunu Kişiselleştirin:", + "personalization_p1": "Karşılaşacağınız soru türünü ve cevaplayacağınız soru sayısını seçebilirsiniz.", + "personalization_p2": "Rekabetçi modda, cevaplamak için 10 saniyeniz olacak ve farklı türlerden 5 soru alacaksınız.", + "scoring": "Puanlama:", + "scoring_p1": "Her doğru cevap size 100 puan kazandırır.", + "scoring_p2": "Yanlış cevaplar 50 puanı düşürür.", + "time_limit": "Zaman Sınırı:", + "time_limit_p1": "Bazı oyun modlarında her soruya cevap vermek için bir zaman sınırı olabilir. Puanınızı maksimize etmek için hızlı ve doğru olun.", + "have_fun": "Keyfini Çıkarın!", + "have_fun_p1": "Oyunun tadını çıkarın ve bilginizi test edin. İyi şanslar!", + "voice": "Ses ve Klavye Erişimi:", + "voice_p1": "Soruyu ve cevabı dinleme seçeneğiniz vardır. Bir düğmeye basarak soruyu ve ardından cevapları bir numara ile dinleyebilirsiniz.", + "voice_p2": "Bir düğmeye basarak soruyu ve ardından cevapları bir numara ile dinleyebilirsiniz.", + "voice_p3": "Bu numara, istediğiniz cevaba klavyeden yanıt vermek için ilgili numarayı basmanızı sağlar.", + "voice_p4": "Ayrıca, 's' harfine basarak sesi etkinleştirebilirsiniz." }, - "login":{ - "title":"Giriş", - "username_placeholder":"Kullanıcı Adı veya E-posta", - "password_placeholder":"Şifre", - "remember_me":"Beni hatırla", - "forgot_password":"Şifrenizi mi unuttunuz?", - "login_button":"Giriş Yap", - "register_link":"Hesabınız yok mu? Buradan kaydolun." + "login": { + "title": "Giriş Yap", + "username_placeholder": "Kullanıcı Adı veya E-posta", + "password_placeholder": "Şifre", + "remember_me": "Beni Hatırla", + "forgot_password": "Şifrenizi mi unuttunuz?", + "login_button": "Giriş Yap", + "forget_pass":"Şifrenizi mi unuttunuz? Sıfırlayalım.", + "register_link": "Hesabınız yok mu? Buradan kaydolun." }, - "addUser":{ - "title":"Kayıt Ol", - "username_placeholder":"Kullanıcı Adı", - "password_placeholder":"Şifre", - "repeat_password_placeholder":"Şifreyi Tekrar Girin", - "register_button":"Kaydol", - "login_link":"Hesabınız var mı? Buradan giriş yapın.", - "email_placeholder":"E-posta", - "very_weak_password":"Çok zayıf şifre", - "weak_password":"Zayıf şifre", - "good_password":"İyi şifre", - "strong_password":"Güçlü şifre", - "error_passwords_no_match":"Şifreler uyuşmuyor", - "error_password_spaces":"Şifre boşluk içeremez", - "error_username_spaces":"Kullanıcı adı boşluk içeremez", - "error_password_minimum_length":"Şifre en az 8 karakter uzunluğunda olmalıdır", - "error_password_maximum_length":"Şifre en fazla 64 karakter uzunluğunda olabilir", - "error_username_in_use":"Kullanıcı adı zaten kullanımda", - "error_wrong_email_format":"Yanlış e-posta formatı (user@example.com)" + "addUser": { + "title": "Kayıt Ol", + "username_placeholder": "Kullanıcı Adı", + "password_placeholder": "Şifre", + "repeat_password_placeholder": "Şifreyi Tekrar Girin", + "register_button": "Kaydol", + "login_link": "Zaten bir hesabınız mı var? Buradan giriş yapın.", + "email_placeholder" : "E-posta", + "very_weak_password": "Çok Zayıf Şifre", + "weak_password": "Zayıf Şifre", + "good_password": "İyi Şifre", + "strong_password": "Güçlü Şifre", + "error_passwords_no_match": "Şifreler eşleşmiyor", + "error_password_spaces": "Şifre boşluk içeremez", + "error_username_spaces": "Kullanıcı adı boşluk içeremez", + "error_password_minimum_length": "Şifre en az 8 karakter olmalıdır", + "error_password_maximum_length": "Şifre en fazla 64 karakter olmalıdır", + "error_username_in_use": "Kullanıcı adı zaten kullanılıyor", + "error_wrong_email_format": "Yanlış e-posta biçimi (user@example.com)" }, - "gameMenu":{ - "history_button":"Geçmiş Verileri Görüntüle", - "new_game_button":"Yeni Oyun Oluştur", - "view_ranking":"Sıralama", - "title":"Oyun Menüsü", - "back":"Geri" + "gameMenu": { + "history_button": "Geçmişi Görüntüle", + "new_game_button": "Yeni Oyun Oluştur", + "view_ranking": "Sıralama", + "title": "Oyun Menüsü", + "back": "Geri" }, - "questionView":{ - "seconds":"saniye", - "question_counter":"Soru numarası ", - "end_countdown":"Süre doldu!", - "finished_game":"Oyun bitti!", - "point":"Puan", - "no_questions_message":"Lütfen biraz bekleyin" + "questionView": { + "seconds": "saniye", + "question_counter": "Soru numarası ", + "end_countdown": "Zaman doldu!", + "finished_game": "Oyun bitti!", + "point": "Puan", + "no_questions_message": "Lütfen biraz bekleyin" }, - "historicalView":{ - "game":"Oyun", - "points":"puan", - "no_games_played":"Henüz oyun oynanmamış" + "historicalView": { + "game": "Oyun", + "points": "puan", + "no_games_played": "Henüz oyun oynamadınız" }, - "gameConfigurator":{ - "game_config":"Oyun yapılandırması", - "type_quest":"Soru Türü: ", - "num_quest":"Soru Sayısı: ", - "play_custom":"Özelleştirilmiş Oyunu Oyna", - "rules_competi":"Tüm türlerdeki sorularla ve 5 adetle oyna", - "play_competi":"Rekabetçi Oyunu Oyna", - "option_all":"Hepsi", - "option_population":"Nüfus", - "option_capital":"Başkent", - "option_language":"Dil", - "option_size":"Boyut", - "option_head_of_goverment":"Hükümet Başkanı", - "custo_game":"Özel oyun oluştur", - "competi_game":"Rekabetçi Oyna", - "randomize":"Parametreleri Rastgeleleştir" + "gameConfigurator": { + "game_config": "Oyun yapılandırması", + "type_quest": "Soru Türü: ", + "num_quest": "Soru Sayısı: ", + "play_custom": "Özel Oyun Oyna", + "rules_competi": "Tüm türlerde ve 5 adet soru ile oynayın", + "play_competi": "Rekabetçi Oyun Oyna", + "option_all": "Tümü", + "option_population": "Nüfus", + "option_capital": "Başkent", + "option_language": "Dil", + "option_size": "Boyut", + "option_head_of_goverment":"Hükümet Başkanı", + "custo_game": "Özel oyun oluştur", + "competi_game": "Rekabetçi oyun oyna", + "randomize": "Parametreleri Rastgele Yap" }, - "ranking":{ - "ranking":"Sıralama", - "position":"Pozisyon", - "username":"Kullanıcı Adı", - "points":"Puanlar", - "num_games":"Rekabetçi oyunlar", - "search":"Ara", - "enter_username":"Kullanıcı Adını Girin..." + "ranking": { + "ranking": "Sıralama", + "position": "Pozisyon", + "username": "Kullanıcı Adı", + "points": "Puanlar", + "num_games": "Rekabetçi oyunlar", + "search": "Ara", + "enter_username": "Kullanıcı adını girin..." }, - "error":{ - "error":"Hata", - "sorry":"Üzgünüz, bu sayfa mevcut değil. Kızma, ben sadece küçük bir kediğim." + "error": { + "error": "Hata", + "sorry": "Üzgünüz, bu sayfa mevcut değil. Kızma, sadece küçük bir kedicik olduğumu unutma." }, + "forgotPassword": { + "sending_title": "E-postanızı kontrol edin", + "sending_paragraph": "Şifrenizi yeniden ayarlamak için kaydolduğunuz e-postaya 6 haneli bir kod gönderdik", + "enter_code": "E-postanıza gönderilen 6 haneli kodu girin", + "enter_email": "E-posta ve kullanıcı adını girin", + "enter_password":"Yeni şifreyi girin", + "enter_email_button": "E-posta Gönder", + "send_code": "Kodu Gönder", + "enter_password_button":"Şifre Gönder" + +}, "footer":{ "about": "Hakkımızda", "API": "API Belgeleri", @@ -144,4 +155,5 @@ "name5": "Ahmet Erdem Yabacı", "name6": "Laura Gómez Menéndez" } - } \ No newline at end of file +} + \ No newline at end of file diff --git a/webapp/src/utils/manageError.js b/webapp/src/utils/manageError.js deleted file mode 100644 index ea56f89c..00000000 --- a/webapp/src/utils/manageError.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * 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 diff --git a/webapp/src/utils/utils.js b/webapp/src/utils/utils.js new file mode 100644 index 00000000..3135a649 --- /dev/null +++ b/webapp/src/utils/utils.js @@ -0,0 +1,56 @@ +/** + * 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"}; +} + +function validateEmail(errors, email){ + var isValid = 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,}))$/); + + if(!isValid) + errors.push("addUser.error_wrong_email_format") +} + +function validateUsername(errors, username){ + if(/\s/.test(username)){ + //Spaces in username + errors.push("addUser.error_username_spaces"); + } +} + +function validatePasswords(errors, password, repeatPassword){ + if(password !== repeatPassword){ + //User put the same password + errors.push("addUser.error_passwords_no_match"); + } + if(/\s/.test(password)){ + //User put spaces in password + errors.push("addUser.error_password_spaces"); + } + if(password.length < 8){ + //Password too short + errors.push("addUser.error_password_minimum_length"); + } + + if(password.length > 64){ + //Password too long + errors.push("addUser.error_password_maximum_length"); + } +} + +module.exports = { + manageError, + validateEmail, + validateUsername, + validatePasswords +}; \ No newline at end of file