diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 74f4a99e..cef2a143 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -76,6 +76,8 @@ jobs:
- uses: actions/checkout@v4
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@v5
+ env:
+ JWT_KEY: ${{secrets.JWT_KEY}}
with:
name: arquisoft/wiq_en1b/authservice
username: ${{ github.actor }}
diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js
index 317c55ae..c08d0e37 100644
--- a/gatewayservice/gateway-service.js
+++ b/gatewayservice/gateway-service.js
@@ -6,7 +6,7 @@ const promBundle = require('express-prom-bundle');
const swaggerUi = require('swagger-ui-express');
const fs = require("fs")
const YAML = require('yaml')
-
+const jwt = require('jsonwebtoken');
const app = express();
const port = 8000;
@@ -33,7 +33,7 @@ app.post('/login', async (req, res) => {
const authResponse = await axios.post(authServiceUrl+'/login', req.body);
res.json(authResponse.data);
} catch (error) {
- res.status(error.response.status).json({ error: error.response.data.error });
+ manageError(error)
}
});
@@ -43,103 +43,130 @@ app.post('/adduser', async (req, res) => {
const userResponse = await axios.post(userServiceUrl+'/adduser', req.body);
res.json(userResponse.data);
} catch (error) {
- res.status(error.response.status).json({ error: error.response.data.error });
+ manageError(error);
+
}
});
-app.get('/questions', async (req, res) => {
+app.get('/questions', verifyToken, async (req, res) => {
try {
// Forward the question request to the quetion service
const questionResponse = await axios.get(questionServiceUrl+'/questions');
res.json(questionResponse.data);
} catch (error) {
- res.status(error.response.status).json({ error: error.response.data.error });
+ manageError(error)
}
});
-app.get('/questions/:lang/:amount/:type', async (req, res) => {
- try {
- const lang = req.params.lang.toString();
- const amount = req.params.amount.toString();
- const type = req.params.type.toString();
- // Forward the question request to the quetion service
- const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang + '/' + amount + '/' + type);
- res.json(questionResponse.data);
+
+app.get('/questions/:lang/:amount/:type', verifyToken, async (req, res) => {
+ try {
+ if(!validateLang(req.params.lang.toString()) ||
+ !validateAmount(req.params.amount.toString()) ||
+ !validateType(req.params.type.toString()))
+ res.status(400).json({ error: 'Wrong values given' });
+ else {
+ const lang = encodeURIComponent(req.params.lang.toString());
+ const amount = encodeURIComponent(req.params.amount.toString());
+ const type = encodeURIComponent(req.params.type.toString());
+ // Forward the question request to the quetion service
+ const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang + '/' + amount + '/' + type);
+
+ res.json(questionResponse.data);
+ }
} catch (error) {
- res.status(error.response.status).json({ error: error.response.data.error });
+ manageError(error)
}
});
-app.get('/questions/:lang/:amount', async (req, res) => {
+app.get('/questions/:lang/:amount', verifyToken, async (req, res) => {
try {
- const lang = req.params.lang.toString();
- const amount = req.params.amount.toString();
- // Forward the question request to the quetion service
- const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang + '/' + amount);
-
- res.json(questionResponse.data);
+ if(!validateLang(req.params.lang.toString()) ||
+ !validateAmount(req.params.amount.toString()))
+ res.status(400).json({ error: 'Wrong values given' });
+ else{
+ const lang = encodeURIComponent(req.params.lang.toString());
+ const amount = encodeURIComponent(req.params.amount.toString());
+ // Forward the question request to the quetion service
+ const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang + '/' + amount);
+
+ res.json(questionResponse.data);
+ }
} catch (error) {
-
- res.status(error.response.status).json({ error: error.response.data.error });
+ manageError(error)
}
});
-app.get('/questions/:lang', async (req, res) => {
+app.get('/questions/:lang', verifyToken, async (req, res) => {
try {
- const lang = req.params.lang.toString();
- // Forward the question request to the quetion service
- const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang);
-
- res.json(questionResponse.data);
+ if(!validateLang(req.params.lang.toString()))
+ res.status(400).json({ error: 'Wrong values given' });
+ else{
+ const lang = encodeURIComponent(req.params.lang.toString());
+ // Forward the question request to the quetion service
+ const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang.toString());
+
+ res.json(questionResponse.data);
+ }
+
} catch (error) {
- res.status(error.response.status).json({ error: error.response.data.error });
+ manageError(error)
}
});
-app.post('/record', async(req, res) => {
+app.post('/record', verifyToken, async(req, res) => {
+
try {
// Forward the record request to the record service
const recordResponse = await axios.post(recordServiceUrl+'/record', req.body);
res.json(recordResponse.data);
} catch (error) {
- res.send(error);
+ manageError(error)
}
});
-app.get('/record/ranking/top10', async(req, res)=>{
+app.get('/record/ranking/top10', verifyToken, async(req, res)=>{
try {
// Forward the record request to the record service
const recordResponse = await axios.get(recordServiceUrl + '/record/ranking/top10');
res.json(recordResponse.data);
} catch (error) {
- res.send(error);
+ manageError(error)
}
});
-app.get('/record/ranking/:user', async(req, res)=>{
+app.get('/record/ranking/:user', verifyToken, async(req, res)=>{
try {
- const user = req.params.user;
- // Forward the record request to the record service
- const recordResponse = await axios.get(recordServiceUrl + '/record/ranking/' + user);
- res.json(recordResponse.data);
+ if(!validateUser(req.params.user.toString()))
+ res.status(400).json({ error: 'Wrong values given' });
+ else{
+ const user = encodeURIComponent(req.params.user.toString());
+ // Forward the record request to the record service
+ const recordResponse = await axios.get(recordServiceUrl + '/record/ranking/' + user);
+ res.json(recordResponse.data);
+ }
} catch (error) {
- res.send(error);
+ manageError(error)
}
});
-app.get('/record/:user', async(req, res)=>{
+app.get('/record/:user', verifyToken, async(req, res)=>{
try {
- const user = req.params.user;
- // Forward the record request to the record service
- const recordResponse = await axios.get(recordServiceUrl + '/record/' + user);
- res.json(recordResponse.data);
+ if(!validateUser(req.params.user.toString()))
+ res.status(400).json({ error: 'Wrong values given' });
+ else{
+ const user = encodeURIComponent(req.params.user.toString());
+ // Forward the record request to the record service
+ const recordResponse = await axios.get(recordServiceUrl + '/record/' + user);
+ res.json(recordResponse.data);
+ }
} catch (error) {
- res.send(error);
+ manageError(error)
}
});
@@ -159,4 +186,48 @@ const server = app.listen(port, () => {
console.log(`Gateway Service listening at http://localhost:${port}`);
});
+function verifyToken(req, res, next) {
+ // Get the token from the request headers
+ const token = req.headers['token'] || req.body.token || req.query.token;
+
+ // Verify if the token is valid
+ jwt.verify(token, (process.env.JWT_KEY??'my-key'), (err, decoded) => {
+ if (err) {
+ // Token is not valid
+ res.status(403).json({authorized: false,
+ error: 'Invalid token or outdated'});
+ } else {
+ // Token is valid
+ req.decodedToken = decoded;
+ // Call next() to proceed to the next middleware or route handler
+ next();
+ }
+ });
+}
+
+function validateLang(lang){
+ return ['en', 'es', 'tk'].includes(lang);
+}
+
+function validateAmount(amount) {
+ const parsed = parseInt(amount, 10);
+ // We only accept integers and positive ones
+ return !isNaN(parsed) && parsed > 0;
+}
+
+function validateType(type){
+ return ['POPULATION', 'CAPITAL', 'LANGUAGE', 'SIZE'].includes(type);
+}
+
+function validateUser(user){
+ return !(/\s/.test(user)) //True if there are no spaces
+}
+
+function manageError(error){
+ if(error.response) //Some microservice responded with an error
+ res.status(error.response.status).json({ error: error.response.data.error });
+ else //Some other error
+ res.status(500).json({error : "Interanl server error"})
+}
+
module.exports = server
diff --git a/gatewayservice/gateway-service.test.js b/gatewayservice/gateway-service.test.js
index 971fead9..550c4024 100644
--- a/gatewayservice/gateway-service.test.js
+++ b/gatewayservice/gateway-service.test.js
@@ -1,14 +1,21 @@
const request = require('supertest');
const axios = require('axios');
+const jwt = require('jsonwebtoken');
const app = require('./gateway-service');
afterAll(async () => {
app.close();
});
+
+jest.mock('jsonwebtoken');
+
jest.mock('axios');
-describe('Gateway Service', () => {
+
+
+describe('Gateway Service with token mock', () => {
+
// Mock responses from external services
axios.post.mockImplementation((url, data) => {
if (url.endsWith('/login')) {
@@ -45,6 +52,14 @@ describe('Gateway Service', () => {
}
});
+
+
+ // Mock the `verify` function of JWT
+ jwt.verify.mockImplementation((token, secretOrPublicKey, callback) => {
+ // Assume the token is valid and return the payload
+ callback(null, "decoded");
+ });
+
// Test /login endpoint
it('should forward login request to auth service', async () => {
const response = await request(app)
@@ -69,7 +84,7 @@ describe('Gateway Service', () => {
// Test /questions endpoint
it('should forward questions request to question service', async () => {
const response = await request(app)
- .get('/questions');
+ .get('/questions').set('token', 'valorDelToken');
checkQuestion(response);
});
@@ -77,7 +92,7 @@ describe('Gateway Service', () => {
// Test /questions/:lang endpoint
it('should forward questions request to question service', async () => {
const response = await request(app)
- .get('/questions/es');
+ .get('/questions/es').set('token', 'valorDelToken');
checkQuestion(response);
});
@@ -85,7 +100,7 @@ describe('Gateway Service', () => {
// Test /questions/:lang/:amount endpoint
it('should forward questions request to question service', async () => {
const response = await request(app)
- .get('/questions/es/1');
+ .get('/questions/es/1').set('token', 'valorDelToken');
checkQuestion(response);
});
@@ -93,7 +108,7 @@ describe('Gateway Service', () => {
// Test /questions/:lang/:amount/:type endpoint
it('should forward questions request to question service', async () => {
const response = await request(app)
- .get('/questions/es/1/CAPITAL');
+ .get('/questions/es/1/CAPITAL').set('token', 'valorDelToken');
checkQuestion(response);
});
@@ -101,7 +116,7 @@ describe('Gateway Service', () => {
// Test /record endpoint
it('should forward record request to record service', async () => {
const response = await request(app)
- .post('/record');
+ .post('/record').set('token', 'valorDelToken');
expect(response.statusCode).toBe(200);
expect(response.body.user).toBe('testuser');
@@ -110,7 +125,7 @@ describe('Gateway Service', () => {
// Test /record/:user endpoint
it('should forward record request to record service', async () => {
const response = await request(app)
- .get('/record/testuser');
+ .get('/record/testuser').set('token', 'valorDelToken');
checkRecord(response);
});
@@ -118,7 +133,7 @@ describe('Gateway Service', () => {
// Test /record/ranking/:user endpoint
it('should forward record request to record service', async () => {
const response = await request(app)
- .get('/record/ranking/testuser');
+ .get('/record/ranking/testuser').set('token', 'valorDelToken');
checkRecord(response);
});
@@ -126,10 +141,11 @@ describe('Gateway Service', () => {
// Test /record/ranking/top10 endpoint
it('should forward record request to record service', async () => {
const response = await request(app)
- .get('/record/ranking/top10');
+ .get('/record/ranking/top10').set('token', 'valorDelToken');
checkRecord(response);
});
+
});
function checkRecord(response){
diff --git a/gatewayservice/package-lock.json b/gatewayservice/package-lock.json
index ca71293c..48729129 100644
--- a/gatewayservice/package-lock.json
+++ b/gatewayservice/package-lock.json
@@ -14,6 +14,7 @@
"express": "^4.18.2",
"express-openapi": "^12.1.3",
"express-prom-bundle": "^7.0.0",
+ "jsonwebtoken": "^9.0.2",
"swagger-ui-express": "^5.0.0",
"yaml": "^2.4.1"
},
@@ -1600,6 +1601,11 @@
"node-int64": "^0.4.0"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -1980,6 +1986,14 @@
"esprima": "^4.0.0"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -3438,6 +3452,81 @@
"node": ">=6"
}
},
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/jsonwebtoken/node_modules/semver": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -3474,11 +3563,46 @@
"node": ">=8"
}
},
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
diff --git a/gatewayservice/package.json b/gatewayservice/package.json
index 93a9914e..0757ca8f 100644
--- a/gatewayservice/package.json
+++ b/gatewayservice/package.json
@@ -22,6 +22,7 @@
"cors": "^2.8.5",
"express": "^4.18.2",
"express-openapi": "^12.1.3",
+ "jsonwebtoken": "^9.0.2",
"express-prom-bundle": "^7.0.0",
"swagger-ui-express": "^5.0.0",
"yaml": "^2.4.1"
diff --git a/questionservice/question-service.js b/questionservice/question-service.js
index effc5ac7..77377c74 100644
--- a/questionservice/question-service.js
+++ b/questionservice/question-service.js
@@ -89,7 +89,7 @@ app.get('/questions/:lang', async (req, res) => {
const lang = req.params.lang;
const questions = await Question.aggregate([
- {$match: {language : lang}}, //Condition
+ {$match: {language : lang.toString()}}, //Condition
{$sample: {size:5}} //5 random from the ones that fullfil the condition
]);
diff --git a/users/authservice/auth-model.js b/users/authservice/auth-model.js
index 7763b51e..5cdbaf69 100644
--- a/users/authservice/auth-model.js
+++ b/users/authservice/auth-model.js
@@ -1,6 +1,7 @@
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
+ email: String,
username: String,
password: String,
createdAt: Date,
diff --git a/users/authservice/auth-service.js b/users/authservice/auth-service.js
index 98779572..924dc512 100644
--- a/users/authservice/auth-service.js
+++ b/users/authservice/auth-service.js
@@ -35,17 +35,20 @@ app.post('/login', async (req, res) => {
return
}
- const { username, password } = req.body;
+ const email = req.body.username.toString();
+ const username = req.body.username.toString();
+ const password = req.body.password.toString();
- // Find the user by username in the database
- const user = await User.findOne({ username });
+ let user = await User.findOne({ username })
+ if(!user) //There is no user by that username we may have received an email
+ user = await User.findOne({ email })
// Check if the user exists and verify the password
if (user && await bcrypt.compare(password, user.password)) {
// Generate a JWT token
- const token = jwt.sign({ userId: user._id }, 'your-secret-key', { expiresIn: '1h' });
+ const token = jwt.sign({ userId: user._id }, (process.env.JWT_KEY??'my-key'), { expiresIn: '1h' });
// Respond with the token and user information
- res.json({ token: token, username: username});
+ res.json({ token: token, username: user.username, email: user.email});
} else {
res.status(400).json({ error: 'Invalid credentials' });
}
diff --git a/users/authservice/auth-service.test.js b/users/authservice/auth-service.test.js
index f4a5a82e..7f258f5c 100644
--- a/users/authservice/auth-service.test.js
+++ b/users/authservice/auth-service.test.js
@@ -7,7 +7,7 @@ let mongoServer;
let app;
//test user
-const user = {
+let user = {
username: 'testuser',
password: 'testpassword',
};
@@ -15,6 +15,7 @@ const user = {
async function addUser(user){
const hashedPassword = await bcrypt.hash(user.password, 10);
const newUser = new User({
+ email: "user@gmail.com",
username: user.username,
password: hashedPassword,
createdAt: new Date()
@@ -44,7 +45,14 @@ describe('Auth Service', () => {
expect(response.body).toHaveProperty('username', 'testuser');
});
- it('Should show missing field user /login', async () => {
+ it('Should perform a login operation with email /login', async () => {
+ user.username = "user@gmail.com";
+ const response = await request(app).post('/login').send(user);
+ expect(response.status).toBe(200);
+ expect(response.body).toHaveProperty('username', 'testuser');
+ });
+
+ it('Should show missing field username /login', async () => {
const response = await request(app).post('/login').send();
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('error', 'Missing required field: username');
diff --git a/users/recordservice/record-service.js b/users/recordservice/record-service.js
index cf94544a..3bca1fa8 100644
--- a/users/recordservice/record-service.js
+++ b/users/recordservice/record-service.js
@@ -22,7 +22,7 @@ app.post('/record', async (req, res) => {
const user = req.body.user;
const game = req.body.game;
if(user && game){
- let record = await Record.findOne({ user : user });
+ let record = await Record.findOne({ user : user.toString() });
if(record){ //If it exits
record.games.push(game);
}
diff --git a/users/userservice/package-lock.json b/users/userservice/package-lock.json
index f21b26cb..e2ccf4a2 100644
--- a/users/userservice/package-lock.json
+++ b/users/userservice/package-lock.json
@@ -12,6 +12,7 @@
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"express": "^4.18.2",
+ "jsonwebtoken": "^9.0.2",
"mongoose": "^8.0.4"
},
"devDependencies": {
@@ -1656,6 +1657,11 @@
"node": "*"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -2061,6 +2067,14 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -3602,6 +3616,51 @@
"node": ">=6"
}
},
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/kareem": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz",
@@ -3646,6 +3705,41 @@
"node": ">=8"
}
},
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
+ },
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
diff --git a/users/userservice/package.json b/users/userservice/package.json
index 71cc1cdb..a2efb649 100644
--- a/users/userservice/package.json
+++ b/users/userservice/package.json
@@ -21,6 +21,7 @@
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"express": "^4.18.2",
+ "jsonwebtoken": "^9.0.2",
"mongoose": "^8.0.4"
},
"devDependencies": {
diff --git a/users/userservice/user-model.js b/users/userservice/user-model.js
index 71d81b5f..e6643ff2 100644
--- a/users/userservice/user-model.js
+++ b/users/userservice/user-model.js
@@ -1,6 +1,10 @@
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
+ email: {
+ type: String,
+ required: true,
+ },
username: {
type: String,
required: true,
diff --git a/users/userservice/user-service.js b/users/userservice/user-service.js
index 69899c29..a6ea8a09 100644
--- a/users/userservice/user-service.js
+++ b/users/userservice/user-service.js
@@ -3,6 +3,7 @@ const express = require('express');
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const bodyParser = require('body-parser');
+const jwt = require('jsonwebtoken');
const User = require('./user-model')
const app = express();
@@ -16,6 +17,11 @@ const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/userdb';
mongoose.connect(mongoUri);
+const validateEmail = (email) => {
+ return String(email)
+ .toLowerCase()
+ .match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/);
+};
// Function to validate required fields in the request body
function validateRequiredFields(req, requiredFields) {
@@ -24,35 +30,80 @@ function validateRequiredFields(req, requiredFields) {
throw new Error(`Missing required field: ${field}`);
}
}
+
+ let email = req.body.email.toString();
+ let username = req.body.username.toString();
+ let password = req.body.password.toString();
+ let repeatPassword = req.body.repeatPassword.toString();
+
+ if(!validateEmail(email)){
+ //User put a wrong format email
+ throw new Error("Wrong email format (example@example.com)")
+ }
+
+ if(password !== repeatPassword){
+ //User put the same password
+ throw new Error("Passwords dont match");
+ }
+ if(/\s/.test(password)){
+ //User put spaces in password
+ throw new Error("Password cannot have spaces");
+ }
+ if(password.length < 8){
+ //Password too short
+ throw new Error("Password must be at least 8 characters long");
+ }
+
+ if(password.length > 64){
+ //Password too long
+ throw new Error("Password must less than 64 characters long");
+ }
+
+ if(/\s/.test(username)){
+ //Spaces in username
+ throw new Error("Username cannot have spaces");
+ }
+
}
app.post('/adduser', async (req, res) => {
try {
// Check if required fields are present in the request body
try{
- validateRequiredFields(req, ['username', 'password']);
+ validateRequiredFields(req, ['email', 'username', 'password', 'repeatPassword']);
}
catch(error){
res.status(400).json({ error : error.message });
+ console.log(res)
return
}
//Check there is not a user with the same name
- const user = await User.findOne({username: req.body.username});
+ const userUsername = await User.findOne({username: req.body.username.toString()});
+
+ //Check there is not a user with the same name
+ const userEmail = await User.findOne({email: req.body.email.toString()});
- if(user)
+ if(userUsername)
return res.status(400).json({error : "Username already in use"})
+ if(userEmail)
+ return res.status(400).json({error : "Email already in use"})
+
// Encrypt the password before saving it
const hashedPassword = await bcrypt.hash(req.body.password, 10);
const newUser = new User({
+ email: req.body.email,
username: req.body.username,
password: hashedPassword,
});
- await newUser.save();
- res.json({username: newUser.username});
+ const savedUser = await newUser.save();
+
+ const token = jwt.sign({ userId: savedUser._id }, (process.env.JWT_KEY??'my-key'), { expiresIn: '1h' });
+
+ res.json({ token: token, username: savedUser.username, email: savedUser.email});
} catch (error) {
res.status(400).json({ error: error.message });
}});
diff --git a/users/userservice/user-service.test.js b/users/userservice/user-service.test.js
index 7b9e1b4f..ab9fa21e 100644
--- a/users/userservice/user-service.test.js
+++ b/users/userservice/user-service.test.js
@@ -4,7 +4,12 @@ const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
let app;
-
+let newUser = {
+ email: 'example@example.com',
+ username: 'testuser',
+ password: 'testpassword',
+ repeatPassword: 'testpassword'
+};
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
@@ -18,35 +23,91 @@ afterAll(async () => {
await mongoServer.stop();
});
+afterEach(async () => {
+ newUser = {
+ email: 'example@example.com',
+ username: 'testuser',
+ password: 'testpassword',
+ repeatPassword: 'testpassword'
+ };
+})
+
describe('User Service', () => {
it('should add a new user on POST /adduser', async () => {
- const newUser = {
- username: 'testuser',
- password: 'testpassword'
- };
-
const response = await request(app).post('/adduser').send(newUser);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('username', 'testuser');
});
- it('Should show missing field user /adduser', async () => {
+ it('Should show missing field email /adduser', async () => {
const response = await request(app).post('/adduser').send();
expect(response.status).toBe(400);
- expect(response.body).toHaveProperty('error', 'Missing required field: username');
+ expect(response.body).toHaveProperty('error', 'Missing required field: email');
});
it('Should not register user /adduser', async () => {
- const newUser = {
- username: 'testuser',
- password: 'testpassword'
- };
+ newUser.email = 'example2@example.com';
const response = await request(app).post('/adduser').send(newUser);
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('error', 'Username already in use');
});
-
+ it('Should not register user /adduser', async () => {
+ newUser.username = 'testuser2';
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Email already in use');
+ });
+
+});
+
+describe('User service validations', () => {
+ it('shows error message on wrong formed email', async () => {
+ newUser.email = "test"
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Wrong email format (example@example.com)');
+ });
+
+ it('shows error message on not equal passwords', async () => {
+ newUser.repeatPassword = newUser.repeatPassword + "n";
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Passwords dont match');
+ });
+
+ it('shows error message on password have spaces', async () => {
+ setPassword("1234 56789")
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Password cannot have spaces');
+ });
+
+ it('shows error message on password length is less than 8', async () => {
+ setPassword("12")
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Password must be at least 8 characters long');
+ });
+
+ it('shows error message on password length is more than 64', async () => {
+ setPassword("01234567890123456789012345678901234567890123456789012345678901234")
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Password must less than 64 characters long');
+ });
+
+ it('shows error message on username has spaces', async () => {
+ newUser.username = newUser.username + " yes"
+ const response = await request(app).post('/adduser').send(newUser);
+ expect(response.status).toBe(400);
+ expect(response.body).toHaveProperty('error', 'Username cannot have spaces');
+ });
});
+
+function setPassword(newPassword){
+ newUser.password = newPassword;
+ newUser.repeatPassword = newPassword;
+}
\ No newline at end of file
diff --git a/webapp/e2e/steps/gameMenu.steps.js b/webapp/e2e/steps/gameMenu.steps.js
index 215af2f7..9d00a32d 100644
--- a/webapp/e2e/steps/gameMenu.steps.js
+++ b/webapp/e2e/steps/gameMenu.steps.js
@@ -4,22 +4,36 @@ const setDefaultOptions = require('expect-puppeteer').setDefaultOptions;
const feature = loadFeature('./features/gameMenu.feature');
+const { register, login, logout } = require("../utils");
+
let page;
let browser;
+const email = "testUser@example.com";
+const username = "testUser"
+const password = "testUserPassword"
+
defineFeature(feature, test => {
beforeAll(async () => {
browser = await puppeteer.launch({
- slowMo: 20,
- defaultViewport: { width: 1920, height: 1080 },
- args: ['--window-size=1920,1080']
+ headless: "new",
+ slowMo: 40,
+ defaultViewport: { width: 1920, height: 1080 },
+ args: ['--window-size=1920,1080']
});
page = await browser.newPage();
- setDefaultOptions({ timeout: 10000 });
+ setDefaultOptions({ timeout: 30000 });
+
+ await register(page, email, username, password);
});
+ beforeEach(async () => {
+ await logout(page);
+ await login(page, username, password);
+ })
+
test('There should be visible three links', ({ given, then }) => {
given('I am on the game menu', async () => {
await page.goto('http://localhost:3000/menu');
@@ -29,7 +43,7 @@ defineFeature(feature, test => {
then('three buttons should be visible', async () => {
//await expect(page).toMatchElement('.linkButton');
const elements = await page.$$('.linkButton');
- expect(elements.length).toBeGreaterThan(0); // At least one element with class 'linkButton'
+ expect(elements.length).toBe(3);
});
});
test('New Game should go to game configurator', ({ given, when, then }) => {
diff --git a/webapp/e2e/steps/home.steps.js b/webapp/e2e/steps/home.steps.js
index 770b2a0a..3bae6848 100644
--- a/webapp/e2e/steps/home.steps.js
+++ b/webapp/e2e/steps/home.steps.js
@@ -11,9 +11,10 @@ defineFeature(feature, test => {
beforeAll(async () => {
browser = await puppeteer.launch({
- slowMo: 20,
- defaultViewport: { width: 1920, height: 1080 },
- args: ['--window-size=1920,1080']
+ headless: "new",
+ slowMo: 20,
+ defaultViewport: { width: 1920, height: 1080 },
+ args: ['--window-size=1920,1080']
});
page = await browser.newPage();
diff --git a/webapp/e2e/utils.js b/webapp/e2e/utils.js
new file mode 100644
index 00000000..b25f6aaa
--- /dev/null
+++ b/webapp/e2e/utils.js
@@ -0,0 +1,37 @@
+async function register(page, email, username, password) {
+ await page.goto('http://localhost:3000/addUser');
+ await page.waitForSelector('.general');
+
+ await page.type('input[name="email"]', email);
+ await page.type('input[name="username"]', username);
+ await page.type('input[name="password"]', password);
+ await page.type('input[name="repeat_password"]', password);
+ await page.click('button[type="submit"]');
+ //Wait for menu to load
+ await page.waitForSelector('.divMenu');
+}
+
+async function login(page, username, password) {
+ await page.goto('http://localhost:3000/login');
+ await page.waitForSelector('.general');
+
+ await page.type('input[type="text"]', username);
+ await page.type('input[type="password"]', password);
+ await page.click('button[type="submit"]');
+ //Wait for menu to load
+ await page.waitForSelector('.divMenu');
+}
+
+async function logout(page){
+ await page.click('.user-button');
+ await page.waitForSelector('.MuiMenu-paper', { visible: true });
+ await page.click('text=Log Out');
+ //Wait for home to load
+ await page.waitForSelector('.general');
+}
+
+module.exports = {
+ register,
+ login,
+ logout
+ };
\ No newline at end of file
diff --git a/webapp/package-lock.json b/webapp/package-lock.json
index 9ecc289d..2a9213a2 100644
--- a/webapp/package-lock.json
+++ b/webapp/package-lock.json
@@ -19,6 +19,7 @@
"express": "^4.19.2",
"i18n": "^0.15.1",
"jquery": "^3.7.1",
+ "js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.3.0",
"react": "^18.2.0",
@@ -18518,6 +18519,14 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
},
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
diff --git a/webapp/package.json b/webapp/package.json
index e843dbb5..c258e1f2 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -14,6 +14,7 @@
"express": "^4.19.2",
"i18n": "^0.15.1",
"jquery": "^3.7.1",
+ "js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.3.0",
"react": "^18.2.0",
diff --git a/webapp/src/App.js b/webapp/src/App.js
index b79ab18b..69c168cf 100644
--- a/webapp/src/App.js
+++ b/webapp/src/App.js
@@ -11,7 +11,7 @@ import Container from '@mui/material/Container';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import './custom.css';
import HistoricalView from './components/HistoricalData/HistoricalView';
-import { UserContextProvider } from './components/loginAndRegistration/UserContext';
+import Cookies from 'js-cookie';
import GameConfigurator from './components/GameConfigurator/GameConfigurator';
import RankingView from './components/ranking/RankingView';
@@ -20,28 +20,30 @@ function App() {
useEffect(() => {
document.title = 'WIQ';
}, []);
+
+ //The double !! converts an expression that can be a boolean into an actual boolean
+ const isLoggedIn = !!Cookies.get('user');
+
return (
{user.username}
- ) : null} + + {Cookies.get('user') ? ( + <> + + + > + ) : null} +{t(error)}
+ )); + } + return null; + }; + return (