From 19733855e419c8de910d53d1bc79729d2e73437c Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Sun, 14 Apr 2024 17:27:58 +0200 Subject: [PATCH 01/22] Added cookie managment in the whole application --- .github/workflows/release.yml | 2 ++ gatewayservice/gateway-service.js | 20 ++++++++++++++++++- questionservice/question-service.js | 2 +- users/authservice/auth-service.js | 2 +- users/recordservice/record-service.js | 4 ++-- webapp/package-lock.json | 9 +++++++++ webapp/package.json | 1 + webapp/src/App.js | 3 --- .../HistoricalData/HistoricalView.js | 10 +++++----- .../HistoricalData/HistoricalView.test.js | 12 +++++------ .../HistoricalData/HistoryRecordRetriever.js | 4 ++-- webapp/src/components/fragments/NavBar.js | 8 ++++---- .../src/components/fragments/NavBar.test.js | 8 +++----- .../loginAndRegistration/AddUser.js | 1 + .../components/loginAndRegistration/Login.js | 7 ++++--- .../loginAndRegistration/Login.test.js | 3 --- .../loginAndRegistration/UserContext.js | 19 ------------------ .../questionView/CreationHistoricalRecord.js | 5 +++-- .../questionView/QuestionGenerator.js | 4 ++-- .../components/questionView/QuestionView.js | 10 ++++++---- .../questionView/QuestionView.test.js | 14 ++++++------- 21 files changed, 77 insertions(+), 71 deletions(-) delete mode 100644 webapp/src/components/loginAndRegistration/UserContext.js 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 009db707..fba6f97b 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -62,7 +62,7 @@ app.get('/questions/:lang', async (req, res) => { try { const lang = req.params.lang; // Forward the question request to the quetion service - const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang); + const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang.toString()); res.json(questionResponse.data); } catch (error) { @@ -108,4 +108,22 @@ 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(401).json({ message: 'Invalid token' }); + } else { + // Token is valid + req.decodedToken = decoded; + // Call next() to proceed to the next middleware or route handler + next(); + } + }); +} + module.exports = server diff --git a/questionservice/question-service.js b/questionservice/question-service.js index 026feb5c..76dc4234 100644 --- a/questionservice/question-service.js +++ b/questionservice/question-service.js @@ -35,7 +35,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-service.js b/users/authservice/auth-service.js index 98779572..0bdbf096 100644 --- a/users/authservice/auth-service.js +++ b/users/authservice/auth-service.js @@ -43,7 +43,7 @@ app.post('/login', async (req, res) => { // 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}); } else { diff --git a/users/recordservice/record-service.js b/users/recordservice/record-service.js index dd383984..dc123b4b 100644 --- a/users/recordservice/record-service.js +++ b/users/recordservice/record-service.js @@ -21,7 +21,7 @@ app.post('/record', async (req, res) => { console.log(user) console.log(game) if(user && game){ - let record = await Record.findOne({ user : user }); + let record = await Record.findOne({ user : user.toString() }); if(record){ //If it exits record.games.push(game); } @@ -47,7 +47,7 @@ app.post('/record', async (req, res) => { app.get('/record/:user', async (req, res) => { try { - const recordFound = await Record.findOne({ user: req.params.user }, 'games'); + const recordFound = await Record.findOne({ user: req.params.user.toString() }, 'games'); if (!recordFound) { res.json({record: "undefined" }); } else { diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 076761a2..6e5b491a 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", @@ -18497,6 +18498,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 2ea16917..cb732d60 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 5eefcf70..887abf5a 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -10,12 +10,10 @@ 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'; function App() { return ( -
@@ -31,7 +29,6 @@ function App() {
-
); } diff --git a/webapp/src/components/HistoricalData/HistoricalView.js b/webapp/src/components/HistoricalData/HistoricalView.js index f110430c..05d3515e 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 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 d586a867..4732f308 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, {token:token}); const receivedRecords = await response.data; console.log(receivedRecords) console.log(receivedRecords[0]) diff --git a/webapp/src/components/fragments/NavBar.js b/webapp/src/components/fragments/NavBar.js index cddffd1b..b2bee539 100644 --- a/webapp/src/components/fragments/NavBar.js +++ b/webapp/src/components/fragments/NavBar.js @@ -4,14 +4,13 @@ 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 Cookies from 'js-cookie'; function Navbar() { const [t, i18n] = useTranslation("global"); const [anchorEl, setAnchorEl] = useState(null); - const { user } = useUserContext(); const handleLanguageMenuOpen = (event) => { setAnchorEl(event.currentTarget); @@ -44,8 +43,9 @@ function Navbar() { changeLanguage("tk")}> {t("navBar.tk")} - {user != null ? ( -

{user.username}

+ + {Cookies.get('user') ? ( +

{ JSON.parse(Cookies.get('user')).username}

) : 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 2324ecab..9ec557cd 100644 --- a/webapp/src/components/loginAndRegistration/AddUser.js +++ b/webapp/src/components/loginAndRegistration/AddUser.js @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import axios from 'axios'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import Cookies from 'js-cookie'; const AddUser = () => { diff --git a/webapp/src/components/loginAndRegistration/Login.js b/webapp/src/components/loginAndRegistration/Login.js index d7381d5f..0ad60066 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,13 +13,14 @@ 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}) + let oneHourAfter = new Date().getTime() + (1 * 60 * 60 * 1000) + Cookies.set('user', JSON.stringify({username : response.data.username, token : response.data.token}) + , {expires:oneHourAfter}); //Used to redirect to the menu to start playing navigate('/menu'); } catch (error) { diff --git a/webapp/src/components/loginAndRegistration/Login.test.js b/webapp/src/components/loginAndRegistration/Login.test.js index edd34e82..2406e721 100644 --- a/webapp/src/components/loginAndRegistration/Login.test.js +++ b/webapp/src/components/loginAndRegistration/Login.test.js @@ -2,7 +2,6 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import Login from './Login'; import { BrowserRouter as Router } from 'react-router-dom'; -import { UserContextProvider } from './UserContext'; // Mocking useTranslation hook jest.mock('react-i18next', () => ({ @@ -12,11 +11,9 @@ jest.mock('react-i18next', () => ({ describe('', () => { test('renders the Login component', () => { render( - - ); expect(screen.getByText('login.title')).toBeInTheDocument(); 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 9da8f624..5b7d12c2 100644 --- a/webapp/src/components/questionView/CreationHistoricalRecord.js +++ b/webapp/src/components/questionView/CreationHistoricalRecord.js @@ -34,7 +34,7 @@ class CreationHistoricalRecord{ } - async sendRecord(user) { + async sendRecord(user, token) { const apiUrl = (process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000') + "/record"; const body = { @@ -44,7 +44,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 28038332..a48dbb82 100644 --- a/webapp/src/components/questionView/QuestionGenerator.js +++ b/webapp/src/components/questionView/QuestionGenerator.js @@ -8,9 +8,9 @@ class QuestionGenerator{ } - async generateQuestions(lang) { + async generateQuestions(lang, token) { try { - const response = await axios.get(this.apiUrl + '/' + lang); + const response = await axios.get(this.apiUrl + '/' + lang, {token:token}); 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 96d0978c..16160a7a 100644 --- a/webapp/src/components/questionView/QuestionView.js +++ b/webapp/src/components/questionView/QuestionView.js @@ -8,21 +8,23 @@ 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' const creationHistoricalRecord = new CreationHistoricalRecord(); const questionGenerator = new QuestionGenerator(); var points = 0; + function QuestionView(){ 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 generateQuestions = async (numQuestion) => { if (numQuestion < 0) { try { - var generatedQuestions = await questionGenerator.generateQuestions(i18n.language); + + var generatedQuestions = await questionGenerator.generateQuestions(i18n.language, cookie.token); setQuestions(generatedQuestions); setnumQuestion(0); } catch (error) { @@ -92,7 +94,7 @@ function QuestionView(){ if(!(numQuestion < questions.length - 1)){ 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 25392c62..cf26e93d 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' configure({ testIdAttribute: 'data-value', @@ -26,6 +25,7 @@ i18en.use(initReactI18next).init({ } }); global.i18en = i18en; +Cookies.set('user', JSON.stringify({username:"dummy", token:"fasfda"})) describe('Question View component', () => { @@ -34,7 +34,7 @@ describe('Question View component', () => { }); 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 @@ -48,7 +48,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()); @@ -65,7 +65,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()); @@ -83,7 +83,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()); @@ -101,7 +101,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()); From a65f6e5d6f4d5426a8bc920b93a7376bfe2fa2cd Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Sun, 14 Apr 2024 17:40:25 +0200 Subject: [PATCH 02/22] Forgot to add the token verifier on some endpoints --- gatewayservice/gateway-service.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index fba6f97b..046158f7 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -47,7 +47,7 @@ app.post('/adduser', async (req, res) => { } }); -app.get('/questions', async (req, res) => { +app.get('/questions', verifyToken, async (req, res) => { try { // Forward the question request to the quetion service @@ -58,7 +58,7 @@ app.get('/questions', async (req, res) => { } }); -app.get('/questions/:lang', async (req, res) => { +app.get('/questions/:lang', verifyToken, async (req, res) => { try { const lang = req.params.lang; // Forward the question request to the quetion service @@ -71,7 +71,7 @@ app.get('/questions/:lang', async (req, res) => { } }); -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); @@ -81,7 +81,7 @@ app.post('/record', async(req, res) => { } }); -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 From e9ad4778324c0395f8b5c57a193a091bca7ee8d1 Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Sun, 14 Apr 2024 17:50:33 +0200 Subject: [PATCH 03/22] Added token to the test requests of the gateway service --- gatewayservice/gateway-service.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gatewayservice/gateway-service.test.js b/gatewayservice/gateway-service.test.js index bfed664f..98baf495 100644 --- a/gatewayservice/gateway-service.test.js +++ b/gatewayservice/gateway-service.test.js @@ -57,7 +57,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'); expect(response.statusCode).toBe(200); expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?"); @@ -66,7 +66,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'); expect(response.statusCode).toBe(200); expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?"); @@ -75,7 +75,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'); @@ -84,7 +84,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'); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty('record', "undefined"); From cbb7d358bb9434c335b23ba9519b633240119a72 Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Sun, 14 Apr 2024 18:05:38 +0200 Subject: [PATCH 04/22] Mocked jwt in gateway tests --- gatewayservice/gateway-service.js | 4 +- gatewayservice/gateway-service.test.js | 15 ++- gatewayservice/package-lock.json | 124 +++++++++++++++++++++++++ gatewayservice/package.json | 1 + 4 files changed, 142 insertions(+), 2 deletions(-) diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index 046158f7..0a2a7dc3 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; @@ -72,6 +72,7 @@ app.get('/questions/:lang', verifyToken, async (req, res) => { }); app.post('/record', verifyToken, async(req, res) => { + console.log("in") try { // Forward the record request to the record service const recordResponse = await axios.post(recordServiceUrl+'/record', req.body); @@ -114,6 +115,7 @@ function verifyToken(req, res, next) { // Verify if the token is valid jwt.verify(token, (process.env.JWT_KEY??'my-key'), (err, decoded) => { + console.log(decoded) if (err) { // Token is not valid res.status(401).json({ message: 'Invalid token' }); diff --git a/gatewayservice/gateway-service.test.js b/gatewayservice/gateway-service.test.js index 98baf495..3e42091f 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', () => { + // Mock responses from external services axios.post.mockImplementation((url, data) => { if (url.endsWith('/login')) { @@ -33,6 +40,12 @@ 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) @@ -85,7 +98,7 @@ describe('Gateway Service', () => { it('should forward record request to record service', async () => { const response = await request(app) .get('/record/testuser').set('token', 'valorDelToken'); - + console.log(response) expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty('record', "undefined"); }); 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" From 32b7a628dc7363516c095625e31d5fc01b8f8818 Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Sun, 14 Apr 2024 18:19:16 +0200 Subject: [PATCH 05/22] Added test to check no token requests --- gatewayservice/gateway-service.js | 3 ++- gatewayservice/gateway-service.test.js | 19 +++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index 0a2a7dc3..57f9ac0b 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -118,7 +118,8 @@ function verifyToken(req, res, next) { console.log(decoded) if (err) { // Token is not valid - res.status(401).json({ message: 'Invalid token' }); + res.status(403).json({authorized: false, + error: 'Invalid token or outdated'}); } else { // Token is valid req.decodedToken = decoded; diff --git a/gatewayservice/gateway-service.test.js b/gatewayservice/gateway-service.test.js index 3e42091f..ca42bc32 100644 --- a/gatewayservice/gateway-service.test.js +++ b/gatewayservice/gateway-service.test.js @@ -14,7 +14,7 @@ jest.mock('axios'); -describe('Gateway Service', () => { +describe('Gateway Service with token mock', () => { // Mock responses from external services axios.post.mockImplementation((url, data) => { @@ -40,6 +40,8 @@ describe('Gateway Service', () => { } }); + + // Mock the `verify` function of JWT jwt.verify.mockImplementation((token, secretOrPublicKey, callback) => { // Assume the token is valid and return the payload @@ -102,4 +104,17 @@ describe('Gateway Service', () => { expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty('record', "undefined"); }); -}); \ No newline at end of file + + +}); + +describe('Gateway Service without token mock', () => { + // Test /record/:user endpoint + it('should not verify the token', async () => { + const response = await request(app) + .get('/record/testuser'); + console.log(response) + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty('record', "undefined"); + }); +}) \ No newline at end of file From dd1c1fe8f498afad981e9c1b3d0b6c8ff6a6578e Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Sun, 14 Apr 2024 18:25:02 +0200 Subject: [PATCH 06/22] Removed unused import --- webapp/src/components/loginAndRegistration/AddUser.js | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp/src/components/loginAndRegistration/AddUser.js b/webapp/src/components/loginAndRegistration/AddUser.js index 9ec557cd..2324ecab 100644 --- a/webapp/src/components/loginAndRegistration/AddUser.js +++ b/webapp/src/components/loginAndRegistration/AddUser.js @@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next"; import axios from 'axios'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import Cookies from 'js-cookie'; const AddUser = () => { From 24931ad9fdc1f29bf986d4d1c49c6ee11345bf3a Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Sun, 14 Apr 2024 19:41:33 +0200 Subject: [PATCH 07/22] Added logout functionality --- gatewayservice/gateway-service.js | 1 - .../HistoricalData/HistoryRecordRetriever.js | 2 +- webapp/src/components/fragments/NavBar.js | 35 ++++++++++++++++--- .../questionView/CreationHistoricalRecord.js | 6 +--- .../questionView/QuestionGenerator.js | 3 +- webapp/src/translations/en/global.json | 3 +- webapp/src/translations/es/global.json | 3 +- webapp/src/translations/tk/global.json | 3 +- 8 files changed, 40 insertions(+), 16 deletions(-) diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index 57f9ac0b..745e428b 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -115,7 +115,6 @@ function verifyToken(req, res, next) { // Verify if the token is valid jwt.verify(token, (process.env.JWT_KEY??'my-key'), (err, decoded) => { - console.log(decoded) if (err) { // Token is not valid res.status(403).json({authorized: false, diff --git a/webapp/src/components/HistoricalData/HistoryRecordRetriever.js b/webapp/src/components/HistoricalData/HistoryRecordRetriever.js index 4732f308..dd566cef 100644 --- a/webapp/src/components/HistoricalData/HistoryRecordRetriever.js +++ b/webapp/src/components/HistoricalData/HistoryRecordRetriever.js @@ -9,7 +9,7 @@ class HistoryRecordRetriever{ async getRecords(user, token) { try { - const response = await axios.get(this.apiUrl + '/' + user, {token:token}); + const response = await axios.get(this.apiUrl + '/' + user, { headers : {'token':token}}); const receivedRecords = await response.data; console.log(receivedRecords) console.log(receivedRecords[0]) diff --git a/webapp/src/components/fragments/NavBar.js b/webapp/src/components/fragments/NavBar.js index b2bee539..2609183d 100644 --- a/webapp/src/components/fragments/NavBar.js +++ b/webapp/src/components/fragments/NavBar.js @@ -4,13 +4,15 @@ import MenuItem from '@mui/material/MenuItem'; import Menu from '@mui/material/Menu'; import "../../custom.css"; import { useTranslation } from "react-i18next"; +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 [anchorEl, setAnchorEl] = useState(); + const [anchorUser, setAnchorUser] = useState(); const handleLanguageMenuOpen = (event) => { setAnchorEl(event.currentTarget); @@ -20,6 +22,19 @@ function Navbar() { setAnchorEl(null); }; + const handleUserMenuOpen = (event) => { + setAnchorUser(event.currentTarget); + }; + + const handleUserMenuClose = () => { + setAnchorUser(null); + }; + + const removeCookie = () => { + Cookies.remove('user'); + navigate('/home') + }; + const changeLanguage = (language) => { i18n.changeLanguage(language); handleLanguageMenuClose(); @@ -43,10 +58,20 @@ function Navbar() { changeLanguage("tk")}> {t("navBar.tk")} - + {Cookies.get('user') ? ( -

{ JSON.parse(Cookies.get('user')).username}

- ) : null} + <> + + + removeCookie()}> {t("navBar.logout")} + + + ) : null} + ); diff --git a/webapp/src/components/questionView/CreationHistoricalRecord.js b/webapp/src/components/questionView/CreationHistoricalRecord.js index 5b7d12c2..a07aac31 100644 --- a/webapp/src/components/questionView/CreationHistoricalRecord.js +++ b/webapp/src/components/questionView/CreationHistoricalRecord.js @@ -49,11 +49,7 @@ class CreationHistoricalRecord{ } }); - if (!response.ok) { - throw new Error('Error al enviar el registro'); - } - - const data = await response.json(); + const data = await response.data; console.log(data); } catch (error) { console.error('Error:', error); diff --git a/webapp/src/components/questionView/QuestionGenerator.js b/webapp/src/components/questionView/QuestionGenerator.js index a48dbb82..bd7d14d2 100644 --- a/webapp/src/components/questionView/QuestionGenerator.js +++ b/webapp/src/components/questionView/QuestionGenerator.js @@ -10,7 +10,8 @@ class QuestionGenerator{ async generateQuestions(lang, token) { try { - const response = await axios.get(this.apiUrl + '/' + lang, {token:token}); + const response = await axios.get(this.apiUrl + '/' + lang, {headers : {'token':token}}); + console.log(response) const receivedQuestions = await response.data; let i = 0; var questions = []; diff --git a/webapp/src/translations/en/global.json b/webapp/src/translations/en/global.json index 8f79adda..f04a6b86 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", diff --git a/webapp/src/translations/es/global.json b/webapp/src/translations/es/global.json index 410eebfa..9a6d1428 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": { diff --git a/webapp/src/translations/tk/global.json b/webapp/src/translations/tk/global.json index 5a17d359..064da5dd 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ı", From 24f10df42def0066d3ac1350f71698b341c45592 Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Sun, 14 Apr 2024 21:18:10 +0200 Subject: [PATCH 08/22] Added restrictions to routes in the webapp --- webapp/src/App.js | 16 ++++++++++------ webapp/src/components/GameMenu/GameMenu.js | 1 + .../src/components/loginAndRegistration/Login.js | 2 +- .../src/components/questionView/QuestionView.js | 2 ++ 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/webapp/src/App.js b/webapp/src/App.js index 887abf5a..683d31d7 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -1,4 +1,3 @@ -import React from 'react'; import QuestionView from './components/questionView/QuestionView'; import GameMenu from './components/GameMenu/GameMenu'; import Navbar from './components/fragments/NavBar'; @@ -10,8 +9,13 @@ 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 React from 'react'; +import Cookies from 'js-cookie'; function App() { + + const isLoggedIn = !!Cookies.get('user'); + return (
@@ -20,12 +24,12 @@ function App() { } /> } /> - } /> + : } /> } /> - } /> - } /> - } /> - } /> + : } /> + : } /> + : } /> + : } />
diff --git a/webapp/src/components/GameMenu/GameMenu.js b/webapp/src/components/GameMenu/GameMenu.js index 7f7bd957..1b79a7e3 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/loginAndRegistration/Login.js b/webapp/src/components/loginAndRegistration/Login.js index 0ad60066..695f7d70 100644 --- a/webapp/src/components/loginAndRegistration/Login.js +++ b/webapp/src/components/loginAndRegistration/Login.js @@ -21,8 +21,8 @@ const Login = () => { let oneHourAfter = new Date().getTime() + (1 * 60 * 60 * 1000) Cookies.set('user', JSON.stringify({username : response.data.username, token : response.data.token}) , {expires:oneHourAfter}); - //Used to redirect to the menu to start playing navigate('/menu'); + window.location.reload(); } catch (error) { console.error('Error adding user:', error); } diff --git a/webapp/src/components/questionView/QuestionView.js b/webapp/src/components/questionView/QuestionView.js index 16160a7a..a895c48a 100644 --- a/webapp/src/components/questionView/QuestionView.js +++ b/webapp/src/components/questionView/QuestionView.js @@ -20,6 +20,8 @@ function QuestionView(){ const[t, i18n] = useTranslation("global"); const cookie = JSON.parse(Cookies.get('user')) + + const generateQuestions = async (numQuestion) => { if (numQuestion < 0) { try { From 490c2ba458aaaf0dbb0c78b7035da60f3d942a14 Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Sun, 14 Apr 2024 21:35:06 +0200 Subject: [PATCH 09/22] Added a reload when logging out too --- webapp/src/components/fragments/NavBar.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/webapp/src/components/fragments/NavBar.js b/webapp/src/components/fragments/NavBar.js index 2609183d..1bebea15 100644 --- a/webapp/src/components/fragments/NavBar.js +++ b/webapp/src/components/fragments/NavBar.js @@ -11,15 +11,15 @@ import Cookies from 'js-cookie'; function Navbar() { const navigate = useNavigate(); const [t, i18n] = useTranslation("global"); - const [anchorEl, setAnchorEl] = useState(); - const [anchorUser, setAnchorUser] = useState(); + 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) => { @@ -32,7 +32,8 @@ function Navbar() { const removeCookie = () => { Cookies.remove('user'); - navigate('/home') + navigate('/home'); + window.location.reload(); }; const changeLanguage = (language) => { @@ -49,9 +50,10 @@ function Navbar() {
changeLanguage("en")}> {t("navBar.en")} changeLanguage("es")}> {t("navBar.es")} @@ -63,9 +65,10 @@ function Navbar() { <> removeCookie()}> {t("navBar.logout")} From 62d7d76c3e6a9fa1ad1f14db67e15e3854f6ec90 Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Sun, 14 Apr 2024 23:39:50 +0200 Subject: [PATCH 10/22] Added new test case for code cover --- .../loginAndRegistration/Login.test.js | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/loginAndRegistration/Login.test.js b/webapp/src/components/loginAndRegistration/Login.test.js index 2406e721..d58571a1 100644 --- a/webapp/src/components/loginAndRegistration/Login.test.js +++ b/webapp/src/components/loginAndRegistration/Login.test.js @@ -1,15 +1,19 @@ 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 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( @@ -22,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'); + }); + + }); }); From 46ca5377f49c7858b41e418678c7e450a3fc69fb Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Mon, 15 Apr 2024 00:25:49 +0200 Subject: [PATCH 11/22] Added email to users at the backend --- users/authservice/auth-model.js | 1 + users/authservice/auth-service.js | 14 +++++++++----- users/authservice/auth-service.test.js | 6 ++++-- users/userservice/user-model.js | 4 ++++ users/userservice/user-service.js | 13 ++++++++++--- users/userservice/user-service.test.js | 17 +++++++++++++++-- 6 files changed, 43 insertions(+), 12 deletions(-) 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 0bdbf096..60c425b4 100644 --- a/users/authservice/auth-service.js +++ b/users/authservice/auth-service.js @@ -28,24 +28,28 @@ app.post('/login', async (req, res) => { try { // Check if required fields are present in the request body try{ - validateRequiredFields(req, ['username', 'password']); + validateRequiredFields(req, ['email', 'username', 'password']); } catch(error){ res.status(400).json({ error : error.message }); return } - const { username, password } = req.body; + const { email, username, password } = req.body; - // Find the user by username in the database - const user = await User.findOne({ username }); + let user; + if(username) //Can log in with both + // Find the user by username in the database + user = await User.findOne({ username }) + else + 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 }, (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: username, email: 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..28067d5a 100644 --- a/users/authservice/auth-service.test.js +++ b/users/authservice/auth-service.test.js @@ -8,6 +8,7 @@ let app; //test user const user = { + email: 'nice@g.com', username: 'testuser', password: 'testpassword', }; @@ -15,6 +16,7 @@ const user = { async function addUser(user){ const hashedPassword = await bcrypt.hash(user.password, 10); const newUser = new User({ + email: user.email, username: user.username, password: hashedPassword, createdAt: new Date() @@ -47,11 +49,11 @@ describe('Auth Service', () => { it('Should show missing field user /login', async () => { const response = await request(app).post('/login').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 show invalid credentials /login', async () => { - const user2 = {username:"Hello", password:"world"} + const user2 = {email:"nice@g.com" ,username:"Hello", password:"world"} const response = await request(app).post('/login').send(user2); expect(response.status).toBe(400); expect(response.body).toHaveProperty('error', 'Invalid credentials'); 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..6278ce18 100644 --- a/users/userservice/user-service.js +++ b/users/userservice/user-service.js @@ -30,7 +30,7 @@ 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']); } catch(error){ res.status(400).json({ error : error.message }); @@ -38,15 +38,22 @@ app.post('/adduser', async (req, res) => { } //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}); - if(user) + //Check there is not a user with the same name + const userEmail = await User.findOne({email: req.body.email}); + + 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, }); diff --git a/users/userservice/user-service.test.js b/users/userservice/user-service.test.js index 7b9e1b4f..a691d432 100644 --- a/users/userservice/user-service.test.js +++ b/users/userservice/user-service.test.js @@ -1,5 +1,6 @@ const request = require('supertest'); const { MongoMemoryServer } = require('mongodb-memory-server'); +const { email } = require('asciidoctor-emoji/dist/node/twemoji-map'); let mongoServer; let app; @@ -21,6 +22,7 @@ afterAll(async () => { describe('User Service', () => { it('should add a new user on POST /adduser', async () => { const newUser = { + email: 'Nice@g.com', username: 'testuser', password: 'testpassword' }; @@ -33,11 +35,12 @@ describe('User Service', () => { it('Should show missing field user /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 = { + email: 'Nice2@g.com', username: 'testuser', password: 'testpassword' }; @@ -47,6 +50,16 @@ describe('User Service', () => { expect(response.body).toHaveProperty('error', 'Username already in use'); }); - + it('Should not register user /adduser', async () => { + const newUser = { + email: 'Nice@g.com', + username: 'testuser2', + password: 'testpassword' + }; + + const response = await request(app).post('/adduser').send(newUser); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('error', 'Email already in use'); + }); }); From 257db61f995179c1f4432eb73ca52c34eed2e12b Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Mon, 15 Apr 2024 00:29:48 +0200 Subject: [PATCH 12/22] Removed weird import prob vs code stuff --- users/userservice/user-service.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/users/userservice/user-service.test.js b/users/userservice/user-service.test.js index a691d432..396b6f1d 100644 --- a/users/userservice/user-service.test.js +++ b/users/userservice/user-service.test.js @@ -1,6 +1,5 @@ const request = require('supertest'); const { MongoMemoryServer } = require('mongodb-memory-server'); -const { email } = require('asciidoctor-emoji/dist/node/twemoji-map'); let mongoServer; let app; From 57b44f33bea83059b2f8c53c928ad17332e2bf9e Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Mon, 15 Apr 2024 00:36:54 +0200 Subject: [PATCH 13/22] Added changes to fix code duplication issues and security flaws --- gatewayservice/gateway-service.test.js | 2 -- users/authservice/auth-service.js | 4 +++- users/userservice/user-service.js | 4 ++-- users/userservice/user-service.test.js | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gatewayservice/gateway-service.test.js b/gatewayservice/gateway-service.test.js index ca42bc32..d3904a1d 100644 --- a/gatewayservice/gateway-service.test.js +++ b/gatewayservice/gateway-service.test.js @@ -100,7 +100,6 @@ describe('Gateway Service with token mock', () => { it('should forward record request to record service', async () => { const response = await request(app) .get('/record/testuser').set('token', 'valorDelToken'); - console.log(response) expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty('record', "undefined"); }); @@ -113,7 +112,6 @@ describe('Gateway Service without token mock', () => { it('should not verify the token', async () => { const response = await request(app) .get('/record/testuser'); - console.log(response) expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty('record', "undefined"); }); diff --git a/users/authservice/auth-service.js b/users/authservice/auth-service.js index 60c425b4..85ca92ec 100644 --- a/users/authservice/auth-service.js +++ b/users/authservice/auth-service.js @@ -35,7 +35,9 @@ app.post('/login', async (req, res) => { return } - const { email, username, password } = req.body; + const email = req.body.email.toString(); + const username = req.body.username.toString(); + const password = req.body.password.toString(); let user; if(username) //Can log in with both diff --git a/users/userservice/user-service.js b/users/userservice/user-service.js index 6278ce18..34cfb7e0 100644 --- a/users/userservice/user-service.js +++ b/users/userservice/user-service.js @@ -38,10 +38,10 @@ app.post('/adduser', async (req, res) => { } //Check there is not a user with the same name - const userUsername = 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}); + const userEmail = await User.findOne({email: req.body.email.toString()}); if(userUsername) return res.status(400).json({error : "Username already in use"}) diff --git a/users/userservice/user-service.test.js b/users/userservice/user-service.test.js index 396b6f1d..227755bb 100644 --- a/users/userservice/user-service.test.js +++ b/users/userservice/user-service.test.js @@ -41,7 +41,7 @@ describe('User Service', () => { const newUser = { email: 'Nice2@g.com', username: 'testuser', - password: 'testpassword' + password: 'test' }; const response = await request(app).post('/adduser').send(newUser); @@ -53,7 +53,7 @@ describe('User Service', () => { const newUser = { email: 'Nice@g.com', username: 'testuser2', - password: 'testpassword' + password: 'password' }; const response = await request(app).post('/adduser').send(newUser); From 22dba52ae62b50bc68324a4fb171ee727ea1e0c0 Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Mon, 15 Apr 2024 00:45:34 +0200 Subject: [PATCH 14/22] Try to reduce duplicate code --- users/userservice/user-service.test.js | 32 ++++++++++++-------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/users/userservice/user-service.test.js b/users/userservice/user-service.test.js index 227755bb..d5352626 100644 --- a/users/userservice/user-service.test.js +++ b/users/userservice/user-service.test.js @@ -4,7 +4,11 @@ const { MongoMemoryServer } = require('mongodb-memory-server'); let mongoServer; let app; - +let newUser = { + email: 'Nice@g.com', + username: 'testuser', + password: 'testpassword' +}; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); @@ -18,14 +22,16 @@ afterAll(async () => { await mongoServer.stop(); }); +afterEach(async () => { + newUser = { + email: 'Nice@g.com', + username: 'testuser', + password: 'testpassword' + }; +}) + describe('User Service', () => { it('should add a new user on POST /adduser', async () => { - const newUser = { - email: 'Nice@g.com', - username: 'testuser', - password: 'testpassword' - }; - const response = await request(app).post('/adduser').send(newUser); expect(response.status).toBe(200); expect(response.body).toHaveProperty('username', 'testuser'); @@ -38,11 +44,7 @@ describe('User Service', () => { }); it('Should not register user /adduser', async () => { - const newUser = { - email: 'Nice2@g.com', - username: 'testuser', - password: 'test' - }; + newUser.email = 'Nice2@g.com'; const response = await request(app).post('/adduser').send(newUser); expect(response.status).toBe(400); @@ -50,11 +52,7 @@ describe('User Service', () => { }); it('Should not register user /adduser', async () => { - const newUser = { - email: 'Nice@g.com', - username: 'testuser2', - password: 'password' - }; + newUser.username = 'testuser2'; const response = await request(app).post('/adduser').send(newUser); expect(response.status).toBe(400); From bb17d3998c5e52523d6655613403dca4f1953861 Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Mon, 15 Apr 2024 20:35:44 +0200 Subject: [PATCH 15/22] Added email field on login and register and be able to login with username or email --- users/authservice/auth-service.js | 13 +++++-------- users/authservice/auth-service.test.js | 18 ++++++++++++------ .../components/loginAndRegistration/AddUser.js | 14 +++++++++++++- .../components/loginAndRegistration/Login.js | 7 ------- webapp/src/translations/en/global.json | 5 +++-- webapp/src/translations/es/global.json | 5 +++-- 6 files changed, 36 insertions(+), 26 deletions(-) diff --git a/users/authservice/auth-service.js b/users/authservice/auth-service.js index 85ca92ec..924dc512 100644 --- a/users/authservice/auth-service.js +++ b/users/authservice/auth-service.js @@ -28,22 +28,19 @@ app.post('/login', async (req, res) => { try { // Check if required fields are present in the request body try{ - validateRequiredFields(req, ['email', 'username', 'password']); + validateRequiredFields(req, ['username', 'password']); } catch(error){ res.status(400).json({ error : error.message }); return } - const email = req.body.email.toString(); + const email = req.body.username.toString(); const username = req.body.username.toString(); const password = req.body.password.toString(); - let user; - if(username) //Can log in with both - // Find the user by username in the database - user = await User.findOne({ username }) - else + 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 @@ -51,7 +48,7 @@ app.post('/login', async (req, res) => { // Generate a JWT token 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, email: email}); + 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 28067d5a..7f258f5c 100644 --- a/users/authservice/auth-service.test.js +++ b/users/authservice/auth-service.test.js @@ -7,8 +7,7 @@ let mongoServer; let app; //test user -const user = { - email: 'nice@g.com', +let user = { username: 'testuser', password: 'testpassword', }; @@ -16,7 +15,7 @@ const user = { async function addUser(user){ const hashedPassword = await bcrypt.hash(user.password, 10); const newUser = new User({ - email: user.email, + email: "user@gmail.com", username: user.username, password: hashedPassword, createdAt: new Date() @@ -46,14 +45,21 @@ 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: email'); + expect(response.body).toHaveProperty('error', 'Missing required field: username'); }); it('Should show invalid credentials /login', async () => { - const user2 = {email:"nice@g.com" ,username:"Hello", password:"world"} + const user2 = {username:"Hello", password:"world"} const response = await request(app).post('/login').send(user2); expect(response.status).toBe(400); expect(response.body).toHaveProperty('error', 'Invalid credentials'); diff --git a/webapp/src/components/loginAndRegistration/AddUser.js b/webapp/src/components/loginAndRegistration/AddUser.js index 2324ecab..f5ee2557 100644 --- a/webapp/src/components/loginAndRegistration/AddUser.js +++ b/webapp/src/components/loginAndRegistration/AddUser.js @@ -11,6 +11,7 @@ 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(''); @@ -21,7 +22,7 @@ const AddUser = () => { try { //TODO: Add more validations if(password === repeatPassword){ //User put the same password - const response = await axios.post(apiUrl, { username, password }); + const response = await axios.post(apiUrl, { email, username, password }); console.log("Registered user: " + response.data.username); navigate('/login'); } @@ -40,6 +41,17 @@ const AddUser = () => {

{t("addUser.title")}

+
+

{t("addUser.email_placeholder")}:

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

{t("addUser.username_placeholder")}:

{ onChange={(e) => setPassword(e.target.value)} />
- {//TODO: Study this option and see if it is viable - } -
- -
diff --git a/webapp/src/translations/en/global.json b/webapp/src/translations/en/global.json index f04a6b86..5b8cd91f 100644 --- a/webapp/src/translations/en/global.json +++ b/webapp/src/translations/en/global.json @@ -38,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,7 +51,8 @@ "password_placeholder": "Password", "repeat_password_placeholder": "Repeat password", "register_button": "Register", - "login_link": "Do you have an account? Login here." + "login_link": "Do you have an account? Login here.", + "email_placeholder" : "Email" }, "gameMenu":{ "history_button":"View Historical Data", diff --git a/webapp/src/translations/es/global.json b/webapp/src/translations/es/global.json index 9a6d1428..57a91fdf 100644 --- a/webapp/src/translations/es/global.json +++ b/webapp/src/translations/es/global.json @@ -41,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,7 +55,8 @@ "password_placeholder": "Contraseña", "repeat_password_placeholder": "Repetir contraseña", "register_button": "Registrarse", - "login_link": "¿Ya tienes una cuenta? Inicia sesión aquí." + "login_link": "¿Ya tienes una cuenta? Inicia sesión aquí.", + "email_placeholder" : "Correo electrónico" }, "gameMenu":{ "history_button":"Ver Historial", From 51ff8391dff9322cd1500ebf99059593dc3928a4 Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Sun, 21 Apr 2024 19:08:51 +0200 Subject: [PATCH 16/22] Fixed all tests that did not use the Cookie and the email --- webapp/src/App.js | 4 +- webapp/src/App.test.js | 2 +- .../loginAndRegistration/AddUser.js | 246 ++++++++---------- .../loginAndRegistration/AddUser.test.js | 31 ++- .../questionView/QuestionGenerator.js | 2 +- .../questionView/QuestionView.test.js | 8 +- .../components/ranking/RankingRetriever.js | 8 +- webapp/src/components/ranking/RankingView.js | 12 +- .../components/ranking/RankingView.test.js | 21 +- webapp/src/translations/en/global.json | 3 +- 10 files changed, 156 insertions(+), 181 deletions(-) diff --git a/webapp/src/App.js b/webapp/src/App.js index 4c32df75..970ac846 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -11,7 +11,6 @@ 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 React from 'react'; import Cookies from 'js-cookie'; import GameConfigurator from './components/GameConfigurator/GameConfigurator'; import RankingView from './components/ranking/RankingView'; @@ -22,7 +21,8 @@ function App() { document.title = 'WIQ'; }, []); - const isLoggedIn = !!Cookies.get('user'); + //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/loginAndRegistration/AddUser.js b/webapp/src/components/loginAndRegistration/AddUser.js index cb27a124..e5552072 100644 --- a/webapp/src/components/loginAndRegistration/AddUser.js +++ b/webapp/src/components/loginAndRegistration/AddUser.js @@ -7,67 +7,75 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import zxcvbn from "zxcvbn"; - 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 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 }); - console.log("Registered user: " + response.data.username); navigate('/login'); + } catch (error) { + if (error.response.data.error === "Username already in use") { + setSubmitErrors(["addUser.error_username_in_use"]); + } + console.error("Error adding user:", error); } - - } catch (error) { - if(error.response.data.error === "Username already in use"){ //TODO: Improve - setSubmitError("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); @@ -97,74 +105,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.email_placeholder")}:

- +
+ +

{t("addUser.title")}

+ {showErrors()} +
+

{t("addUser.email_placeholder")}:

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

{t("addUser.username_placeholder")}:

- +
+

{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)} + /> +
+ + + + +
+
); }; @@ -172,61 +185,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..cd36c68c 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' }); }); }); diff --git a/webapp/src/components/questionView/QuestionGenerator.js b/webapp/src/components/questionView/QuestionGenerator.js index b8ef243c..ec7a1b6d 100644 --- a/webapp/src/components/questionView/QuestionGenerator.js +++ b/webapp/src/components/questionView/QuestionGenerator.js @@ -42,7 +42,7 @@ class QuestionGenerator{ if(type==="COMPETITIVE"){ 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; diff --git a/webapp/src/components/questionView/QuestionView.test.js b/webapp/src/components/questionView/QuestionView.test.js index cc0858c4..d00a36e9 100644 --- a/webapp/src/components/questionView/QuestionView.test.js +++ b/webapp/src/components/questionView/QuestionView.test.js @@ -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('🔊')); @@ -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 3024588d..54f087cf 100644 --- a/webapp/src/translations/en/global.json +++ b/webapp/src/translations/en/global.json @@ -62,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", From de0e44d8bf937890a3fe4e94bff9c026e2b17765 Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Mon, 22 Apr 2024 12:54:42 +0200 Subject: [PATCH 17/22] Now registering is the same as login, added e2e tests --- users/userservice/package.json | 1 + users/userservice/user-service.js | 8 +++-- webapp/e2e/steps/gameMenu.steps.js | 17 +++++++--- webapp/e2e/steps/home.steps.js | 7 +++-- webapp/e2e/utils.js | 31 +++++++++++++++++++ webapp/src/App.js | 2 +- webapp/src/components/fragments/NavBar.js | 2 +- .../loginAndRegistration/AddUser.js | 7 ++++- 8 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 webapp/e2e/utils.js 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-service.js b/users/userservice/user-service.js index 34cfb7e0..9b058aa1 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(); @@ -58,8 +59,11 @@ app.post('/adduser', async (req, res) => { 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/webapp/e2e/steps/gameMenu.steps.js b/webapp/e2e/steps/gameMenu.steps.js index 215af2f7..8cddc046 100644 --- a/webapp/e2e/steps/gameMenu.steps.js +++ b/webapp/e2e/steps/gameMenu.steps.js @@ -4,20 +4,29 @@ 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: false, // Muestra el navegador + slowMo: 40, + defaultViewport: { width: 1920, height: 1080 }, + args: ['--window-size=1920,1080'] }); page = await browser.newPage(); setDefaultOptions({ timeout: 10000 }); + + await register(page, email, username, password); }); test('There should be visible three links', ({ given, then }) => { @@ -29,7 +38,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..27eff77d --- /dev/null +++ b/webapp/e2e/utils.js @@ -0,0 +1,31 @@ +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"]'); +} + +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"]'); +} + +async function logout(page){ + await page.click('.user-button'); + await page.waitForSelector('.MuiMenu-paper', { visible: true }); + await page.click('text=Log Out'); +} + +module.exports = { + register, + login, + logout + }; \ No newline at end of file diff --git a/webapp/src/App.js b/webapp/src/App.js index 970ac846..69c168cf 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -31,7 +31,7 @@ function App() { } /> - } /> + : } /> : } /> } /> : } /> diff --git a/webapp/src/components/fragments/NavBar.js b/webapp/src/components/fragments/NavBar.js index 11cf5642..5ce411cd 100644 --- a/webapp/src/components/fragments/NavBar.js +++ b/webapp/src/components/fragments/NavBar.js @@ -72,7 +72,7 @@ function Navbar() { onClose={handleUserMenuClose} disableAutoFocusItem > - removeCookie()}> {t("navBar.logout")} + removeCookie()}> {t("navBar.logout")}
) : null} diff --git a/webapp/src/components/loginAndRegistration/AddUser.js b/webapp/src/components/loginAndRegistration/AddUser.js index e5552072..103fbe44 100644 --- a/webapp/src/components/loginAndRegistration/AddUser.js +++ b/webapp/src/components/loginAndRegistration/AddUser.js @@ -6,6 +6,7 @@ import axios from 'axios'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import zxcvbn from "zxcvbn"; +import Cookies from 'js-cookie'; const AddUser = () => { const navigate = useNavigate(); @@ -66,7 +67,11 @@ const AddUser = () => { if (newSubmitErrors.length === 0) { try { const response = await axios.post(apiUrl, { email, username, password }); - navigate('/login'); + 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) { if (error.response.data.error === "Username already in use") { setSubmitErrors(["addUser.error_username_in_use"]); From 7d79eb40f78b3332767c6c14d9575eb925b07df0 Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Mon, 22 Apr 2024 12:57:37 +0200 Subject: [PATCH 18/22] Made npm install to sync package.json and package-lock.json --- users/userservice/package-lock.json | 94 +++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) 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", From 975cca09cd5bd4a9d7c0678564b3e6138e4e15bf Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Mon, 22 Apr 2024 13:02:42 +0200 Subject: [PATCH 19/22] Forgot to remove headless mode --- webapp/e2e/steps/gameMenu.steps.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/e2e/steps/gameMenu.steps.js b/webapp/e2e/steps/gameMenu.steps.js index 8cddc046..4d16b917 100644 --- a/webapp/e2e/steps/gameMenu.steps.js +++ b/webapp/e2e/steps/gameMenu.steps.js @@ -17,7 +17,7 @@ defineFeature(feature, test => { beforeAll(async () => { browser = await puppeteer.launch({ - headless: false, // Muestra el navegador + headless: "new", slowMo: 40, defaultViewport: { width: 1920, height: 1080 }, args: ['--window-size=1920,1080'] From 19a68d3dbcff093277ff1ce16f364ed5ac937585 Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Mon, 22 Apr 2024 13:37:33 +0200 Subject: [PATCH 20/22] Now e2e should work --- webapp/e2e/steps/gameMenu.steps.js | 7 ++++++- webapp/e2e/utils.js | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/webapp/e2e/steps/gameMenu.steps.js b/webapp/e2e/steps/gameMenu.steps.js index 4d16b917..9d00a32d 100644 --- a/webapp/e2e/steps/gameMenu.steps.js +++ b/webapp/e2e/steps/gameMenu.steps.js @@ -24,11 +24,16 @@ defineFeature(feature, test => { }); 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'); diff --git a/webapp/e2e/utils.js b/webapp/e2e/utils.js index 27eff77d..b25f6aaa 100644 --- a/webapp/e2e/utils.js +++ b/webapp/e2e/utils.js @@ -7,6 +7,8 @@ async function register(page, email, username, password) { 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) { @@ -16,12 +18,16 @@ async function login(page, username, password) { 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 = { From e27f8427be889aaab9995dca802c41e73b2feef6 Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Mon, 22 Apr 2024 19:24:50 +0200 Subject: [PATCH 21/22] Added validations to solve sonarcloud security ranking --- gatewayservice/gateway-service.js | 148 +++++++++--------- gatewayservice/gateway-service.test.js | 21 +-- users/userservice/user-service.js | 41 ++++- users/userservice/user-service.test.js | 63 +++++++- .../loginAndRegistration/AddUser.js | 2 +- 5 files changed, 173 insertions(+), 102 deletions(-) diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index ce9298ce..298dbc6d 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -58,45 +58,23 @@ app.get('/questions', verifyToken, async (req, res) => { } }); -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); - } catch (error) { - - res.status(error.response.status).json({ error: error.response.data.error }); - } -}); - - -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); - } catch (error) { - res.status(error.response.status).json({ error: error.response.data.error }); - } -}); app.get('/questions/:lang/:amount/:type', verifyToken, 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); + 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 }); @@ -104,27 +82,36 @@ app.get('/questions/:lang/:amount/:type', verifyToken, async (req, res) => { }); -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 }); } }); 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.toString()); - - 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 }); @@ -132,7 +119,7 @@ app.get('/questions/:lang', verifyToken, async (req, res) => { }); app.post('/record', verifyToken, async(req, res) => { - console.log("in") + try { // Forward the record request to the record service const recordResponse = await axios.post(recordServiceUrl+'/record', req.body); @@ -154,31 +141,14 @@ app.get('/record/ranking/top10', verifyToken, 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); - } catch (error) { - res.send(error); - } -}); - -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); - } -}); - -app.get('/record/ranking/:user', 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); } @@ -186,10 +156,14 @@ app.get('/record/ranking/: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); } @@ -230,4 +204,22 @@ function verifyToken(req, res, 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 +} + module.exports = server diff --git a/gatewayservice/gateway-service.test.js b/gatewayservice/gateway-service.test.js index 3bf212df..550c4024 100644 --- a/gatewayservice/gateway-service.test.js +++ b/gatewayservice/gateway-service.test.js @@ -100,7 +100,7 @@ describe('Gateway Service with token mock', () => { // 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); }); @@ -108,7 +108,7 @@ describe('Gateway Service with token mock', () => { // 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); }); @@ -126,18 +126,6 @@ describe('Gateway Service with token mock', () => { it('should forward record request to record service', async () => { const response = await request(app) .get('/record/testuser').set('token', 'valorDelToken'); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('record', "undefined"); - }); - - -}); - -describe('Gateway Service without token mock', () => { - // Test /record/:user endpoint - it('should not verify the token', async () => { - const response = await request(app) - .get('/record/testuser'); checkRecord(response); }); @@ -145,7 +133,7 @@ describe('Gateway Service without token mock', () => { // 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); }); @@ -153,10 +141,11 @@ describe('Gateway Service without token mock', () => { // 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/users/userservice/user-service.js b/users/userservice/user-service.js index 9b058aa1..f57a0aa5 100644 --- a/users/userservice/user-service.js +++ b/users/userservice/user-service.js @@ -17,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) { @@ -25,13 +30,47 @@ 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, ['email', 'username', 'password']); + validateRequiredFields(req, ['email', 'username', 'password', 'repeatPassword']); } 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 d5352626..ab9fa21e 100644 --- a/users/userservice/user-service.test.js +++ b/users/userservice/user-service.test.js @@ -5,9 +5,10 @@ let mongoServer; let app; let newUser = { - email: 'Nice@g.com', + email: 'example@example.com', username: 'testuser', - password: 'testpassword' + password: 'testpassword', + repeatPassword: 'testpassword' }; beforeAll(async () => { @@ -24,9 +25,10 @@ afterAll(async () => { afterEach(async () => { newUser = { - email: 'Nice@g.com', + email: 'example@example.com', username: 'testuser', - password: 'testpassword' + password: 'testpassword', + repeatPassword: 'testpassword' }; }) @@ -37,14 +39,14 @@ describe('User Service', () => { 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: email'); }); it('Should not register user /adduser', async () => { - newUser.email = 'Nice2@g.com'; + newUser.email = 'example2@example.com'; const response = await request(app).post('/adduser').send(newUser); expect(response.status).toBe(400); @@ -60,3 +62,52 @@ describe('User Service', () => { }); }); + +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/src/components/loginAndRegistration/AddUser.js b/webapp/src/components/loginAndRegistration/AddUser.js index 103fbe44..bccffdbc 100644 --- a/webapp/src/components/loginAndRegistration/AddUser.js +++ b/webapp/src/components/loginAndRegistration/AddUser.js @@ -66,7 +66,7 @@ const AddUser = () => { if (newSubmitErrors.length === 0) { try { - const response = await axios.post(apiUrl, { email, username, password }); + 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}); From 1b0592585c09cd4923ae1c489b2034b5bba1d1ef Mon Sep 17 00:00:00 2001 From: Mister-Mario Date: Mon, 22 Apr 2024 19:55:03 +0200 Subject: [PATCH 22/22] Fixed addUser test failing cause the new endpoint --- gatewayservice/gateway-service.js | 28 ++++++++++++------- users/userservice/user-service.js | 1 + .../loginAndRegistration/AddUser.js | 9 +++--- .../loginAndRegistration/AddUser.test.js | 2 +- webapp/src/utils/manageError.js | 17 +++++++++++ 5 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 webapp/src/utils/manageError.js diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index 298dbc6d..c08d0e37 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -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,7 +43,8 @@ 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); + } }); @@ -54,7 +55,7 @@ app.get('/questions', verifyToken, async (req, res) => { 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) } }); @@ -77,7 +78,7 @@ app.get('/questions/:lang/:amount/:type', verifyToken, async (req, res) => { } } catch (error) { - res.status(error.response.status).json({ error: error.response.data.error }); + manageError(error) } }); @@ -96,7 +97,7 @@ app.get('/questions/:lang/:amount', verifyToken, async (req, res) => { res.json(questionResponse.data); } } catch (error) { - res.status(error.response.status).json({ error: error.response.data.error }); + manageError(error) } }); @@ -114,7 +115,7 @@ app.get('/questions/:lang', verifyToken, async (req, res) => { } catch (error) { - res.status(error.response.status).json({ error: error.response.data.error }); + manageError(error) } }); @@ -125,7 +126,7 @@ app.post('/record', verifyToken, async(req, res) => { const recordResponse = await axios.post(recordServiceUrl+'/record', req.body); res.json(recordResponse.data); } catch (error) { - res.send(error); + manageError(error) } }); @@ -135,7 +136,7 @@ app.get('/record/ranking/top10', verifyToken, async(req, res)=>{ const recordResponse = await axios.get(recordServiceUrl + '/record/ranking/top10'); res.json(recordResponse.data); } catch (error) { - res.send(error); + manageError(error) } }); @@ -150,7 +151,7 @@ app.get('/record/ranking/:user', verifyToken, async(req, res)=>{ res.json(recordResponse.data); } } catch (error) { - res.send(error); + manageError(error) } }); @@ -165,7 +166,7 @@ app.get('/record/:user', verifyToken, async(req, res)=>{ res.json(recordResponse.data); } } catch (error) { - res.send(error); + manageError(error) } }); @@ -222,4 +223,11 @@ 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/users/userservice/user-service.js b/users/userservice/user-service.js index f57a0aa5..a6ea8a09 100644 --- a/users/userservice/user-service.js +++ b/users/userservice/user-service.js @@ -74,6 +74,7 @@ app.post('/adduser', async (req, res) => { } catch(error){ res.status(400).json({ error : error.message }); + console.log(res) return } diff --git a/webapp/src/components/loginAndRegistration/AddUser.js b/webapp/src/components/loginAndRegistration/AddUser.js index bccffdbc..66683cc6 100644 --- a/webapp/src/components/loginAndRegistration/AddUser.js +++ b/webapp/src/components/loginAndRegistration/AddUser.js @@ -8,6 +8,8 @@ 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"; @@ -73,10 +75,9 @@ const AddUser = () => { navigate('/menu'); window.location.reload(); } catch (error) { - if (error.response.data.error === "Username already in use") { - setSubmitErrors(["addUser.error_username_in_use"]); - } - console.error("Error adding user:", error); + let processedError = manageError(error); + if(processedError.status !== 500) + setSubmitErrors(['addUser.error_username_in_use']); } } }; diff --git a/webapp/src/components/loginAndRegistration/AddUser.test.js b/webapp/src/components/loginAndRegistration/AddUser.test.js index cd36c68c..f95b52ef 100644 --- a/webapp/src/components/loginAndRegistration/AddUser.test.js +++ b/webapp/src/components/loginAndRegistration/AddUser.test.js @@ -84,7 +84,7 @@ describe('', () => { await waitFor(() => { expect(screen.getByText('addUser.error_username_in_use')).toBeInTheDocument(); }); - expect(axios.post).toHaveBeenCalledWith(expect.any(String), { email: 'user@example.com' ,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/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