Massive refactor complete

This commit is contained in:
2019-05-31 13:44:01 +10:00
parent b97d8fc184
commit 4a2bfb3db9
47 changed files with 1223 additions and 991 deletions
+71 -52
View File
@@ -4,7 +4,7 @@ require('dotenv').config();
//libraries
let bcrypt = require('bcrypt');
let formidable = require('formidable');
let sendmail = require('sendmail')();
let sendmail = require('sendmail')({silent: true});
//utilities
let { log, validateEmail } = require('../common/utilities.js');
@@ -20,7 +20,7 @@ const signupRequest = (connection) => (req, res) => {
//prevent too many clicks
if (isThrottled(fields.email)) {
res.status(400).write(log('signup throttled', fields.email));
res.status(400).write(log('Signup throttled', fields.email));
res.end();
return;
}
@@ -65,11 +65,15 @@ const signupRequest = (connection) => (req, res) => {
connection.query(query, [fields.email, fields.username, salt, hash, rand], (err) => {
if (err) throw err;
//TODO: make the verification email prettier
//build the verification email
let addr = `http://${process.env.WEB_ADDRESS}/verifyrequest?email=${fields.email}&verify=${rand}`;
let msg = 'Hello! Please visit the following address to verify your account: ';
let msgHtml = `<html><body><p>${msg}<a href='${addr}'>${addr}</a></p></body></html>`;
//BUGFIX: is gmail being cruel?
let sentinel = false;
//send the verification email
sendmail({
from: `signup@${process.env.WEB_ADDRESS}`,
@@ -78,32 +82,39 @@ const signupRequest = (connection) => (req, res) => {
text: msg + addr,
html: msgHtml
}, (err, reply) => {
//final check
if (err) {
res.status(400).write(log('Something went wrong (did you use a valid email?)', err));
res.end();
return;
}
if (err) { //final check
let msg = log('Something went wrong (did you use a valid email?)', err);
res.status(200).write(log('Verification email sent!', fields.email));
res.end();
if (!sentinel) {
res.status(400).write(msg);
res.end();
}
} else {
let msg = log('Verification email sent!', fields.email);
if (!sentinel) {
res.status(200).json({ msg: msg });
res.end();
}
}
sentinel = true;
});
});
});
});
});
});
}
};
const verifyRequest = (connection) => (req, res) => {
//get the saved data
let query = 'SELECT email, username, salt, hash, verify FROM signups WHERE email = ?;';
let query = 'SELECT * FROM signups WHERE email = ?;';
connection.query(query, [req.query.email], (err, results) => {
if (err) throw err;
//correct number of results
if (results.length != 1) {
if (results.length !== 1) {
console.log(req.query.email);
res.status(400).write(log('That account does not exist or this link has already been used.', req.query.email, req.query.verify));
res.end();
return;
@@ -126,12 +137,13 @@ const verifyRequest = (connection) => (req, res) => {
connection.query(query, [results[0].email], (err) => {
if (err) throw err;
//TODO: prettier verification page
res.status(200).write(log('Verification succeeded!', req.query.email));
res.end();
});
});
});
}
};
const loginRequest = (connection) => (req, res) => {
//formidable handles forms
@@ -155,7 +167,7 @@ const loginRequest = (connection) => (req, res) => {
//found this email?
if (results.length === 0) {
res.status(400).write(log('Incorrect email or password', fields.email, 'Did not find this email'));
res.status(400).write(log('Incorrect email or password', fields.email, 'Did not find this email')); //NOTE: deliberately obscure incorrect email or password
res.end();
return;
}
@@ -183,25 +195,25 @@ const loginRequest = (connection) => (req, res) => {
id: results[0].id,
email: fields.email,
username: results[0].username,
token: rand
token: rand,
msg: log('Logged in', fields.email, rand)
});
res.end();
log('Logged in', fields.email, rand);
});
});
});
});
}
};
const logoutRequest = (connection) => (req, res) => {
let query = 'DELETE FROM sessions WHERE sessions.accountId IN (SELECT accounts.id FROM accounts WHERE email = ?) AND token = ?;';
connection.query(query, [req.body.email, req.body.token], (err) => {
let query = 'DELETE FROM sessions WHERE sessions.accountId = ? AND token = ?;'; //NOTE: The user now loses this access token
connection.query(query, [req.body.id, req.body.token], (err) => {
if (err) throw err;
log('Logged out', req.body.email, req.body.token);
log('Logged out', req.body.id, req.body.token);
});
res.end();
}
res.end(); //NOTE: don't send a response
};
const passwordChangeRequest = (connection) => (req, res) => {
//formidable handles forms
@@ -212,23 +224,19 @@ const passwordChangeRequest = (connection) => (req, res) => {
if (err) throw err;
//validate password, retype
if (!validateEmail(fields.email) || fields.password.length < 8 || fields.password !== fields.retype) {
res.status(400).write(log('Invalid password change data', fields.email));
if (fields.password.length < 8 || fields.password !== fields.retype) {
res.status(400).write(log('Invalid password change data', fields.id));
res.end();
return;
}
//validate token
query = 'SELECT sessions.token FROM sessions WHERE sessions.accountId IN (SELECT id FROM accounts WHERE email = ?);';
connection.query(query, [fields.email], (err, results) => {
query = 'SELECT COUNT(*) AS total FROM sessions WHERE sessions.accountId = ? AND sessions.token = ?;';
connection.query(query, [fields.id, fields.token], (err, results) => {
if (err) throw err;
let found = false;
results.map((result) => { if (result.token == fields.token) found = true; });
if (!found) {
res.status(400).write(log('Invalid password change authentication', fields.email, fields.token));
if (results[0].total !== 1) {
res.status(400).write(log('Invalid password change credentials', fields.id, fields.token));
res.end();
return;
}
@@ -239,26 +247,26 @@ const passwordChangeRequest = (connection) => (req, res) => {
bcrypt.hash(fields.password, salt, (err, hash) => {
if (err) throw err;
let query = 'UPDATE accounts SET salt = ?, hash = ? WHERE email = ?;';
connection.query(query, [salt, hash, fields.email], (err) => {
let query = 'UPDATE accounts SET salt = ?, hash = ? WHERE id = ?;';
connection.query(query, [salt, hash, fields.id], (err) => {
if (err) throw err;
//clear all session data for this user (a 'feature')
let query = 'DELETE FROM sessions WHERE sessions.accountId IN (SELECT accounts.id FROM accounts WHERE email = ?);';
connection.query(query, [fields.email], (err) => {
let query = 'DELETE FROM sessions WHERE sessions.accountId = ?;';
connection.query(query, [fields.id], (err) => {
if (err) throw err;
//create the new session
let rand = Math.floor(Math.random() * 100000);
let query = 'INSERT INTO sessions (accountId, token) VALUES ((SELECT accounts.id FROM accounts WHERE email = ?), ?);';
connection.query(query, [fields.email, rand], (err) => {
let query = 'INSERT INTO sessions (accountId, token) VALUES (?, ?);';
connection.query(query, [fields.id, rand], (err) => {
if (err) throw err;
//send json containing the account info
res.status(200).json({
token: rand,
msg: log('Password changed!', fields.email)
msg: log('Password changed!', fields.id)
});
res.end();
});
@@ -268,7 +276,7 @@ const passwordChangeRequest = (connection) => (req, res) => {
});
});
});
}
};
const passwordRecoverRequest = (connection) => (req, res) => {
//formidable handles forms
@@ -280,14 +288,14 @@ const passwordRecoverRequest = (connection) => (req, res) => {
//prevent too many clicks
if (isThrottled(fields.email)) {
res.status(400).write(log('recover throttled', fields.email));
res.status(400).write(log('Recover throttled', fields.email));
res.end();
return;
}
throttle(fields.email);
//validate email, username and password
//validate email
if (!validateEmail(fields.email)) {
res.status(400).write(log('Invalid recover data', fields.email));
res.end();
@@ -312,11 +320,15 @@ const passwordRecoverRequest = (connection) => (req, res) => {
connection.query(query, [results[0].id, rand], (err) => {
if (err) throw err;
//TODO: prettier recovery email
//build the recovery email
let addr = `http://${process.env.WEB_ADDRESS}/passwordreset?email=${fields.email}&token=${rand}`;
let msg = 'Hello! Please visit the following address to set a new password (if you didn\'t request a password recovery, ignore this email): ';
let msgHtml = `<html><body><p>${msg}<a href='${addr}'>${addr}</a></p></body></html>`;
//BUGFIX: is gmail being cruel?
let sentinel = false;
//send the verification email
sendmail({
from: `passwordrecover@${process.env.WEB_ADDRESS}`,
@@ -327,18 +339,25 @@ const passwordRecoverRequest = (connection) => (req, res) => {
}, (err, reply) => {
//final check
if (err) {
res.status(400).write(log('Something went wrong (did you use a valid email?)', err));
if (!sentinel) {
let msg = log('Something went wrong (did you use a valid email?)', err);
res.status(400).write(msg);
res.end();
}
} else {
let msg = log('Recovery email sent!', fields.email);
res.status(200).json({ msg: msg });
res.end();
return;
}
res.status(200).write(log('Recovery email sent!', fields.email));
res.end();
sentinel = true;
});
});
});
});
}
};
const passwordResetRequest = (connection) => (req, res) => {
//formidable handles forms
@@ -348,7 +367,7 @@ const passwordResetRequest = (connection) => (req, res) => {
form.parse(req, (err, fields) => {
if (err) throw err;
//validate email, username and password
//validate email and password
if (!validateEmail(fields.email) || fields.password.length < 8 || fields.password !== fields.retype) {
res.status(400).write(log('Invalid reset data (invalid email/password)', fields.email));
res.end();
@@ -383,7 +402,7 @@ const passwordResetRequest = (connection) => (req, res) => {
connection.query(query, [fields.email], (err) => {
if (err) throw err;
res.status(200).write(log('Password updated!', fields.email));
res.status(200).json({ msg: log('Password updated!', fields.email) });
res.end();
return;
});
@@ -392,7 +411,7 @@ const passwordResetRequest = (connection) => (req, res) => {
});
});
});
}
};
module.exports = {
signupRequest: signupRequest,
+36 -39
View File
@@ -8,35 +8,33 @@ let CronJob = require('cron').CronJob;
let { log } = require('../common/utilities.js');
const attackRequest = (connection) => (req, res) => {
//verify the attacker's credentials
let query = 'SELECT accountId FROM sessions WHERE accountId IN (SELECT id FROM accounts WHERE username = ?) AND token = ?;';
connection.query(query, [req.body.attacker, req.body.token], (err, results) => {
//verify the attacker's credentials (only the attacker can launch an attack)
let query = 'SELECT COUNT(*) AS total FROM sessions WHERE accountId = ? AND accountId IN (SELECT id FROM accounts WHERE username = ?) AND token = ?;';
connection.query(query, [req.body.id, req.body.attacker, req.body.token], (err, results) => {
if (err) throw err;
if (results.length !== 1) {
res.status(400).write(log('Invalid attack credentials', req.body.attacker, req.body.defender, req.body.token));
if (results[0].total !== 1) {
res.status(400).write(log('Invalid attack credentials', req.body.id, req.body.attacker, req.body.defender, req.body.token));
res.end();
return;
}
let attackerId = results[0].accountId;
//verify that the defender exists
let query = 'SELECT id FROM accounts WHERE username = ?;';
//verify that the defender's profile exists
let query = 'SELECT accountId FROM profiles WHERE accountId IN (SELECT id FROM accounts WHERE username = ?);';
connection.query(query, [req.body.defender], (err, results) => {
if (err) throw err;
if (results.length !== 1) {
res.status(400).write(log('Invalid defender credentials', req.body.attacker, req.body.defender));
res.status(400).write(log('Invalid defender credentials', req.body.id, req.body.attacker, req.body.defender, req.body.token));
res.end();
return;
}
let defenderId = results[0].id;
let defenderId = results[0].accountId;
//verify that the attacker has enough soldiers
let query = 'SELECT soldiers FROM profiles WHERE accountId = ?;';
connection.query(query, [attackerId], (err, results) => {
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
if (results[0].soldiers <= 0) {
@@ -52,54 +50,57 @@ const attackRequest = (connection) => (req, res) => {
if (err) throw err;
if (attacking) {
res.status(400).write(log('You are already attacking someone', req.body.attacker, req.body.defender));
res.status(400).write(log('You are already attacking someone', req.body.id, req.body.attacker, req.body.token));
res.end();
return;
}
//create the pending attack value
//create the pending attack record
let query = 'INSERT INTO pendingCombat (eventTime, attackerId, defenderId, attackingUnits) VALUES (DATE_ADD(CURRENT_TIMESTAMP(), INTERVAL 60 * ? SECOND), ?, ?, ?);';
connection.query(query, [attackingUnits, attackerId, defenderId, attackingUnits], (err) => {
connection.query(query, [attackingUnits, req.body.id, defenderId, attackingUnits], (err) => {
if (err) throw err;
res.status(200).json({
status: 'attacking',
defender: req.body.defender
attacker: req.body.attacker,
defender: req.body.defender,
msg: log('Attacking', req.body.attacker, req.body.defender)
});
res.end();
log(`attacking ${req.body.defender}`, req.body.attacker, req.body.defender)
});
});
});
});
});
}
};
const attackStatusRequest = (connection) => (req, res) => {
isAttacking(connection, req.body.username, (err, attacking, defender) => {
const attackStatusRequest = (connection) => (req, res) => { //TODO: proper credentials
isAttacking(connection, req.body.attacker, (err, attacking, defender) => {
if (err) throw err;
res.status(200).json({
status: log(attacking ? 'attacking' : 'idle', req.body.username, defender),
defender: defender
status: attacking ? 'attacking' : 'idle',
attacker: req.body.attacker,
defender: defender,
msg: null
});
res.end();
});
}
};
const combatLogRequest = (connection) => (req, res) => {
let query = 'SELECT pastCombat.*, atk.username AS attackerUsername, def.username AS defenderUsername FROM pastCombat JOIN accounts AS atk ON pastCombat.attackerId = atk.id JOIN accounts AS def ON pastCombat.defenderId = def.id WHERE atk.username = ? OR def.username = ? ORDER BY eventTime DESC LIMIT ?, ?;';
let query = 'SELECT pastCombat.*, atk.username AS attacker, def.username AS defender FROM pastCombat JOIN accounts AS atk ON pastCombat.attackerId = atk.id JOIN accounts AS def ON pastCombat.defenderId = def.id WHERE atk.username = ? OR def.username = ? ORDER BY eventTime DESC LIMIT ?, ?;';
connection.query(query, [req.body.username, req.body.username, req.body.start, req.body.length], (err, results) => {
if (err) throw err;
res.status(200).json(results);
log('Combat log sent', req.body.username, req.body.start, req.body.length, JSON.stringify(results));
log('Combat log sent', req.body.username, req.body.start, req.body.length);
});
}
};
const runCombatTick = (connection) => {
//once per second
let combatTick = new CronJob('* * * * * *', () => {
//find each pending combat
let query = 'SELECT * FROM pendingCombat WHERE eventTime < CURRENT_TIMESTAMP();';
@@ -140,6 +141,7 @@ const runCombatTick = (connection) => {
}
//determine the victor
//TODO: add equipment effectiveness
let rand = Math.random() * (pendingCombat.attackingUnits + defendingUnits * (undefended ? 0.25 : 1));
let victor = rand <= pendingCombat.attackingUnits ? 'attacker' : 'defender';
@@ -147,23 +149,18 @@ const runCombatTick = (connection) => {
let spoilsGold = Math.floor(results[0].gold * (victor === 'attacker' ? 0.1 : 0.02));
let casualtiesVictor = Math.floor((pendingCombat.attackingUnits >= 10 ? pendingCombat.attackingUnits - 10 : 0) * (victor === 'attacker' ? 0.05 : 0.1));
//NOTE: there is a negative gold bug somewhere
if (spoilsGold <= 0) {
log('WARNING: spoilsGold <= 0', pendingCombat.attackerId, pendingCombat.defenderId, spoilsGold);
}
//save the combat
let query = 'INSERT INTO pastCombat (eventTime, attackerId, defenderId, attackingUnits, defendingUnits, undefended, victor, spoilsGold, casualtiesVictor) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);';
connection.query(query, [pendingCombat.eventTime, pendingCombat.attackerId, pendingCombat.defenderId, pendingCombat.attackingUnits, defendingUnits, undefended, victor, spoilsGold, casualtiesVictor], (err) => {
if (err) throw err;
//update the attacker profile
let query = 'UPDATE profiles SET gold = gold + ?, soldiers = soldiers - ? WHERE id = ?;';
let query = 'UPDATE profiles SET gold = gold + ?, soldiers = soldiers - ? WHERE accountId = ?;';
connection.query(query, [spoilsGold, casualtiesVictor, pendingCombat.attackerId], (err) => {
if (err) throw err;
//update the defender profile
let query = 'UPDATE profiles SET gold = gold - ? WHERE id = ?;';
let query = 'UPDATE profiles SET gold = gold - ? WHERE accountId = ?;';
connection.query(query, [spoilsGold, pendingCombat.defenderId], (err) => {
if (err) throw err;
@@ -185,12 +182,12 @@ const runCombatTick = (connection) => {
});
combatTick.start();
}
};
const isNormalInteger = (str) => {
let n = Math.floor(Number(str));
return n !== Infinity && String(n) == str && n >= 0;
}
};
const isAttacking = (connection, user, cb) => {
let query;
@@ -200,7 +197,7 @@ const isAttacking = (connection, user, cb) => {
} else if (typeof(user) === 'string') {
query = 'SELECT * FROM pendingCombat WHERE attackerId IN (SELECT id FROM accounts WHERE username = ?);';
} else {
return cb('Unknown argument type for user');
return cb(`Unknown argument type for user: ${typeof(user)}`);
}
connection.query(query, [user], (err, results) => {
@@ -217,7 +214,7 @@ const isAttacking = (connection, user, cb) => {
});
}
});
}
};
module.exports = {
attackRequest: attackRequest,
@@ -225,4 +222,4 @@ module.exports = {
combatLogRequest: combatLogRequest,
runCombatTick: runCombatTick,
isAttacking: isAttacking
}
};
+2 -2
View File
@@ -42,8 +42,8 @@ function handleDisconnect() {
//finally
return connection;
}
};
module.exports = {
connectToDatabase: handleDisconnect
}
};
+4 -2
View File
@@ -10,7 +10,9 @@ let equipmentStatistics = require('./equipment_statistics.json');
const statisticsRequest = () => (req, res) => {
res.status(200).json(equipmentStatistics);
res.end();
}
};
//TODO: incomplete
const listRequest = (connection) => (req, res) => {
//verify identity
@@ -37,7 +39,7 @@ const listRequest = (connection) => (req, res) => {
res.end();
});
});
}
};
module.exports = {
statisticsRequest: statisticsRequest,
+9 -9
View File
@@ -1,15 +1,15 @@
{
"Weapons": {
"Stick": { "cost": 50, "combatBoost": 0.02, "scientists": 1 },
"Dagger": { "cost": 75, "combatBoost": 0.03, "scientists": 2 },
"Sword": { "cost": 100, "combatBoost": 0.04, "scientists": 3 },
"Longsword": { "cost": 150, "combatBoost": 0.05, "scientists": 4 },
"Frying Pan": { "cost": 200, "combatBoost": 0.06, "scientists": 5 }
"Stick": { "cost": 50, "combatBoost": 0.02, "scientistsRequired": 1 },
"Dagger": { "cost": 75, "combatBoost": 0.03, "scientistsRequired": 2 },
"Sword": { "cost": 100, "combatBoost": 0.04, "scientistsRequired": 3 },
"Longsword": { "cost": 150, "combatBoost": 0.05, "scientistsRequired": 4 },
"Frying Pan": { "cost": 200, "combatBoost": 0.06, "scientistsRequired": 5 }
},
"Armour": {
"Leather": { "cost": 75, "combatBoost": 0.02, "scientists": 2 },
"Gambeson": { "cost": 100, "combatBoost": 0.03, "scientists": 3 },
"Chainmail": { "cost": 150, "combatBoost": 0.04, "scientists": 4 },
"Platemail": { "cost": 200, "combatBoost": 0.05, "scientists": 5 }
"Leather": { "cost": 75, "combatBoost": 0.02, "scientistsRequired": 2 },
"Gambeson": { "cost": 100, "combatBoost": 0.03, "scientistsRequired": 3 },
"Chainmail": { "cost": 150, "combatBoost": 0.04, "scientistsRequired": 4 },
"Platemail": { "cost": 200, "combatBoost": 0.05, "scientistsRequired": 5 }
}
}
+4 -3
View File
@@ -15,6 +15,7 @@ app.use(bodyParser.json());
//handle the news request
let news = require('./news.js');
app.get('/newsrequest', news.newsRequest());
app.post('/newsrequest', news.newsRequest());
//database
@@ -46,9 +47,9 @@ app.post('/attackstatusrequest', combat.attackStatusRequest(connection));
app.post('/combatlogrequest', combat.combatLogRequest(connection));
combat.runCombatTick(connection);
let equipment = require('./equipment.js');
app.post('/equipmentstatisticsrequest', equipment.statisticsRequest());
app.post('/equipmentlistrequest', equipment.listRequest(connection));
//let equipment = require('./equipment.js');
//app.post('/equipmentstatisticsrequest', equipment.statisticsRequest());
//app.post('/equipmentlistrequest', equipment.listRequest(connection));
//static directories
app.use('/styles', express.static(path.resolve(__dirname + '/../public/styles')) );
+9 -5
View File
@@ -5,18 +5,20 @@ require('dotenv').config();
let fs = require('fs');
let path = require('path');
let { log } = require('../common/utilities.js');
const newsRequest = () => (req, res) => {
let fpath = path.join(__dirname, '..', 'public', 'news');
let fileNames = fs.readdirSync(fpath);
//set the maximum
let max = parseInt(req.body.max);
if (max > fileNames.length) {
let max = parseInt(req.body.length) || 99;
if (isNaN(max) || max > fileNames.length) {
max = fileNames.length;
}
//build the object to send
let json = {} //TODO: caching
let json = {}; //TODO: caching
//send each file as json
for (let i = 0; i < max; i++) {
@@ -26,8 +28,10 @@ const newsRequest = () => (req, res) => {
//actually send the data
res.json(json);
res.end();
}
log('News sent', max, fileNames, JSON.stringify(json));
};
module.exports = {
newsRequest: newsRequest
}
};
+230 -267
View File
@@ -2,7 +2,6 @@
require('dotenv').config();
//libraries
let formidable = require('formidable');
let CronJob = require('cron').CronJob;
let { isAttacking } = require('./combat.js');
@@ -12,87 +11,73 @@ let { log } = require('../common/utilities.js');
//profile creation & requesting
const profileCreateRequest = (connection) => (req, res) => {
//formidable handles forms
let form = formidable.IncomingForm();
//separate this section so it can be used elsewhere too
return profileCreateRequestInner(connection, req, res, req.body);
};
//parse form TODO: form? That was a bad idea
form.parse(req, (err, fields) => {
if (err) throw err;
//separate this section so it can be used elsewhere too
return profileCreateRequestInner(connection, req, res, fields);
});
}
function profileCreateRequestInner(connection, req, res, fields) {
function profileCreateRequestInner(connection, req, res, body) {
let query = 'SELECT accountId FROM profiles WHERE accountId IN (SELECT accounts.id FROM accounts WHERE username = ?);';
connection.query(query, [fields.username], (err, results) => {
connection.query(query, [body.username], (err, results) => {
if (err) throw err;
if (results.length === 1) {
res.status(400).write(log('That profile already exists', fields.username));
res.status(400).write(log('That profile already exists', body.username));
res.end();
return;
}
//check ID, username and token match
//check ID, username and token match (only the profile's owner can create it)
let query = 'SELECT accountId FROM sessions WHERE accountId IN (SELECT id FROM accounts WHERE username = ?) AND token = ?;';
connection.query(query, [fields.username, fields.token], (err, results) => {
connection.query(query, [body.username, body.token], (err, results) => {
if (err) throw err;
if (results.length !== 1 || results[0].accountId != fields.id) {
res.status(400).write(log('Invalid profile creation credentials', fields.username, fields.id, fields.token));
if (results.length !== 1 || results[0].accountId != body.id) {
res.status(400).write(log('Invalid profile creation credentials', body.username, body.id, body.token));
res.end();
return;
}
//create the profile
let query = 'INSERT INTO profiles (accountId) SELECT accounts.id FROM accounts WHERE username = ?;';
connection.query(query, [fields.username], (err) => {
connection.query(query, [body.username], (err) => {
if (err) throw err;
log('Profile created', fields.username, fields.id, fields.token);
log('Profile created', body.username, body.id, body.token);
return profileRequestInner(connection, req, res, fields);
return profileRequestInner(connection, req, res, body);
});
});
});
}
const profileRequest = (connection) => (req, res) => {
//formidable handles forms
let form = formidable.IncomingForm();
//parse form
form.parse(req, (err, fields) => {
if (err) throw err;
//separate this section so it can be used elsewhere too
return profileRequestInner(connection, req, res, fields);
});
};
function profileRequestInner(connection, req, res, fields) {
const profileRequest = (connection) => (req, res) => {
//separate this section so it can be used elsewhere too
return profileRequestInner(connection, req, res, req.body);
};
function profileRequestInner(connection, req, res, body) {
//find the profile
let query = 'SELECT * FROM profiles WHERE accountId IN (SELECT accounts.id FROM accounts WHERE username = ?);';
connection.query(query, [fields.username], (err, results) => {
connection.query(query, [body.username], (err, results) => {
if (err) throw err;
if (results.length !== 1) {
//pass it off to the profile creation process, IF the user is requesting their own profile
let query = 'SELECT id FROM accounts WHERE id = ? AND id IN (SELECT accountId FROM sessions WHERE token = ?);';
connection.query(query, [fields.id, fields.token], (err, results) => {
connection.query(query, [body.id, body.token], (err, results) => {
if (err) throw err;
if (results.length === 1) {
return profileCreateRequestInner(connection, req, res, fields);
return profileCreateRequestInner(connection, req, res, body);
} else {
res.status(400).write(log('Profile not found', fields.username, fields.id, fields.token));
res.status(400).write(log('Profile not found', body.username, body.id, body.token));
res.end();
}
});
} else {
//results.length === 1
res.status(200).json({
username: fields.username,
username: body.username,
gold: results[0].gold,
recruits: results[0].recruits,
soldiers: results[0].soldiers,
@@ -100,71 +85,161 @@ function profileRequestInner(connection, req, res, fields) {
scientists: results[0].scientists
});
res.end();
log('Profile sent', fields.username, fields.id, fields.token);
log('Profile sent', body.username, body.id, body.token);
}
});
}
};
//actual actions to be taken
const recruitRequest = (connection) => (req, res) => {
//formidable handles forms
let form = formidable.IncomingForm();
//parse form
form.parse(req, (err, fields) => {
//verify the credentials
let query = 'SELECT COUNT(*) AS total FROM sessions WHERE accountId = ? AND token = ?;';
connection.query(query, [req.body.id, req.body.token], (err, results) => {
if (err) throw err;
//verify the credentials
let query = 'SELECT accountId FROM sessions WHERE accountId = ? AND token = ?;';
connection.query(query, [fields.id, fields.token], (err, results) => {
if (results[0].total !== 1) {
res.status(400).write(log('Invalid recruit credentials - 1', req.body.id, req.body.token));
res.end();
return;
}
//verify enough time has passed since the last successful recruit action
let query = 'SELECT TIMESTAMPDIFF(HOUR, (SELECT lastRecruitTime FROM profiles WHERE accountId = ?), CURRENT_TIMESTAMP());';
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
if (results.length !== 1) {
res.status(400).write(log('Invalid recruit credentials', fields.username, fields.id, fields.token));
res.status(400).write(log('Invalid database state', req.body.id, req.body.token));
res.end();
return;
}
//verify enough time has passed since the last successful recruit action
let query = 'SELECT TIMESTAMPDIFF(HOUR, (SELECT lastRecruitTime FROM profiles WHERE accountId = ?), CURRENT_TIMESTAMP());';
connection.query(query, [fields.id], (err, results) => {
let timespans = results[0][Object.keys(results[0])];
//not enough time has passed
if (timespans < 20) {
res.status(400).write(log('Not enough time has passed', req.body.id, req.body.token));
res.end();
return;
}
//update the profile with the new data (gaining 1 recruit)
let query = 'UPDATE profiles SET recruits = recruits + 1, lastRecruitTime = CURRENT_TIMESTAMP() WHERE accountId = ?;';
connection.query(query, [req.body.id], (err) => {
if (err) throw err;
if (results.length !== 1) {
res.status(400).write(log('Invalid database state', fields.username, fields.id, fields.token));
//send the new profile data as JSON
let query = 'SELECT username, profiles.* FROM profiles JOIN accounts ON accounts.id = profiles.accountId WHERE accounts.id = ?;';
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
//check just in case
if (results.length !== 1) {
res.status(400).write(log('Invalid recruit credentials - 2', req.body.id, req.body.token));
res.end();
return;
}
//results.length === 1
res.status(200).json({
username: results[0].username,
gold: results[0].gold,
recruits: results[0].recruits,
soldiers: results[0].soldiers,
spies: results[0].spies,
scientists: results[0].scientists
});
res.end();
log('Recruit successful', results[0].username, req.body.id, req.body.token);
});
});
});
});
};
const trainRequest = (connection) => (req, res) => {
//verify the credentials (NOTE: duplication)
let query = 'SELECT COUNT(*) AS total FROM sessions WHERE accountId = ? AND token = ?;';
connection.query(query, [req.body.id, req.body.token], (err, results) => {
if (err) throw err;
if (results[0].total !== 1) {
res.status(400).write(log('Invalid train credentials - 1', req.body.id, req.body.token));
res.end();
return;
}
//verify the role argument
if (req.body.role !== 'soldier' && req.body.role !== 'spy' && req.body.role !== 'scientist') {
res.status(400).write(log('Invalid train parameters', req.body.role, req.body.id, req.body.token));
res.end();
return;
}
//can't train while attacking
isAttacking(connection, req.body.id, (err, attacking) => {
if (err) throw err;
if (attacking) {
res.status(400).write(log('Can\'t train while attacking', req.body.id));
res.end();
return;
}
//determine the cost of the training TODO: make these global for the client too
let cost = 0;
switch(req.body.role) {
case 'soldier':
cost = 100;
break;
case 'spy':
cost = 200;
break;
case 'scientist':
cost = 120;
break;
}
//verify that the user has a high enough gold and recruit balance
let query = 'SELECT recruits, gold FROM profiles WHERE accountId = ?;';
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
if (results[0].recruits <= 0) {
res.status(400).write(log('Not enough recruits', results[0].recruits, req.body.id, req.body.token));
res.end();
return;
}
let timespans = results[0][Object.keys(results[0])];
//not enough time has passed
if (timespans < 22) {
res.status(400).write(log('Not enough time has passed', fields.username, fields.id, fields.token));
if (results[0].gold < cost) {
res.status(400).write(log('Not enough gold', results[0].gold, req.body.id, req.body.token));
res.end();
return;
}
//update the profile with the new data (gaining 1 recruit)
let query = 'UPDATE profiles SET recruits = recruits + 1, lastRecruitTime = CURRENT_TIMESTAMP() WHERE accountId = ?;';
connection.query(query, [fields.id], (err) => {
//update the profile with new values (NOTE: extra protection for network latency)
let query = 'UPDATE profiles SET gold = gold - ?, recruits = recruits - 1, soldiers = soldiers + ?, spies = spies + ?, scientists = scientists + ? WHERE accountId = ? AND gold >= ? AND recruits > 0;';
connection.query(query, [cost, req.body.role === 'soldier' ? 1 : 0, req.body.role === 'spy' ? 1 : 0, req.body.role === 'scientist' ? 1 : 0, req.body.id, cost], (err) => {
if (err) throw err;
//send the new profile data as JSON (NOTE: possible duplication)
let query = 'SELECT * FROM profiles WHERE accountId = ?;';
connection.query(query, [fields.id], (err, results) => {
let query = 'SELECT username, profiles.* FROM profiles JOIN accounts ON accounts.id = profiles.accountId WHERE accounts.id = ?;';
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
//check just in case
if (results.length !== 1) {
res.status(400).write(log('Invalid recruit credentials', fields.username, fields.id, fields.token));
res.status(400).write(log('Invalid recruit credentials - 2', req.body.id, req.body.token));
res.end();
return;
}
//results.length === 1
res.status(200).json({
username: fields.username, //TODO: join here
username: results[0].username,
gold: results[0].gold,
recruits: results[0].recruits,
soldiers: results[0].soldiers,
@@ -172,227 +247,115 @@ const recruitRequest = (connection) => (req, res) => {
scientists: results[0].scientists
});
res.end();
log('Recruit successful', fields.username, fields.id, fields.token);
log('Train executed', results[0].username, req.body.role, req.body.id, req.body.token);
});
});
});
});
});
}
const trainRequest = (connection) => (req, res) => {
//formidable handles forms
let form = formidable.IncomingForm();
//parse form
form.parse(req, (err, fields) => {
if (err) throw err;
//verify the credentials (NOTE: duplication)
let query = 'SELECT accountId FROM sessions WHERE accountId = ? AND token = ?;';
connection.query(query, [fields.id, fields.token], (err, results) => {
if (err) throw err;
if (results.length !== 1) {
res.status(400).write(log('Invalid train credentials', fields.username, fields.id, fields.token));
res.end();
return;
}
//verify the role argument
if (fields.role !== 'soldier' && fields.role !== 'spy' && fields.role !== 'scientist') {
res.status(400).write(log('Invalid train parameters', fields.username, fields.role, fields.id, fields.token));
res.end();
return;
}
//can't train while attacking
isAttacking(connection, fields.id, (err, attacking) => {
if (err) throw err;
if (attacking) {
res.status(400).write(log('Can\'t train while attacking', fields.id));
res.end();
return;
}
//determine the cost of the training TODO: make these global for the client too
let cost = 0;
switch(fields.role) {
case 'soldier':
cost = 100;
break;
case 'spy':
cost = 200;
break;
case 'scientist':
cost = 120;
break;
}
//verify that the user has a high enough gold and recruit balance
let query = 'SELECT recruits, gold FROM profiles WHERE accountId = ?;';
connection.query(query, [fields.id], (err, results) => {
if (err) throw err;
if (results[0].recruits <= 0) {
res.status(400).write(log('Not enough recruits', fields.username, results[0].recruits, fields.id, fields.token));
res.end();
return;
}
if (results[0].gold < cost) {
res.status(400).write(log('Not enough gold', fields.username, results[0].gold, fields.id, fields.token));
res.end();
return;
}
//update the profile with new values
let query = 'UPDATE profiles SET gold = gold - ?, recruits = recruits - 1, soldiers = soldiers + ?, spies = spies + ?, scientists = scientists + ? WHERE accountId = ? AND gold >= ? AND recruits > 0;';
connection.query(query, [cost, fields.role === 'soldier' ? 1 : 0, fields.role === 'spy' ? 1 : 0, fields.role === 'scientist' ? 1 : 0, fields.id, cost], (err) => {
if (err) throw err;
//send the new profile data as JSON (NOTE: possible duplication)
let query = 'SELECT * FROM profiles WHERE accountId = ?;';
connection.query(query, [fields.id], (err, results) => {
if (err) throw err;
//check just in case
if (results.length !== 1) {
res.status(400).write(log('Invalid recruit credentials', fields.username, fields.id, fields.token));
res.end();
return;
}
//results.length === 1
res.status(200).json({
username: fields.username, //TODO: join here
gold: results[0].gold,
recruits: results[0].recruits,
soldiers: results[0].soldiers,
spies: results[0].spies,
scientists: results[0].scientists
});
res.end();
log('Train executed', fields.username, fields.role, fields.id, fields.token);
});
});
});
});
});
});
}
};
const untrainRequest = (connection) => (req, res) => {
//formidable handles forms
let form = formidable.IncomingForm();
//parse form
form.parse(req, (err, fields) => {
//verify the credentials (NOTE: duplication)
let query = 'SELECT accountId FROM sessions WHERE accountId = ? AND token = ?;';
connection.query(query, [req.body.id, req.body.token], (err, results) => {
if (err) throw err;
//verify the credentials (NOTE: duplication)
let query = 'SELECT accountId FROM sessions WHERE accountId = ? AND token = ?;';
connection.query(query, [fields.id, fields.token], (err, results) => {
if (results.length !== 1) {
res.status(400).write(log('Invalid untrain credentials - 1', req.body.role, req.body.id, req.body.token));
res.end();
return;
}
//verify the role argument
if (req.body.role !== 'soldier' && req.body.role !== 'spy' && req.body.role !== 'scientist') {
res.status(400).write(log('Invalid untrain parameters', req.body.role, req.body.id, req.body.token));
res.end();
return;
}
//can't untrain while attacking
isAttacking(connection, req.body.id, (err, attacking) => {
if (err) throw err;
if (results.length !== 1) {
res.status(400).write(log('Invalid untrain credentials', fields.username, fields.role, fields.id, fields.token));
res.end();
return;
}
//verify the role argument
if (fields.role !== 'soldier' && fields.role !== 'spy' && fields.role !== 'scientist') {
res.status(400).write(log('Invalid untrain parameters', fields.username, fields.role, fields.id, fields.token));
res.end();
return;
}
//can't untrain while attacking
isAttacking(connection, fields.id, (err, attacking) => {
if (err) throw err;
if (attacking) {
res.status(400).write(log('Can\'t untrain while attacking', fields.id));
if (attacking) {
res.status(400).write(log('Can\'t untrain while attacking', req.body.id, req.body.token));
res.end();
return;
}
//verify that the user has a high enough balance
let query = 'SELECT soldiers, spies, scientists FROM profiles WHERE accountId = ?;';
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
if (req.body.role === 'soldier' && results[0].soldiers <= 0) {
res.status(400).write(log('Not enough soldiers', results[0].soldiers, req.body.id, req.body.token));
res.end();
return;
}
//verify that the user has a high enough balance
let query = 'SELECT soldiers, spies, scientists FROM profiles WHERE accountId = ?;';
connection.query(query, [fields.id], (err, results) => {
if (req.body.role === 'spy' && results[0].spies <= 0) {
res.status(400).write(log('Not enough spies', results[0].spies, req.body.id, req.body.token));
res.end();
return;
}
if (req.body.role === 'scientist' && results[0].scientists <= 0) {
res.status(400).write(log('Not enough scientists', results[0].scientists, req.body.id, req.body.token));
res.end();
return;
}
//hacky
let roleName = null;
if (req.body.role === 'soldier') {
roleName = 'soldiers';
} else if (req.body.role === 'spy') {
roleName = 'spies';
} else if (req.body.role === 'scientist') {
roleName = 'scientists';
} else {
res.status(400).write(log('Unknown role received', req.body.role, req.body.id, req.body.token));
res.end();
return;
}
//update the profile with new values (NOTE: extra protection for network latency)
let query = `UPDATE profiles SET recruits = recruits + 1, soldiers = soldiers - ?, spies = spies - ?, scientists = scientists - ? WHERE accountId = ? AND ${roleName} > 0;`;
connection.query(query, [roleName === 'soldiers' ? 1 : 0, roleName === 'spies' ? 1 : 0, roleName === 'scientists' ? 1 : 0, req.body.id], (err) => {
if (err) throw err;
if (fields.role === 'soldier' && results[0].soldiers <= 0) {
res.status(400).write(log('Not enough soldiers', fields.username, results[0].soldiers, fields.id, fields.token));
res.end();
return;
}
if (fields.role === 'spy' && results[0].spies <= 0) {
res.status(400).write(log('Not enough spies', fields.username, results[0].spies, fields.id, fields.token));
res.end();
return;
}
if (fields.role === 'scientist' && results[0].scientists <= 0) {
res.status(400).write(log('Not enough scientists', fields.username, results[0].scientists, fields.id, fields.token));
res.end();
return;
}
//hacky
let role = null;
if (fields.role === 'soldier') {
role = 'soldiers';
} else if (fields.role === 'spy') {
role = 'spies';
} else if (fields.role === 'scientist') {
role = 'scientists';
} else {
res.status(400).write(log('Unknown role found', fields.role));
res.end();
return;
}
//update the profile with new values
let query = `UPDATE profiles SET recruits = recruits + 1, soldiers = soldiers - ?, spies = spies - ?, scientists = scientists - ? WHERE accountId = ? AND ${role} > 0;`;
connection.query(query, [fields.role === 'soldier' ? 1 : 0, fields.role === 'spy' ? 1 : 0, fields.role === 'scientist' ? 1 : 0, fields.id], (err) => {
//send the new profile data as JSON (NOTE: possible duplication)
let query = 'SELECT username, profiles.* FROM profiles JOIN accounts ON accounts.id = profiles.accountId WHERE accounts.id = ?;';
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
//send the new profile data as JSON (NOTE: possible duplication)
let query = 'SELECT * FROM profiles WHERE accountId = ?;';
connection.query(query, [fields.id], (err, results) => {
if (err) throw err;
//check just in case
if (results.length !== 1) {
res.status(400).write(log('Invalid untrain credentials', fields.username, fields.role, fields.id, fields.token));
res.end();
return;
}
//results.length === 1
res.status(200).json({
username: fields.username, //TODO: join here
gold: results[0].gold,
recruits: results[0].recruits,
soldiers: results[0].soldiers,
spies: results[0].spies,
scientists: results[0].scientists
});
//check just in case
if (results.length !== 1) {
res.status(400).write(log('Invalid untrain credentials - 2', req.body.role, req.body.id, req.body.token));
res.end();
log('Untrain executed', fields.username, fields.role, fields.id, fields.token);
return;
}
//results.length === 1
res.status(200).json({
username: results[0].username,
gold: results[0].gold,
recruits: results[0].recruits,
soldiers: results[0].soldiers,
spies: results[0].spies,
scientists: results[0].scientists
});
res.end();
log('Untrain executed', results[0].username, roleName, req.body.id, req.body.token);
});
});
});
});
});
}
};
const ladderRequest = (connection) => (req, res) => {
let query = 'SELECT username, soldiers, recruits, gold FROM accounts JOIN profiles ON accounts.id = profiles.accountId ORDER BY soldiers DESC, recruits DESC, gold DESC LIMIT ?, ?;';
@@ -402,7 +365,7 @@ const ladderRequest = (connection) => (req, res) => {
res.status(200).json(results);
log('Ladder sent', req.body.start, req.body.length, results);
});
}
};
const runGoldTick = (connection) => {
let goldTickJob = new CronJob('0 */30 * * * *', () => {
@@ -415,7 +378,7 @@ const runGoldTick = (connection) => {
});
goldTickJob.start();
}
};
module.exports = {
// profileCreate: profileCreate, //NOTE: Not actually used
@@ -425,4 +388,4 @@ module.exports = {
untrainRequest: untrainRequest,
ladderRequest: ladderRequest,
runGoldTick: runGoldTick
}
};