29 Commits

Author SHA1 Message Date
5a228b3f75 Fix 2023-12-08 05:31:56 +01:00
20de98bc05 Fix 2023-12-08 03:53:09 +01:00
6e6bdc21ba Added body support 2023-12-08 03:03:47 +01:00
16fdb7a444 Clean 2023-12-08 02:52:59 +01:00
279435d3ae Fix 2023-12-08 02:52:14 +01:00
bc2d085b21 test4 2023-12-08 02:50:06 +01:00
da3e6b566d Fix 2023-12-08 02:48:34 +01:00
1175c41f20 test3 2023-12-08 02:47:31 +01:00
689ef0c314 test2 2023-12-08 02:46:18 +01:00
7f6aa5a499 Test 2023-12-08 02:43:43 +01:00
456deaa958 Test 2023-12-08 02:42:23 +01:00
df2839720a Remove system load check 2023-12-08 02:38:11 +01:00
54f6006289 Added theme:id endpoint 2023-12-08 02:31:33 +01:00
a8c82c036a Fix verify 2023-12-08 02:30:08 +01:00
eb8f984700 change endpoint name 2023-12-08 02:28:05 +01:00
9b671a5e2b Fix 2023-12-08 02:12:49 +01:00
5a7656046c Updated games 2023-12-08 01:37:38 +01:00
57271f51b4 Fix missing export 2023-12-08 00:10:10 +01:00
e94ea47f61 Added verifyToken, themes and set routes 2023-12-08 00:09:06 +01:00
99eb7828ea Fix 2023-12-07 23:48:35 +01:00
5d2be6584e Fix 2023-12-07 23:47:47 +01:00
85baf6a724 Fixes to tokens 2023-12-07 23:46:26 +01:00
22b61433bc Fix 2023-12-07 23:31:27 +01:00
e0e0649908 Working on games 2023-12-07 23:26:41 +01:00
8e7e8f22f6 Fix and Game Class 2023-12-07 22:27:05 +01:00
fec22e6388 Fixed missing packages and database 2023-12-07 21:43:37 +01:00
612c5a8f13 Added routes 2023-12-07 21:27:09 +01:00
bae870b22f Fix requirements 2023-12-07 20:42:24 +01:00
ea881e2403 First commit 2023-12-07 20:35:55 +01:00
21 changed files with 1009 additions and 0 deletions

50
.eslintrc.json Normal file
View File

@@ -0,0 +1,50 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module"
},
"rules": {
"arrow-spacing": ["warn", { "before": true, "after": true }],
"brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
"comma-dangle": ["error", "always-multiline"],
"comma-spacing": "error",
"comma-style": "error",
"curly": ["error", "multi-line", "consistent"],
"dot-location": ["error", "property"],
"handle-callback-err": "off",
"indent": ["error", "tab"],
"keyword-spacing": "error",
"max-nested-callbacks": ["error", { "max": 4 }],
"max-statements-per-line": ["error", { "max": 2 }],
"no-console": "off",
"no-empty-function": "error",
"no-floating-decimal": "error",
"no-inline-comments": "error",
"no-lonely-if": "error",
"no-multi-spaces": "error",
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
"no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
"no-trailing-spaces": ["error"],
"no-var": "error",
"object-curly-spacing": ["error", "always"],
"prefer-const": "error",
"quotes": ["error", "single"],
"semi": ["error", "always"],
"space-before-blocks": "error",
"space-before-function-paren": ["error", {
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}],
"space-in-parens": "error",
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": "error",
"yoda": "error"
}
}

6
api/.env.sample Normal file
View File

@@ -0,0 +1,6 @@
DATABASE_HOST="127.0.0.1"
DATABASE_NAME=nuitdelinfo2023
DATABASE_USER=nuitdelinfo2023
DATABASE_PASSWORD=""
JWT_SECRET=""
PORT=3000

177
api/.gitignore vendored Normal file
View File

@@ -0,0 +1,177 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
# Level
tokensDB/

133
api/Classes/Games.js Normal file
View File

@@ -0,0 +1,133 @@
import { pool } from '../modules/database.js';
class Game {
constructor(id = null, playerId) {
this.id = id;
this.player = playerId;
this.questions = [];
}
async get() {
try {
const [rows] = await pool.execute(
'SELECT * FROM games WHERE id = ? AND player = ? LIMIT 1', [this.id, this.player],
);
if (!rows.length) return false;
const [questions] = await pool.execute(
'SELECT * FROM games_questions WHERE game = ?', [this.id],
);
questions.forEach(q => {
const question = new Question(q.id, q.question);
this.questions.push(question);
});
return true;
}
catch (error) {
console.error(error);
return false;
}
}
async create() {
try {
const [rows] = await pool.execute(
'INSERT INTO games (player) VALUES (?)', [this.player],
);
this.id = rows.insertId;
return true;
}
catch (error) {
console.error(error);
return false;
}
}
async generateQuestions(themeId) {
try {
const [rows] = await pool.execute(
'SELECT * FROM questions WHERE theme = ? ORDER BY RAND() LIMIT 10', [themeId],
);
rows.forEach(async row => {
const question = new Question(row.id, row.question);
this.questions.push(question);
await pool.execute('INSERT INTO games_questions (game, question) VALUES (?, ?)', [this.id, row.id]);
});
return true;
}
catch (error) {
console.error(error);
return false;
}
}
}
class Question {
constructor(id = null, question = null) {
this.id = id;
this.question = question;
this.answers = [];
}
async get() {
try {
const [rows] = await pool.execute(
'SELECT * FROM questions WHERE id = ? LIMIT 1', [this.id],
);
if (!rows.length) return false;
this.question = rows[0].question;
const [answers] = await pool.execute(
'SELECT * FROM answers WHERE question = ?', [this.id],
);
answers.forEach(a => {
const answer = new Answer(a.id, a.answer, a.correct);
this.answers.push(answer);
});
return true;
}
catch (error) {
console.error(error);
return false;
}
}
async fetchAnswers() {
try {
const [rows] = await pool.execute(
'SELECT * FROM answers WHERE question = ?', [this.id],
);
rows.forEach(row => {
const answer = new Answer(row.id, row.answer, row.correct);
this.answers.push(answer);
});
return true;
}
catch (error) {
console.error(error);
return false;
}
}
async verifyAnswer(answerId) {
try {
const [rows] = await pool.execute(
'SELECT * FROM answers WHERE question = ? AND id = ? AND correct = 1', [this.id, answerId],
);
if (!rows.length) return false;
return true;
}
catch (error) {
console.error(error);
return false;
}
}
}
class Answer {
constructor(id = null, answer = null, correct = null) {
this.id = id;
this.answer = answer;
this.correct = correct;
}
}
export { Game, Question, Answer };

13
api/README.md Normal file
View File

@@ -0,0 +1,13 @@
# nuitdelinfo2023-api
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.js
```

BIN
api/bun.lockb Executable file

Binary file not shown.

148
api/database.sql Normal file
View File

@@ -0,0 +1,148 @@
SET default_storage_engine = InnoDB;
DROP DATABASE IF EXISTS `nuitdelinfo2023`;
CREATE DATABASE IF NOT EXISTS `nuitdelinfo2023`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
DROP USER IF EXISTS 'nuitdelinfo2023';
CREATE USER 'nuitdelinfo2023'@'localhost' IDENTIFIED BY 'PASSWORD';
GRANT ALL PRIVILEGES ON nuitdelinfo2023.* TO 'nuitdelinfo2023'@'localhost';
USE `nuitdelinfo2023`;
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(64) NOT NULL,
password VARCHAR(255) NOT NULL,
score INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (id),
INDEX ur_username_idx (username)
) ENGINE=InnoDB;
CREATE TABLE games (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
player INT UNSIGNED NOT NULL,
PRIMARY KEY (id),
INDEX g_player_idx (player)
) ENGINE=InnoDB;
CREATE TABLE themes (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
theme VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE questions (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
theme INT UNSIGNED NOT NULL,
question VARCHAR(255) NOT NULL,
PRIMARY KEY (id),
INDEX q_theme_idx (theme)
) ENGINE=InnoDB;
CREATE TABLE answers (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
question INT UNSIGNED NOT NULL,
answer VARCHAR(255) NOT NULL,
correct BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (id),
INDEX a_question_idx (question)
) ENGINE=InnoDB;
CREATE TABLE game_questions (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
game INT UNSIGNED NOT NULL,
question INT UNSIGNED NOT NULL,
score INT UNSIGNED,
PRIMARY KEY (id),
INDEX gq_game_idx (game),
INDEX gq_question_idx (question)
) ENGINE=InnoDB;
INSERT INTO themes (theme) VALUES ('Environnement');
INSERT INTO themes (theme) VALUES ('Energie Renouvelable');
INSERT INTO themes (theme) VALUES ("Recyclage et consommation d'energie");
INSERT INTO themes (theme) VALUES ('Pollution et energie fossile');
INSERT INTO themes (theme) VALUES ('Politique');
INSERT INTO questions (id, theme, question)
VALUES
(1, 3, 'Il vaut mieux jeter les cendres de sa cheminée'),
(2, 2, 'Dans le nord, les panneaux solaires ne sont pas efficaces'),
(3, 3, 'Il vaut mieux faire la vaisselle à la main'),
(4, 4, 'Le diesel pollue moins que l''essence'),
(5, 3, 'Utiliser des baguettes jetables en bois, c''est écolo'),
(6, 4, 'L''encens nassaini pas l''air'),
(7, 4, 'En ville, il vaut mieux ouvrir ses fenêtres'),
(8, 1, 'Les gratte-ciels, c''est mauvais pour l''environnement'),
(9, 1, 'les maisons en bois brûlent plus facilement'),
(10, 1, 'Les produits écologiques sont plus chers'),
(11, 4, 'Les jardiniers polluent plus que les agriculteurs'),
(12, 1, 'Le lino, c''est un revêtment mauvais pour l''environnement'),
(13, 4, 'Le charbon ne représente plus grand chose'),
(14, 1, 'Les animaux domestiquent nuisent à l''environnement'),
(15, 1, 'Les produits bio sont forcément écologiques'),
(16, 4, 'ne pas laver sa voiture pollue moins que la laver régulièrement'),
(17, 2, 'les panneaux solaires se valent tous'),
(18, 3, 'il vaut mieux consommer l''électricité qu''on produit soi-même que l''acheter à EDF'),
(19, 1, 'Les vêtements en coton, c''est écolo'),
(20, 4, 'Utiliser du papier favorise la déforestation'),
(21, 4, 'Lair est plus pollué en ville quà la campagne'),
(22, 1, 'Un sapin artificiel, c''est mieux pour l''environnement'),
(23, 1, 'Il n''y a pas de produit vraiment bio'),
(24, 1, 'On peut utiliser n''importe quel bois, c''est toujours écologique'),
(25, 2, 'Les éoliennes tuent les oiseaux et font du bruit'),
(26, 4, 'Ne pas utiliser de bouchons en liège, c''est mieux pour la nature'),
(27, 2, 'les panneaux solaires ne sont pas rentables du fait d''une durée de vie trop courte'),
(28, 1, 'un produit sans sucre ne contient pas de sucre'),
(29, 3, 'Il faut laver la vaisselle à l''eau chaude'),
(30, 2, 'les éoliennes produisent plus en été'),
(31, 4, 'en hiver, il vaut mieux moins aérer'),
(32, 3, 'Appareils électroniques en veille'),
(33, 5, 'la politique internationale a du mal à atteindre des accords significatifs sur les changements climatiques'),
(34, 2, 'les panneaux solaires se recyclent mal'),
(35, 1, 'Il est impossible de nourrir toute la planète avec une agriculture bio.'),
(36, 4, 'La voiture électrique va sauver la planète.'),
(37, 2, 'Les énergies renouvelables (ENR) sont plus chères à produire.'),
(38, 4, 'On ne pourra pas atteindre la neutralité carbone sans le nucléaire en France à lhorizon 2050.'),
(39, 3, 'Le numérique lutte contre le changement climatique en réduisant la consommation dénergie.'),
(40, 4, 'Les mécanismes de compensation carbone sont du greenwashing et ne servent à rien.'),
(41, 3, 'Adopter un mode de vie écolo, ce serait revenir au Moyen Âge.'),
(42, 4, 'Si les individus adoptent des gestes responsables, cela ne changera rien, car cest lactivité économique qui pollue.'),
(43, 4, 'La France est une goutte deau : ce sont la Chine et lInde qui émettent les plus grandes quantités de gaz à effet de serre.'),
(44, 5, 'politiques environnementales influencées'),
(45, 5, 'accords internationaux sur le climat'),
(46, 5, 'politiques de tarification du carbone'),
(47, 5, 'politiques environnementales.'),
(48, 5, 'Actions individuelles'),
(49, 3, 'Intérêt du recyclage'),
(50, 4, 'Quel est le principal gaz à effet de serre responsable du réchauffement climatique ?'),
(51, 3, 'eau du robinet ou eau en bouteille'),
(52, 2, 'viabilité de lénergie solaire et éolienne'),
(53, 1, 'Quelle est la solution la plus efficace pour lutter contre le réchauffement climatique ?'),
(54, 5, 'Quel est le nom du célèbre ours polaire qui a ému le monde entier en apparaissant amaigri et affamé sur la banquise ?'),
(55, 4, 'Quel est le pays qui émet le plus de gaz à effet de serre par habitant ?'),
(56, 5, 'Quel est le nom du mouvement de jeunesse qui organise des grèves scolaires pour le climat ?'),
(57, 5, 'Quel est le nom du traité international qui vise à limiter le réchauffement climatique à 2°C dici 2100 ?'),
(58, 2, 'Efficacite des energies renouvelables'),
(59, 5, 'Le réchauffement climatique, cest un complot des élites pour nous imposer leur agenda politique, non ?'),
(60, 2, 'durabilité des stockage dénergie'),
(61, 2, 'Transition energetique trop couteuse'),
(62, 5, 'les politiques nationales sur les énergies renouvelables sont cruciales pour atteindre les objectifs climatiques mondiaux'),
(63, 4, 'Les émissions de CO2 sont le principal facteur du réchauffement climatique et doivent être réduites au maximum.'),
(64, 1, 'Laugmentation de la température moyenne de la planète est due à un cycle naturel et na rien à voir avec les activités humaines.'),
(65, 2, 'Les énergies renouvelables sont trop chères et pas assez efficaces pour remplacer les énergies fossiles.'),
(66, 1, 'La biodiversité nest pas menacée, il y a toujours autant despèces vivantes sur la planète.'),
(67, 3, 'Le recyclage est la meilleure solution pour réduire les déchets et préserver les ressources naturelles.'),
(68, 1, 'Les produits biologiques sont meilleurs pour lenvironnement que les produits conventionnels.'),
(69, 4, 'Les voitures électriques sont plus écologiques que les voitures à essence ou diesel.'),
(70, 4, 'Le plastique est le pire ennemi de lenvironnement et il faut léliminer complètement.'),
(71, 1, 'Le réchauffement climatique est causé par les activités humaines.'),
(72, 1, 'La fonte des glaces polaires va faire monter le niveau des océans et submerger les zones côtières.'),
(73, 2, 'Les énergies renouvelables sont plus propres et plus efficaces que les énergies fossiles.'),
(74, 1, 'La déforestation est un problème mineur qui naffecte que quelques régions du monde.'),
(75, 3, 'Le recyclage est une solution efficace pour réduire les déchets et préserver les ressources naturelles.'),
(76, 1, 'Lagriculture biologique est plus respectueuse de lenvironnement et de la santé que lagriculture conventionnelle.'),
(77, 5, 'politiques environnementales strictes'),
(78, 1, 'Les végétariens et les végétaliens sont plus respectueux de lenvironnement et de la santé que les omnivores.');

39
api/index.js Normal file
View File

@@ -0,0 +1,39 @@
import fs from 'fs';
import path from 'path';
import cors from 'cors';
import logger from 'morgan';
import express from 'express';
import { log } from './modules/log.js';
import { speedLimiter } from './modules/requestHandler.js';
import testRouter from './routes/test.js';
import usersRouter from './routes/users.js';
import leaderboardRouter from './routes/leaderboard.js';
import themeRouter from './routes/themes.js';
import gameRouter from './routes/games.js';
const app = express();
app.set('trust proxy', 1);
app.use(express.json());
app.use(logger('dev'));
app.use(speedLimiter);
app.use(logger('combined', { stream: fs.createWriteStream(path.join(__dirname, 'logs/access.log'), { flags: 'a' }) }));
app.use(cors({
origin: '*',
}));
app.use(express.static('public'));
// routes
app.use('/api/test', testRouter);
app.use('/api/users', usersRouter);
app.use('/api/leaderboard', leaderboardRouter);
app.use('/api/themes', themeRouter);
app.use('/api/games', gameRouter);
// run the API
app.listen(process.env.PORT, async () => {
log(`running at port ${process.env.PORT}`);
});

22
api/jsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"noEmit": true,
"composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"types": [
"bun-types" // add Bun global
]
}
}

0
api/logs/access.log Normal file
View File

26
api/modules/database.js Normal file
View File

@@ -0,0 +1,26 @@
import mysql from 'mysql2';
const connection = mysql.createConnection({
host: process.env.DATABASE_HOST,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
});
const pool = mysql.createPool({
host: process.env.DATABASE_HOST,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
}).promise();
function createPool(host, user, password, db) {
const newPool = mysql.createPool({
host: host,
user: user,
password: password,
database: db,
}).promise();
return newPool;
}
export { connection, pool, createPool };

19
api/modules/log.js Normal file
View File

@@ -0,0 +1,19 @@
import pino from 'pino';
const logger = pino();
export function log(x) {
logger.info(x);
}
export function debug(x) {
logger.debug(x);
}
export function warn(x) {
logger.warn(x);
}
export function error(x) {
logger.error(x);
}

20
api/modules/random.js Normal file
View File

@@ -0,0 +1,20 @@
export function random(x, y) {
return crypto.randomInt(x, y + 1);
}
export function random2(min, max) {
const range = max - min + 1;
const byteLength = Math.ceil(Math.log2(range) / 8);
let randomValue;
do {
const randomBytes = crypto.randomBytes(byteLength);
randomValue = parseInt(randomBytes.toString('hex'), 16);
} while (randomValue >= range);
return randomValue + min;
}
export function randomHEX(x) {
return crypto.randomBytes(x).toString('hex');
}

View File

@@ -0,0 +1,54 @@
import rateLimit from 'express-rate-limit';
import slowDown from 'express-slow-down';
import http from 'http';
import os from 'os';
const requestLimiter = rateLimit({
windowMs: 60 * 1000,
max: 5,
standardHeaders: true,
legacyHeaders: false,
message: 'Too many requests from this IP, please try again later',
});
const speedLimiter = slowDown({
windowMs: 60 * 1000,
delayAfter: 5,
delayMs: (hits) => hits * 100,
});
function checkSystemLoad(req, res, next) {
const load = os.loadavg()[0];
const cores = os.cpus().length;
const threshold = cores * 0.7;
if (load > threshold) {
return respondWithStatus(res, 503, 'API Unavailable - System Overloaded!');
}
return next();
}
function respondWithStatus(res, statusCode, message) {
const response = { status: statusCode, message: message };
if (statusCode >= 400 && statusCode <= 599) {
response.error = http.STATUS_CODES[statusCode];
}
return res.status(statusCode).json(response);
}
function respondWithStatusJSON(res, statusCode, JSON) {
const response = { status: statusCode, JSON };
if (statusCode >= 400 && statusCode <= 599) {
response.error = http.STATUS_CODES[statusCode];
}
return res.status(statusCode).json(response);
}
export {
requestLimiter,
speedLimiter,
checkSystemLoad,
respondWithStatus,
respondWithStatusJSON,
};

39
api/modules/token.js Normal file
View File

@@ -0,0 +1,39 @@
/* eslint-disable no-undef */
import jwt from 'jsonwebtoken';
import { respondWithStatus } from './requestHandler.js';
import { pool } from './database.js';
// Generate a new JWT
const generateToken = async (userId, password) => {
return await jwt.sign({ userId: userId, password: password }, process.env.JWT_SECRET, { expiresIn: '7d' });
};
// Middleware to verify the JWT and set req.userId
const verifyToken = async (req, res, next) => {
const token = req.headers.authorization;
if (!token) return await respondWithStatus(res, 401, 'No token provided');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
const [rows] = await pool.execute(
'SELECT * FROM users WHERE id = ? LIMIT 1', [req.userId],
);
if (!rows.length) return await respondWithStatus(res, 404, 'User not found!');
const passwordMatch = await Bun.password.verify(decoded.password, rows[0].password);
if (!passwordMatch) return await respondWithStatus(res, 401, 'Token is invalid');
const now = Date.now().valueOf() / 1000;
if (decoded.exp - now <= 0) {
return await respondWithStatus(res, 401, 'Token is invalid');
}
req.username = rows[0].username;
next();
}
catch (error) {
return await respondWithStatus(res, 401, 'Invalid user');
}
};
export { generateToken, verifyToken };

22
api/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "nuitdelinfo2023-api",
"module": "index.js",
"type": "module",
"devDependencies": {
"bun-types": "latest",
"eslint": "^8.55.0"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-slow-down": "^2.0.1",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"mysql2": "^3.6.5",
"pino": "^8.16.2"
}
}

44
api/routes/games.js Normal file
View File

@@ -0,0 +1,44 @@
import express from 'express';
import { pool } from '../modules/database.js';
import { verifyToken } from '../modules/token.js';
import { respondWithStatus, respondWithStatusJSON } from '../modules/requestHandler.js';
import { Game, Question } from '../Classes/Games.js';
const router = express.Router();
router.post('/create/:theme', verifyToken, async (req, res) => {
const game = new Game(null, req.userId);
await game.create();
await game.generateQuestions(req.params.theme);
return await respondWithStatusJSON(res, 200, {
message: 'Successfully created game',
game,
});
});
router.post('/verify/:game', verifyToken, async (req, res) => {
const [rows] = await pool.execute(
'SELECT * FROM games WHERE id = ? AND player = ? LIMIT 1', [req.params.game, req.userId],
);
if (!rows.length) return await respondWithStatus(res, 404, 'Game not found');
const { question, answer } = req.body;
if (![question, answer].every(Boolean)) return await respondWithStatus(res, 400, 'Missing fields');
const q = new Question(question);
const [gameQuestions] = await pool.execute('SELECT * FROM game_questions WHERE game = ? AND question = ?', [req.params.game, question]);
if (!gameQuestions.length) return await respondWithStatus(res, 404, 'Question not found');
if (gameQuestions[0].score) return await respondWithStatus(res, 400, 'Question already answered');
if (q.verifyAnswer(answer)) {
await pool.execute('UPDATE game_questions SET score = 1 WHERE game = ? AND question = ?', [req.params.game, question]);
res.status(200).json({
message: 'Answer is correct',
});
}
else {
res.status(200).json({
message: 'Answer is incorrect',
});
}
});
export default router;

40
api/routes/leaderboard.js Normal file
View File

@@ -0,0 +1,40 @@
/* eslint-disable no-undef */
import express from 'express';
import { pool } from '../modules/database.js';
import { requestLimiter, respondWithStatus, respondWithStatusJSON } from '../modules/requestHandler.js';
const router = express.Router();
router.get('/', requestLimiter, async (req, res) => {
try {
const [rows] = await pool.execute('SELECT * FROM users');
if (!rows.length) return await respondWithStatus(res, 404, 'There are no users');
return await respondWithStatusJSON(res, 200, {
message: 'Successfully retrieved users',
users: rows,
});
}
catch (error) {
console.error(error);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
router.get('/:username', requestLimiter, async (req, res) => {
try {
const [rows] = await pool.execute('SELECT u.*, (SELECT COUNT(*) + 1 FROM users AS uu WHERE uu.score > u.score) AS rank FROM users AS u WHERE username = ? LIMIT 1', [req.params.username]);
if (!rows.length) return await respondWithStatus(res, 404, 'There are no users');
return await respondWithStatusJSON(res, 200, {
message: 'Successfully retrieved user',
users: rows[0],
});
}
catch (error) {
console.error(error);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
export default router;

35
api/routes/test.js Normal file
View File

@@ -0,0 +1,35 @@
import express from 'express';
const router = express.Router();
router.get('/', (req, res) => {
res.status(200).json({ code: 200, message:'Received GET request from ip:' + req.ip });
});
router.post('/', (req, res) => {
res.status(200).json({ code: 200, message:'Received POST request' });
});
router.patch('/', (req, res) => {
res.status(200).json({ code: 200, message:'Received PUT request' });
});
router.put('/', (req, res) => {
res.status(200).json({ code: 200, message:'Received PUT request' });
});
router.delete('/', (req, res) => {
res.status(200).json({ code: 200, message:'Received DELETE request' });
});
export default router;

27
api/routes/themes.js Normal file
View File

@@ -0,0 +1,27 @@
import express from 'express';
import { verifyToken } from '../modules/token.js';
import { respondWithStatus, respondWithStatusJSON } from '../modules/requestHandler.js';
import { pool } from '../modules/database.js';
const router = express.Router();
// send list of themes
router.get('/', verifyToken, async (req, res) => {
const [rows] = await pool.execute('SELECT * FROM themes');
if (!rows.length) return await respondWithStatus(res, 404, 'There are no themes');
return await respondWithStatusJSON(res, 200, {
message: 'Successfully retrieved themes',
themes: rows,
});
});
router.get('/:id', verifyToken, async (req, res) => {
const [rows] = await pool.execute('SELECT * FROM themes WHERE id = ? LIMIT 1', [req.params.id]);
if (!rows.length) return await respondWithStatus(res, 404, 'Theme not found');
return await respondWithStatusJSON(res, 200, {
message: 'Successfully retrieved theme',
themes: rows[0],
});
});
export default router;

95
api/routes/users.js Normal file
View File

@@ -0,0 +1,95 @@
/* eslint-disable no-undef */
import express from 'express';
import jwt from 'jsonwebtoken';
import { pool } from '../modules/database.js';
import { generateToken } from '../modules/token.js';
import { requestLimiter, respondWithStatus, respondWithStatusJSON } from '../modules/requestHandler.js';
const router = express.Router();
router.post('/register', requestLimiter, async (req, res) => {
const { username, password } = req.body;
if ([ username, password ].every(Boolean)) {
try {
const [existingUsername] = await pool.execute('SELECT * FROM users WHERE username = ? LIMIT 1', [username]);
if (existingUsername.length) return await respondWithStatus(res, 400, 'Username is already taken');
const hashedPassword = await Bun.password.hash(password);
const [result] = await pool.execute('INSERT INTO users (username, password) VALUES (?, ?)', [ username, hashedPassword ]);
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error storing user');
const user = await pool.execute('SELECT * FROM users WHERE username = ? LIMIT 1', [ username ]);
const token = await generateToken(user[0].id, password);
return await respondWithStatusJSON(res, 200, { message: 'Successfully registered', token: token, username: username });
}
catch (error) {
console.error(error);
return await respondWithStatus(res, 500, 'An error has occured');
}
}
else {
return await respondWithStatus(res, 400, 'Missing fields');
}
});
router.post('/login', requestLimiter, async (req, res) => {
const { username, password } = req.body;
if ([username, password].every(Boolean)) {
try {
const [rows] = await pool.execute(
'SELECT * FROM users WHERE username = ? LIMIT 1', [username],
);
if (!rows.length) return await respondWithStatus(res, 404, 'Incorrect username or email');
const user = rows[0];
const passwordMatch = await Bun.password.verify(password, user.password);
if (!passwordMatch) return await respondWithStatus(res, 401, 'Incorrect password');
const token = await generateToken(user.id, password);
return await respondWithStatusJSON(res, 200, {
message: 'Login successful',
token: token,
user: {
id: user.id,
username: user.username,
},
});
}
catch (error) {
console.error(error);
return await respondWithStatus(res, 500, 'An error has occured');
}
}
else {
return await respondWithStatus(res, 400, 'Missing fields');
}
});
router.post('/verify', requestLimiter, async (req, res) => {
const token = req.headers.authorization || req.body.token;
if (!token) return await respondWithStatus(res, 401, 'No token provided');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const [rows] = await pool.execute(
'SELECT * FROM users WHERE id = ? LIMIT 1', [decoded.userId],
);
if (!rows.length) return await respondWithStatus(res, 404, 'User not found!');
const passwordMatch = await Bun.password.verify(decoded.password, rows[0].password);
if (!passwordMatch) return await respondWithStatus(res, 401, 'Token is invalid');
const now = Date.now().valueOf() / 1000;
if (decoded.exp - now <= 0) {
return await respondWithStatus(res, 401, 'Token is invalid');
}
return await respondWithStatusJSON(res, 200, {
message: 'Token is valid',
user: {
id: rows[0].id,
username: rows[0].username,
},
});
}
catch (error) {
return await respondWithStatus(res, 401, 'Invalid user');
}
});
export default router;