Source: app.js

/**
 * @module Backend
 */

const express 		= require('express');
const axios 		= require('axios');
const { v4:uuid } 	= require('uuid');
const mysql		= require('mysql2/promise');
const crypto		= require('crypto');
const session		= require('express-session');
const parser		= require('body-parser');
const swig		= require('swig');
const db		= require('./db');

const app 		= express();
const port 		= process.env.PORT || 3000;
const engine		= new swig.Swig();

const usernameRegex = new RegExp("^[a-zA-Z0-9_-]{1,45}$");


// array of objects that contain info on the current ongoing games
var currentGames = {};

app.use(session({
	secret: 'secret',
	resave: true,
	saveUninitialized: true,
}));
app.use(parser.urlencoded({extended: true}));
app.use(parser.json());
app.engine('html', swig.renderFile)
app.set('view engine', 'html');
app.set('views', __dirname + '/views');

app.listen(port, () => { console.log("Listening..."); });

/**
 * Index endpoint.
 *
 * @function
 * @name get/
 */

app.get('/', (request, response) => {
	response.render("index", {user: request.session.username});
});

/**
 * Logout endpoint.
 *
 * @function
 * @name get/logout
 */

app.get('/logout', (request, response) => {
	request.session.username = null;
	request.session.loggedin = false;
	response.redirect("/");
});

/**
 * Submission endpoint.
 *
 * @function
 * @name post/attempt
 * @param {string} id - ID of user's game
 * @param {number[]} input - Numbers guessed
 * @param {number} count - (Config) Number of numbers to generate/were generated.
 * @param {number} minValue - (Config) Minimum value of generated numbers.
 * @param {number} maxValue - (Config) Maximum value of generated numbers.
 */

app.get('/attempt', async function(request, response) {
	var id = request.query.id;
	var input = request.query.input;
	var ret = await attempt(id, input, {'count': request.query.count, 'minValue': request.query.minValue, 'maxValue': request.query.maxValue});
	if (ret == null) {
		response.send({'status': 'KO', 'attemptedNumber': input});
		console.log("GAME OVER FOR GAME #"+id);
	}
	else {
		if (ret.matches == request.query.count) {
			console.log(request.session.username + " won!");
			if (db.getUser(request.session.username) != null) {
				db.saveGame(request.session.username, ret.score);
			}
		}
		console.log(ret);
		response.send(ret);
	}
});

/**
 * Leaderboard endpoint.
 *
 * @function
 * @name get/leaderboard
 */

app.get('/leaderboard', async function(request, response) {
	var ret = await db.getLeaderboards();
	response.send(ret);
});

/**
 * Profile endpoint.
 *
 * @function
 * @name get/profile
 */

app.get('/profile', async function(request, response) {
	console.log(request.session.username);
	if (request.session.username == null || request.session.username == "") {
		console.log("problem");
		response.redirect("/");
	}
	else {
		var his = await db.getHistory(request.session.username);
		console.log(his);
		response.render("profile", {user: request.session.username, history: his});
	}
});

/**
 * Auth endpoint.
 *
 * @function
 * @name post/auth
 * @param {string} username - Name of user to be logged in.
 * @param {string} password - Password of user to be logged in.
 */

app.post('/auth', async function(request, response) {
	var username = request.body.username;
	var plainpass = request.body.password;

	if (usernameRegex.test(username) == false) {
		response.send({'status': 'MALFORMED_LOGIN'});
		return ;
	}
	// encrypt paintext
	var password = crypto.createHash("sha256").update(plainpass).digest("hex");

	// if the user doesn't exist, create it
	var user = await db.getUser(username);
	if (JSON.stringify(user) == JSON.stringify({ })) {
		await db.register(username, password, function() { return ; });
		user = db.getUser(username);
	}
	var loginRes = await db.attemptLogin(username, password);
	if (loginRes == true) {
		// successful login, set session variables
		request.session.loggedin = true;
		request.session.username = username;
		response.redirect('/');
	}
	else {
		response.send({'status': 'INVALID_LOGIN'});
	}
});

app.use(express.static('public'));

/**
 * Function to either return an existing game or create a new one based on given id and config.
 *
 * @function
 * @name validateOrCreateGame
 * @param {number} id - id of game being attempted
 * @param {object} config - json object of game generation config
 */

async function validateOrCreateGame(id, config) {
	if (id == "-1" || currentGames.hasOwnProperty(id) == false || JSON.stringify(config) != JSON.stringify(currentGames[id]['config'])) {
		await axios({'method':'GET','url':'https://www.random.org/integers/?num=' + config.count * 2 + '&format=plain&min=' + config.minValue + '&max=' + config.maxValue + '&col=1&base=10&rnd=new'})
		.then(await async function (response) {
			var data = response.data.split('\n');
			var nums = [];
			var nextId = uuid().toString();

			//iterate through numbers received from api
			for (elem in data) {
				if (isNaN(parseInt(data[elem])) != true)
					nums.push(parseInt(data[elem]));
			}
			var finalNums = [];

			//pick half to play with
			for (var x = 0; x != config.count; x++) {
				var idx = Math.floor(Math.random() * nums.length);
				finalNums.push(nums[idx]);
				delete nums[idx];
			}

			// create game session
			currentGames[nextId] = {
				'id': nextId,
				'nums': finalNums,
				'tries': 10,
				'config': JSON.parse(JSON.stringify(config)),
				'score': 0,
			}

			// set the id to a valid one, so we can process the input
			id = nextId;

			console.log("[Mastermind] Creating new game of id " + id + " with nums " + finalNums);
		});
	}
	console.log(id);
	console.log(currentGames[id].id);
	return currentGames[id];
}

/**
 * Function to submit the chosen numbers.
 *
 * @function
 * @name attempt
 * @param {number} id - id of game being attempted
 * @param {number[]} input - numbers to be submitted
 * @param {object} config - json object of game generation config
 */

async function attempt(id, input, config) {
	console.log("[Mastermind] Processing attempt for game " + id);
	var game = await validateOrCreateGame(id, config);
	if (input === undefined || input.length > game['config']['count'] || input.length <= 0) {
		console.log("[Mastermind] ERROR: Invalid input supplied");
		return {'status': '?', 'id': id}
	}
	else {
		// compare numbers to correct answers
		var matches = 0;
		for (elem in input) {
			if (input[elem] == game['nums'][elem])
				matches++;
		}
		game['tries'] -= 1;
		game['score'] += matches * (game['config']['count'] + (game['config']['maxValue'] - game['config']['minValue']));

		var retObj = {
			'id': game['id'],
			'matches': matches,
			'tries': game['tries'],
			'status': 'OK',
			'attemptedNumber': input,
			'score': game['score'],
		};

		// if we're out of tries, or we have guessed correctly, we are done with this game
		if (matches == game['config']['count'] || game['tries'] <= 0)
			delete currentGames[id];
		return game['tries'] > 0 || game['matches'] == game['config']['count'] ? retObj : null;
	}
}

module.exports = {
	app,
	attempt
}