Compare commits
29 Commits
main
...
lightemera
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a228b3f75 | |||
| 20de98bc05 | |||
| 6e6bdc21ba | |||
| 16fdb7a444 | |||
| 279435d3ae | |||
| bc2d085b21 | |||
| da3e6b566d | |||
| 1175c41f20 | |||
| 689ef0c314 | |||
| 7f6aa5a499 | |||
| 456deaa958 | |||
| df2839720a | |||
| 54f6006289 | |||
| a8c82c036a | |||
| eb8f984700 | |||
| 9b671a5e2b | |||
| 5a7656046c | |||
| 57271f51b4 | |||
| e94ea47f61 | |||
| 99eb7828ea | |||
| 5d2be6584e | |||
| 85baf6a724 | |||
| 22b61433bc | |||
| e0e0649908 | |||
| 8e7e8f22f6 | |||
| fec22e6388 | |||
| 612c5a8f13 | |||
| bae870b22f | |||
| ea881e2403 |
50
.eslintrc.json
Normal file
50
.eslintrc.json
Normal 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
6
api/.env.sample
Normal 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
177
api/.gitignore
vendored
Normal 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
133
api/Classes/Games.js
Normal 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
13
api/README.md
Normal 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
BIN
api/bun.lockb
Executable file
Binary file not shown.
148
api/database.sql
Normal file
148
api/database.sql
Normal 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 n’assaini 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, 'L’air 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 à l’horizon 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 c’est l’activité économique qui pollue.'),
|
||||
(43, 4, 'La France est une goutte d’eau : ce sont la Chine et l’Inde 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 d’ici 2100 ?'),
|
||||
(58, 2, 'Efficacite des energies renouvelables'),
|
||||
(59, 5, 'Le réchauffement climatique, c’est 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, 'L’augmentation de la température moyenne de la planète est due à un cycle naturel et n’a 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é n’est pas menacée, il y a toujours autant d’espè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 l’environnement 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 l’environnement 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 n’affecte 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, 'L’agriculture biologique est plus respectueuse de l’environnement et de la santé que l’agriculture conventionnelle.'),
|
||||
(77, 5, 'politiques environnementales strictes'),
|
||||
(78, 1, 'Les végétariens et les végétaliens sont plus respectueux de l’environnement et de la santé que les omnivores.');
|
||||
39
api/index.js
Normal file
39
api/index.js
Normal 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
22
api/jsconfig.json
Normal 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
0
api/logs/access.log
Normal file
26
api/modules/database.js
Normal file
26
api/modules/database.js
Normal 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
19
api/modules/log.js
Normal 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
20
api/modules/random.js
Normal 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');
|
||||
}
|
||||
54
api/modules/requestHandler.js
Normal file
54
api/modules/requestHandler.js
Normal 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
39
api/modules/token.js
Normal 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
22
api/package.json
Normal 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
44
api/routes/games.js
Normal 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
40
api/routes/leaderboard.js
Normal 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
35
api/routes/test.js
Normal 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
27
api/routes/themes.js
Normal 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
95
api/routes/users.js
Normal 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;
|
||||
Reference in New Issue
Block a user