From ea881e2403fce9dd5017eb46c94bdb23dd7452d2 Mon Sep 17 00:00:00 2001 From: Lightemerald Date: Thu, 7 Dec 2023 20:35:55 +0100 Subject: [PATCH] First commit --- .eslintrc.json | 50 ++++++++++ api/.env.sample | 6 ++ api/.gitignore | 173 ++++++++++++++++++++++++++++++++++ api/README.md | 15 +++ api/bun.lockb | Bin 0 -> 79339 bytes api/database.sql | 25 +++++ api/index.js | 32 +++++++ api/jsconfig.json | 22 +++++ api/logs/access.log | 0 api/modules/database.js | 26 +++++ api/modules/log.js | 19 ++++ api/modules/random.js | 20 ++++ api/modules/requestHandler.js | 54 +++++++++++ api/modules/token.js | 52 ++++++++++ api/package.json | 20 ++++ api/routes/leaderboard.js | 40 ++++++++ api/routes/test.js | 35 +++++++ api/routes/users.js | 79 ++++++++++++++++ 18 files changed, 668 insertions(+) create mode 100644 .eslintrc.json create mode 100644 api/.env.sample create mode 100644 api/.gitignore create mode 100644 api/README.md create mode 100755 api/bun.lockb create mode 100644 api/database.sql create mode 100644 api/index.js create mode 100644 api/jsconfig.json create mode 100644 api/logs/access.log create mode 100644 api/modules/database.js create mode 100644 api/modules/log.js create mode 100644 api/modules/random.js create mode 100644 api/modules/requestHandler.js create mode 100644 api/modules/token.js create mode 100644 api/package.json create mode 100644 api/routes/leaderboard.js create mode 100644 api/routes/test.js create mode 100644 api/routes/users.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..2fcbea3 --- /dev/null +++ b/.eslintrc.json @@ -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" + } +} \ No newline at end of file diff --git a/api/.env.sample b/api/.env.sample new file mode 100644 index 0000000..a57d2c1 --- /dev/null +++ b/api/.env.sample @@ -0,0 +1,6 @@ +DATABASE_HOST="127.0.0.1" +DATABASE_NAME=nuitdelinfo2023 +DATABASE_USER=nuitdelinfo2023 +DATABASE_PASSWORD="" +JWT_SECRET="" +PORT=3000 \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000..d9e2431 --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,173 @@ +# 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 diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..276a965 --- /dev/null +++ b/api/README.md @@ -0,0 +1,15 @@ +# nuitdelinfo2023-api + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.js +``` + +This project was created using `bun init` in bun v1.0.13. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/api/bun.lockb b/api/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..48fddcc705986c48b3a12320e4a8fde3a4b6201c GIT binary patch literal 79339 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 literal 0 HcmV?d00001 diff --git a/api/database.sql b/api/database.sql new file mode 100644 index 0000000..6394258 --- /dev/null +++ b/api/database.sql @@ -0,0 +1,25 @@ +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'@'%' IDENTIFIED BY 'PASSWORD'; +GRANT ALL PRIVILEGES ON airjet.* TO 'nuitdelinfo2023'@'%'; + +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), + CONSTRAINT u_user_type_id + FOREIGN KEY (user_type_id) + REFERENCES user_types(id) + ON DELETE RESTRICT + ON UPDATE CASCADE, + INDEX ur_user_type_idx (user_type_id) +) ENGINE=InnoDB; \ No newline at end of file diff --git a/api/index.js b/api/index.js new file mode 100644 index 0000000..b495a98 --- /dev/null +++ b/api/index.js @@ -0,0 +1,32 @@ +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'; +import { speedLimiter, checkSystemLoad } from './modules/requestHandler'; + +import testRouter from './routes/test'; + +const app = express(); +app.set('trust proxy', 1); + +app.use(express.json()); +app.use(logger('dev')); +app.use(speedLimiter); +app.use(checkSystemLoad); +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); + +// run the API +app.listen(process.env.PORT, async () => { + log(`running at port ${process.env.PORT}`); +}); \ No newline at end of file diff --git a/api/jsconfig.json b/api/jsconfig.json new file mode 100644 index 0000000..7556e1d --- /dev/null +++ b/api/jsconfig.json @@ -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 + ] + } +} diff --git a/api/logs/access.log b/api/logs/access.log new file mode 100644 index 0000000..e69de29 diff --git a/api/modules/database.js b/api/modules/database.js new file mode 100644 index 0000000..d29e367 --- /dev/null +++ b/api/modules/database.js @@ -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 }; diff --git a/api/modules/log.js b/api/modules/log.js new file mode 100644 index 0000000..e1d1baa --- /dev/null +++ b/api/modules/log.js @@ -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); +} \ No newline at end of file diff --git a/api/modules/random.js b/api/modules/random.js new file mode 100644 index 0000000..8063195 --- /dev/null +++ b/api/modules/random.js @@ -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'); +} diff --git a/api/modules/requestHandler.js b/api/modules/requestHandler.js new file mode 100644 index 0000000..2c6e0ea --- /dev/null +++ b/api/modules/requestHandler.js @@ -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, +}; \ No newline at end of file diff --git a/api/modules/token.js b/api/modules/token.js new file mode 100644 index 0000000..57ef111 --- /dev/null +++ b/api/modules/token.js @@ -0,0 +1,52 @@ +/* eslint-disable no-undef */ +import jwt from 'jsonwebtoken'; +import { Level } from 'level'; +import { respondWithStatus } from './requestHandler'; +import { pool } from './database'; + + +// Set up LevelDB instance +const db = new Level('./tokensDB'); + +// Generate a new JWT +const generateToken = async (userId, password) => { + const token = jwt.sign({ userId: userId, password: password }, process.env.JWT_SECRET, { expiresIn: '7d' }); + await db.put(token); + return token; +}; + +// 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 < 36000) { + const newToken = generateToken(req.userId, decoded.password); + res.cookie('token', newToken, { + expires: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), + httpOnly: true, + secure: true, + sameSite: 'strict', + }); + res.set('Authorization', newToken); + } + next(); + } + catch (error) { + return await respondWithStatus(res, 401, 'Invalid user'); + } +}; + +export { generateToken, verifyToken }; \ No newline at end of file diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..cd113a9 --- /dev/null +++ b/api/package.json @@ -0,0 +1,20 @@ +{ + "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", + "jsonwebtoken": "^9.0.2", + "level": "^8.0.0", + "morgan": "^1.10.0", + "mysql2": "^3.6.5" + } +} \ No newline at end of file diff --git a/api/routes/leaderboard.js b/api/routes/leaderboard.js new file mode 100644 index 0000000..7cb8c43 --- /dev/null +++ b/api/routes/leaderboard.js @@ -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; \ No newline at end of file diff --git a/api/routes/test.js b/api/routes/test.js new file mode 100644 index 0000000..567566d --- /dev/null +++ b/api/routes/test.js @@ -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' }); + +}); + +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; \ No newline at end of file diff --git a/api/routes/users.js b/api/routes/users.js new file mode 100644 index 0000000..adc23b6 --- /dev/null +++ b/api/routes/users.js @@ -0,0 +1,79 @@ +/* eslint-disable no-undef */ +import express from 'express'; +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'); + } + return await respondWithStatus(res, 200, 'Successfully registered'); + } + 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); + res.cookie('token', token, { + expires: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), + httpOnly: true, + secure: true, + sameSite: 'strict', + }); + return await respondWithStatusJSON(res, 200, { + message: 'Login successful', + token: token, + user: { + id: user.id, + username: user.username, + email: user.email, + name: user.name, + }, + }); + } + catch (error) { + console.error(error); + return await respondWithStatus(res, 500, 'An error has occured'); + } + } + else { + return await respondWithStatus(res, 400, 'Missing fields'); + } +}); + +export default router; \ No newline at end of file