diff --git a/.gitignore b/.gitignore index 5f31eec7..836b7f0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,9 @@ coverage docs/build -# Dependencies lock files -package-lock.json - # Node.js dependencies node_modules # IDE/Editor files .idea/ .vscode/ - -# Environment variables -.env diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index 32949f90..c79ea1d8 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -63,6 +63,85 @@ app.get('/questions', async (req, res) => { } }); +app.post('/user/edit', async (req, res) => { + try { + // Forward the add user request to the user service + const userResponse = await axios.post(userServiceUrl + '/user/edit', req.body); + res.json(userResponse.data); + } catch (error) { + if (error.response && error.response.status) { + res.status(error.response.status).json({ error: error.response.data.error }); + } else if (error.message) { + res.status(500).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Internal Server Error' }); + } + } +}); + +app.get('/group/list', async (req, res) => { + try { + const userResponse = await axios.get(userServiceUrl + '/group/api/list'); + res.json(userResponse.data); + } catch (error) { + if (error.response && error.response.status) { + res.status(error.response.status).json({ error: error.response.data.error }); + } else if (error.message) { + res.status(500).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Internal Server Error' }); + } + } +}); + + +app.post('/group/add', async (req, res) => { + try { + const userResponse = await axios.post(userServiceUrl + '/group/add', req.body); + res.json(userResponse.data); + } catch (error) { + if (error.response && error.response.status) { + res.status(error.response.status).json({ error: error.response.data.error }); + } else if (error.message) { + res.status(500).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Internal Server Error' }); + } + } +}); + +app.get('/group/:name', async (req, res) => { + try { + const { name } = req.params; + const userResponse = await axios.get(`${userServiceUrl}/group/${name}`); + res.json(userResponse.data); + } catch (error) { + if (error.response && error.response.status) { + res.status(error.response.status).json({ error: error.response.data.error }); + } else if (error.message) { + res.status(500).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Internal Server Error' }); + } + } +}); + +app.post('/group/:name/join', async (req, res) => { + try { + const { name } = req.params; + const userResponse = await axios.post(`${userServiceUrl}/group/${name}/join`, req.body); + res.json(userResponse.data); + } catch (error) { + if (error.response && error.response.status) { + res.status(error.response.status).json({ error: error.response.data.error }); + } else if (error.message) { + res.status(500).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Internal Server Error' }); + } + } +}); + // Start the gateway service const server = app.listen(port, () => { console.log(`Gateway Service listening at http://localhost:${port}`); diff --git a/users/index.js b/users/index.js index df43b4a1..3a8d2a58 100644 --- a/users/index.js +++ b/users/index.js @@ -1,9 +1,11 @@ // Imports (express syntax) const express = require('express'); + // Routes: const authRoutes = require('./routes/auth-routes.js'); const userRoutes = require('./routes/user-routes.js'); +const groupRoutes = require('./routes/group-routes.js'); // App definition and const app = express(); @@ -15,6 +17,7 @@ app.use(express.json()); // Routes middlewares to be used app.use('/user', userRoutes); app.use('/login', authRoutes); +app.use('/group', groupRoutes); // Start the service const server = app.listen(port, () => { diff --git a/users/models/user-model.js b/users/models/user-model.js index 3fc86750..0ffa40a2 100644 --- a/users/models/user-model.js +++ b/users/models/user-model.js @@ -1,5 +1,6 @@ const { Sequelize, DataTypes } = require('sequelize'); + // Database connection configuration const sequelize = new Sequelize({ host: 'mariadb', @@ -15,6 +16,7 @@ const User = sequelize.define('User', { username: { type: DataTypes.STRING, primaryKey: true, + notEmpty: true, }, password: { type: DataTypes.STRING, @@ -23,10 +25,12 @@ const User = sequelize.define('User', { name: { type: DataTypes.STRING, allowNull: false, + notEmpty: true, }, surname: { type: DataTypes.STRING, allowNull: false, + notEmpty: true, }, createdAt: { type: DataTypes.DATE, @@ -63,10 +67,14 @@ const Group = sequelize.define('Group', { type: DataTypes.STRING, primaryKey: true }, + creator: { + type: DataTypes.STRING, + }, createdAt: { type: DataTypes.DATE, defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') } + // When the session is introduced, the creator user and more stuff will be added }) const UserGroup = sequelize.define('UserGroup', { diff --git a/users/package-lock.json b/users/package-lock.json index 58362157..827f228b 100644 --- a/users/package-lock.json +++ b/users/package-lock.json @@ -11,7 +11,10 @@ "dependencies": { "bcrypt": "^5.1.1", "body-parser": "^1.20.2", + "dotenv": "^16.4.5", "express": "^4.18.2", + "express-session": "^1.18.0", + "jose": "5.2.2", "jsonwebtoken": "^9.0.2", "mariadb": "^2.5.1", "sequelize": "^6.6.5" @@ -2019,6 +2022,17 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dottie": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", @@ -2229,6 +2243,37 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "dependencies": { + "cookie": "0.6.0", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, "node_modules/express/node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -3469,6 +3514,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.2.tgz", + "integrity": "sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4001,6 +4054,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4271,6 +4332,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4988,6 +5057,17 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/users/package.json b/users/package.json index e193cfed..17a06b3c 100644 --- a/users/package.json +++ b/users/package.json @@ -20,13 +20,16 @@ "dependencies": { "bcrypt": "^5.1.1", "body-parser": "^1.20.2", + "dotenv": "^16.4.5", "express": "^4.18.2", - "sequelize": "^6.6.5", + "express-session": "^1.18.0", + "jose": "5.2.2", "jsonwebtoken": "^9.0.2", - "mariadb": "^2.5.1" + "mariadb": "^2.5.1", + "sequelize": "^6.6.5" }, "devDependencies": { "jest": "^29.7.0", "supertest": "^6.3.4" } -} \ No newline at end of file +} diff --git a/users/routes/auth-routes.js b/users/routes/auth-routes.js index b6a3ce07..92fffd53 100644 --- a/users/routes/auth-routes.js +++ b/users/routes/auth-routes.js @@ -4,9 +4,12 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { User } = require('../models/user-model'); + +require('dotenv').config(); + router.post('/', async (req, res) => { try { - + const { username, password } = req.body; // Check if required fields are present in the request body @@ -20,10 +23,28 @@ router.post('/', async (req, res) => { // Check if the user exists and verify the password if (user && user.username === username && await bcrypt.compare(password, user.password)) { - // Generate a JWT token - const token = jwt.sign({ userId: user.id }, 'your-secret-key', { expiresIn: '1h' }); + + // Token payload + const payload = { + userId: username + }; + + //CHANGE THIS TO ENVIRONMENT VARS (NOT PUBLIC) + const secretKey = 'eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiQWRtaW4iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsImV4cCI6MTcwOTQ2OTkzMywiaWF0IjoxNzA5NDY5OTMzfQ.pQ8H6FKeZyEHPnGs4Ah3-n-QXJ5E8YM_u1AfZHI7Ip0'; + + const options = { + expiresIn: '1h' + }; + + //Token sign and creation + const token = jwt.sign(payload, secretKey, options); + + //This should save token in user's browser, it doesn't seem to do anything + res.cookie("token", token); // maxAge (millis) = 1 hour + // Respond with the token and user information - return res.json({ token, username, createdAt: user.createdAt }); + return res.status(200).json({ token, username, createdAt: user.createdAt }); + } else { return res.status(401).json({ error: 'Invalid credentials' }); } diff --git a/users/routes/group-routes.js b/users/routes/group-routes.js new file mode 100644 index 00000000..e025f8a8 --- /dev/null +++ b/users/routes/group-routes.js @@ -0,0 +1,49 @@ +const express = require('express'); +const router = express.Router(); +const bcrypt = require('bcrypt'); +const { Group,User,UserGroup } = require('../models/user-model'); + +//Group internal routes +const apiRoutes = require('../services/group-api'); + +// Adding a group to the database +router.post('/add', async (req, res) => { + try { + const { name,username } = req.body; + + const newGroup = await Group.create({ + name: name, + creator: username, + createdAt: new Date() + }); + + res.json(newGroup); + } catch (error) { + return res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +// Adding a new relationship in the database between a group and a user when this one joins it +router.post('/:name/join', async (req, res) => { + try { + const groupName = req.params.name; + const { username } = req.body; + + // Need to be tested + const newUserGroup = await UserGroup.create({ + name: groupName, + username: username, + createdAt: new Date() + }); + + res.json(newUserGroup); + } catch (error) { + return res.status(500).json({ error: 'Internal Server Error' }); + } +}); + + +//Api middleware +router.use('/api', apiRoutes); + +module.exports = router; \ No newline at end of file diff --git a/users/routes/user-routes.js b/users/routes/user-routes.js index ad798377..3c6787a5 100644 --- a/users/routes/user-routes.js +++ b/users/routes/user-routes.js @@ -52,6 +52,7 @@ router.post('/add', async (req, res) => { // Route for edit a user router.post('/edit', async (req, res) => { try { + const { username, total_score, correctly_answered_questions, incorrectly_answered_questions, total_time_played, games_played } = req.body; // Find the user in the database by their username @@ -67,11 +68,11 @@ router.post('/edit', async (req, res) => { } // Update the user's fields with the provided values - userToUpdate.total_score = total_score; - userToUpdate.correctly_answered_questions = correctly_answered_questions; - userToUpdate.incorrectly_answered_questions = incorrectly_answered_questions; - userToUpdate.total_time_played = total_time_played; - userToUpdate.games_played = games_played; + userToUpdate.total_score = userToUpdate.total_score + total_score; + userToUpdate.correctly_answered_questions = userToUpdate.correctly_answered_questions + correctly_answered_questions; + userToUpdate.incorrectly_answered_questions = userToUpdate.incorrectly_answered_questions + incorrectly_answered_questions; + userToUpdate.total_time_played = userToUpdate.total_time_played + total_time_played; + userToUpdate.games_played = userToUpdate.games_played + games_played; // Save the changes to the database await userToUpdate.save(); diff --git a/users/services/authVerifyMiddleWare b/users/services/authVerifyMiddleWare new file mode 100644 index 00000000..82d8fe5a --- /dev/null +++ b/users/services/authVerifyMiddleWare @@ -0,0 +1,23 @@ +// middleware/authMiddleware.js + +const jwt = require('jsonwebtoken'); + +function verifyToken(token) { + + + if (token === null) { + return res.status(401).json({ message: "Token not provided" }); + } + + try { + const payload = jwt.verify(token, 'eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiQWRtaW4iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsImV4cCI6MTcwOTQ2OTkzMywiaWF0IjoxNzA5NDY5OTMzfQ.pQ8H6FKeZyEHPnGs4Ah3-n-QXJ5E8YM_u1AfZHI7Ip0'); + + //Once the payload is obtained, you can get user info using its keys + req.username = payload.userId; + + } catch (error) { + return res.status(403).json({ message: "Token not valid" }); + } + } + +module.exports = verifyToken; \ No newline at end of file diff --git a/users/services/group-api.js b/users/services/group-api.js new file mode 100644 index 00000000..c02ebc7f --- /dev/null +++ b/users/services/group-api.js @@ -0,0 +1,59 @@ +const express = require('express'); +const router = express.Router(); +const { Group,User,UserGroup } = require('../models/user-model'); + + +// Getting the list of groups in the database +router.get('/list', async (req, res) => { + try { + const allGroups = await Group.findAll(); + const groupsJSON = allGroups.map(group => group.toJSON()); + + const allGroupsJSON = { + groups: groupsJSON + }; + + res.json(allGroupsJSON); + } catch (error) { + return res.status(500).json({ error: 'Internal Server Error' }); + } +}); + + + +// Getting a group by its name +router.get('/:name', async (req, res) => { + try { + const groupName = req.params.name; + + // Need also to get the group members + const group = await Group.findOne({ + where: { + name: groupName + } + }); + if (!group) { + return res.status(404).json({ error: 'Group not found' }); + } + + const groupUsers = await User.findAll({ + include: [ + { + model: UserGroup, + where: { name: groupName } + } + ] + }); + + // Construct JSON response + const groupJSON = group.toJSON(); + groupJSON.users = groupUsers.map(user => user.toJSON()); + + res.json(groupJSON); + } catch (error) { + return res.status(400).json({ error: error.message }); + } +}); + + +module.exports = router; \ No newline at end of file diff --git a/users/services/user-api.js b/users/services/user-api.js index 159fe98b..77b5c96f 100644 --- a/users/services/user-api.js +++ b/users/services/user-api.js @@ -74,20 +74,4 @@ router.get('/ranking', async (req,res) => { }); - - -//Get Groups - -//Get group by name - - - - - - - - - - - module.exports = router; \ No newline at end of file diff --git a/webapp/public/defaultFavicon.ico b/webapp/public/defaultFavicon.ico new file mode 100644 index 00000000..a11777cc Binary files /dev/null and b/webapp/public/defaultFavicon.ico differ diff --git a/webapp/public/favicon.ico b/webapp/public/favicon.ico index a11777cc..7ffc7f65 100644 Binary files a/webapp/public/favicon.ico and b/webapp/public/favicon.ico differ diff --git a/webapp/public/gameImg/foto0.jpg b/webapp/public/gameImg/foto0.jpg deleted file mode 100644 index a5a33487..00000000 Binary files a/webapp/public/gameImg/foto0.jpg and /dev/null differ diff --git a/webapp/public/gameImg/foto3.jpg b/webapp/public/gameImg/foto3.jpg index a5a33487..f9a1b837 100644 Binary files a/webapp/public/gameImg/foto3.jpg and b/webapp/public/gameImg/foto3.jpg differ diff --git a/webapp/public/gameImg/foto4.jpg b/webapp/public/gameImg/foto4.jpg index a5a33487..e213a172 100644 Binary files a/webapp/public/gameImg/foto4.jpg and b/webapp/public/gameImg/foto4.jpg differ diff --git a/webapp/public/gameImg/foto5.jpg b/webapp/public/gameImg/foto5.jpg deleted file mode 100644 index f9a1b837..00000000 Binary files a/webapp/public/gameImg/foto5.jpg and /dev/null differ diff --git a/webapp/public/gameImg/foto6.jpg b/webapp/public/gameImg/foto6.jpg deleted file mode 100644 index a5a33487..00000000 Binary files a/webapp/public/gameImg/foto6.jpg and /dev/null differ diff --git a/webapp/public/gameImg/foto7.jpg b/webapp/public/gameImg/foto7.jpg deleted file mode 100644 index e213a172..00000000 Binary files a/webapp/public/gameImg/foto7.jpg and /dev/null differ diff --git a/webapp/public/hurta2.jpg b/webapp/public/hurta2.jpg new file mode 100644 index 00000000..33a719f6 Binary files /dev/null and b/webapp/public/hurta2.jpg differ diff --git a/webapp/public/hurtado.jpg b/webapp/public/hurtado.jpg new file mode 100644 index 00000000..0534ae71 Binary files /dev/null and b/webapp/public/hurtado.jpg differ diff --git a/webapp/public/possibleFavicon.ico b/webapp/public/possibleFavicon.ico new file mode 100644 index 00000000..9426db0f Binary files /dev/null and b/webapp/public/possibleFavicon.ico differ diff --git a/webapp/public/possibleIcon.jpg b/webapp/public/possibleIcon.jpg new file mode 100644 index 00000000..4fc25895 Binary files /dev/null and b/webapp/public/possibleIcon.jpg differ diff --git a/webapp/public/sounds/success_sound.mp3 b/webapp/public/sounds/success_sound.mp3 new file mode 100644 index 00000000..ff101229 Binary files /dev/null and b/webapp/public/sounds/success_sound.mp3 differ diff --git a/webapp/public/sounds/wrong_sound.mp3 b/webapp/public/sounds/wrong_sound.mp3 new file mode 100644 index 00000000..33dc9d2d Binary files /dev/null and b/webapp/public/sounds/wrong_sound.mp3 differ diff --git a/webapp/src/App.js b/webapp/src/App.js index 064f3bcb..7442c3a1 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -7,6 +7,8 @@ import Footer from './components/Footer'; import Home from './pages/Home'; import Homepage from './pages/Homepage'; import Game from './pages/Game'; +import GroupList from './pages/GroupList'; +import GroupCreate from './pages/GroupCreate'; import {Route, Routes} from 'react-router-dom'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import { Box } from '@mui/material'; @@ -27,6 +29,9 @@ const theme = createTheme({ }); function App() { + React.useEffect(() => { + document.title = "WIQ - Wikidata Infinite Quest"; + }, []); return ( @@ -38,7 +43,8 @@ function App() { }/> }/> }/> - + }/> + }/>