diff --git a/common/utilities.js b/common/utilities.js index f24f840..61ab191 100644 --- a/common/utilities.js +++ b/common/utilities.js @@ -18,6 +18,7 @@ let excluded = [ //messages that should not be logged 'idle', 'Combat log sent', + 'News sent', 'Can\'t train while attacking', 'Can\'t untrain while attacking' diff --git a/public/news/2019-05-31-01.md b/public/news/2019-05-31-01.md new file mode 100644 index 0000000..f985f44 --- /dev/null +++ b/public/news/2019-05-31-01.md @@ -0,0 +1,34 @@ +14 Hours Of Refactoring And Debugging +--- +_31 May 2019_ + +What started as a relatively simple task - raising state out of the profile page and into the redux store - turned into a massive overhaul of the entire project. Almost every file has been poured over with a fine toothed come to ensure that it works just right, and that the coding standards are up to scratch with the rest of the project. + +I started working at midnight, which is typical for me lately but I'm still working on it now at 2pm. This was... quite an experience. Also, the mere fact that I'm still excited about this project after this ordeal shows that there is definitely something worth working on here. + +I began working on equipment before the refactoring, and the equipment was passed over during it, simply because I haven't finished it yet. However, now that the profile is stored in redux, I should be able to access information such as the scientist count much more easily. So, look forward to the equipment, coming... tomorrow probably. I'm about to get some sleep. + +```js +{ + "Weapons": { + "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, "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 } + } +} +``` + +In the mean time, here's the placeholder data for the equipment! I probably shouldn't be showing this, but meh. It's stored as a JSON file, rather than in the database, to make it faster to edit. Player inventories will be stored in the database, however. + +Oh, also, who would like a public-facing API? I'm not sure what for though, but still... + +P.S. The wait time between recruitments has been lowered to 20 hours. + diff --git a/public/styles/shared.css b/public/styles/shared.css index c47781f..f9d7929 100644 --- a/public/styles/shared.css +++ b/public/styles/shared.css @@ -266,4 +266,11 @@ footer { .minWidth { /* hacky */ min-width: 80px; -} \ No newline at end of file +} + +pre { + color: pink; + background-color: #222222; + margin-top: 1em; + margin-bottom: 1em; +} diff --git a/server/accounts.js b/server/accounts.js index c5e029b..ed34016 100644 --- a/server/accounts.js +++ b/server/accounts.js @@ -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 = `
${msg}${addr}
`; + //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 = `${msg}${addr}
`; + //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, diff --git a/server/combat.js b/server/combat.js index 151eb62..d59c506 100644 --- a/server/combat.js +++ b/server/combat.js @@ -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 -} +}; diff --git a/server/database.js b/server/database.js index 029c27f..614e68f 100644 --- a/server/database.js +++ b/server/database.js @@ -42,8 +42,8 @@ function handleDisconnect() { //finally return connection; -} +}; module.exports = { connectToDatabase: handleDisconnect -} \ No newline at end of file +}; \ No newline at end of file diff --git a/server/equipment.js b/server/equipment.js index 1fcce23..c524033 100644 --- a/server/equipment.js +++ b/server/equipment.js @@ -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, diff --git a/server/equipment_statistics.json b/server/equipment_statistics.json index 8523b4a..18ed71f 100644 --- a/server/equipment_statistics.json +++ b/server/equipment_statistics.json @@ -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 } } } \ No newline at end of file diff --git a/server/index.js b/server/index.js index 896bb97..3494d92 100644 --- a/server/index.js +++ b/server/index.js @@ -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')) ); diff --git a/server/news.js b/server/news.js index 4442d85..138d55a 100644 --- a/server/news.js +++ b/server/news.js @@ -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 -} \ No newline at end of file +}; \ No newline at end of file diff --git a/server/profiles.js b/server/profiles.js index 8a58dd6..54da52a 100644 --- a/server/profiles.js +++ b/server/profiles.js @@ -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 -} \ No newline at end of file +}; \ No newline at end of file diff --git a/sql/drop_everything.sql b/sql/drop_everything.sql index ab5e2ab..a1e4daa 100644 --- a/sql/drop_everything.sql +++ b/sql/drop_everything.sql @@ -1,3 +1,6 @@ +DROP TABLE equipment; +DROP TABLE pastCombat; +DROP TABLE pendingCombat; DROP TABLE profiles; DROP TABLE passwordRecover; DROP TABLE sessions; diff --git a/src/actions/accounts.js b/src/actions/account.js similarity index 52% rename from src/actions/accounts.js rename to src/actions/account.js index 7032105..fc4ab4d 100644 --- a/src/actions/accounts.js +++ b/src/actions/account.js @@ -1,8 +1,8 @@ export const LOGIN = 'LOGIN'; export const LOGOUT = 'LOGOUT'; -export const SESSIONCHANGE = 'SESSIONCHANGE'; +export const SESSION_CHANGE = 'SESSION_CHANGE'; -export function login(id, email, username, token) { +export const login = (id, email, username, token) => { return { type: LOGIN, id: id, @@ -12,15 +12,15 @@ export function login(id, email, username, token) { }; } -export function logout() { +export const logout = () => { return { type: LOGOUT }; } -export function sessionChange(token) { +export const sessionChange = (token) => { return { - type: SESSIONCHANGE, + type: SESSION_CHANGE, token: token }; } \ No newline at end of file diff --git a/src/actions/combat.js b/src/actions/combat.js deleted file mode 100644 index 1906331..0000000 --- a/src/actions/combat.js +++ /dev/null @@ -1,8 +0,0 @@ -export const SET_ATTACK_DISABLED = 'SET_ATTACK_DISABLED'; - -export function setAttackDisabled(disabled) { - return { - type: SET_ATTACK_DISABLED, - disabled: disabled - } -} \ No newline at end of file diff --git a/src/actions/profile.js b/src/actions/profile.js new file mode 100644 index 0000000..9fc969c --- /dev/null +++ b/src/actions/profile.js @@ -0,0 +1,61 @@ +export const STORE_PROFILE = 'STORE_PROFILE'; +export const STORE_USERNAME = 'STORE_USERNAME'; +export const STORE_GOLD = 'STORE_GOLD'; +export const STORE_RECRUITS = 'STORE_RECRUITS'; +export const STORE_SOLDIERS = 'STORE_SOLDIERS'; +export const STORE_SPIES = 'STORE_SPIES'; +export const STORE_SCIENTISTS = 'STORE_SCIENTISTS'; + +export const storeProfile = (username, gold, recruits, soldiers, spies, scientists) => { + return { + type: STORE_PROFILE, + username: username, + gold: gold, + recruits: recruits, + soldiers: soldiers, + spies: spies, + scientists: scientists + }; +} + +export const storeUsername = (username) => { + return { + type: STORE_USERNAME, + username: username + }; +} + +export const storeGold = (gold) => { + return { + type: STORE_GOLD, + gold: gold + }; +} + +export const storeRecruits = (recruits) => { + return { + type: STORE_RECRUITS, + recruits: recruits + }; +} + +export const storeSoldiers = (soldiers) => { + return { + type: STORE_SOLDIERS, + soldiers: soldiers + }; +} + +export const storeSpies = (spies) => { + return { + type: STORE_SPIES, + spies: spies + }; +} + +export const storeScientists = (scientists) => { + return { + type: STORE_SCIENTISTS, + scientists: scientists + }; +} \ No newline at end of file diff --git a/src/components/app.jsx b/src/components/app.jsx index d97c641..4c82b4e 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -59,6 +59,7 @@ export default class App extends React.Component { { GA.init() &&{this.state.warning}
+{this.state.warning}
+{this.state.changeMsg}
); + return ({this.state.changed}
); } } @@ -42,17 +41,17 @@ class PasswordChange extends React.Component { } }; -function mapStoreToProps(store) { +const mapStoreToProps = (store) => { return { - id: store.account.id - } -} + loggedIn: store.account.id !== 0 + }; +}; -function mapDispatchToProps(dispatch) { +const mapDispatchToProps = (dispatch) => { return { // - } -} + }; +}; PasswordChange = connect(mapStoreToProps, mapDispatchToProps)(PasswordChange); diff --git a/src/components/pages/password_recover.jsx b/src/components/pages/password_recover.jsx index 9a9b9bc..9006035 100644 --- a/src/components/pages/password_recover.jsx +++ b/src/components/pages/password_recover.jsx @@ -8,21 +8,20 @@ class PasswordRecover extends React.Component { constructor(props) { super(props); this.state = { - recoverSent: false, - recoverMsg: '' + recovered: '' } } render() { let Panel; - if (!this.state.recoverSent) { + if (!this.state.recovered) { Panel = () => { - return ({this.state.recoverMsg}
); + return ({this.state.recovered}
); } } diff --git a/src/components/pages/password_reset.jsx b/src/components/pages/password_reset.jsx index 5870ef2..e1e4cb9 100644 --- a/src/components/pages/password_reset.jsx +++ b/src/components/pages/password_reset.jsx @@ -1,6 +1,5 @@ import React from 'react'; import { withRouter, Link } from 'react-router-dom'; -import PropTypes from 'prop-types'; import queryString from 'query-string'; //panels @@ -10,8 +9,7 @@ class PasswordReset extends React.Component { constructor(props) { super(props); this.state = { - reset: false, - resetMsg: '', + reset: '', params: queryString.parse(props.location.search) } } @@ -21,11 +19,11 @@ class PasswordReset extends React.Component { if (!this.state.reset) { Panel = () => { - return ({this.state.resetMsg}
); + return ({this.state.reset}
); } } diff --git a/src/components/pages/profile.jsx b/src/components/pages/profile.jsx index a810f29..9516201 100644 --- a/src/components/pages/profile.jsx +++ b/src/components/pages/profile.jsx @@ -1,49 +1,25 @@ import React from 'react'; -import { connect } from 'react-redux'; import { withRouter, Link } from 'react-router-dom'; -import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; import queryString from 'query-string'; +//actions +import { storeProfile } from '../../actions/profile.js'; + //panels import CommonLinks from '../panels/common_links.jsx'; import AttackButton from '../panels/attack_button.jsx'; -import Equipment from '../panels/equipment.jsx'; -import CombatLog from '../panels/combat_log.jsx'; class Profile extends React.Component { constructor(props) { super(props); - let params = queryString.parse(props.location.search); - this.state = { - params: params, - - username: '', - gold: 0, - recruits: 0, - soldiers: 0, - spies: 0, - scientists: 0, - - warning: '', - - //combat log - start: params.log, - - //equipment - fetchStatistics: null, - fetchEquipment: null + params: queryString.parse(props.location.search), + warning: '', //TODO: unified warning? }; - this.sendRequest('/profilerequest', this.state.params.username ? this.state.params.username : this.props.username); - } - - componentDidUpdate(prevProps, prevState, snapshot) { - if (JSON.stringify(this.state) !== JSON.stringify(prevState)) { -// if (this.state.fetchStatistics) this.state.fetchStatistics(); -// if (this.state.fetchEquipment) this.state.fetchEquipment(); - } + this.sendRequest('/profilerequest', {username: this.state.params.username ? this.state.params.username : this.props.account.username}); } render() { @@ -54,8 +30,8 @@ class Profile extends React.Component { //side panel stuff let SidePanel; - if (this.props.id) { - if (this.props.username === this.state.username) { + if (this.props.account.id) { + if (this.props.account.username === this.props.profile.username) { SidePanel = this.MyProfileSidePanel.bind(this); } else { SidePanel = this.NotMyProfileSidePanel.bind(this); @@ -67,13 +43,12 @@ class Profile extends React.Component { //main panel let MainPanel; - if (this.props.id) { + if (this.props.account.id) { //logged in - if (this.state.username === this.props.username) { + if (this.props.account.username === this.props.profile.username) { MainPanel = this.MyProfileMainPanel.bind(this); } else { - //not logged in - if (this.state.username !== '') { + if (this.props.profile.username) { MainPanel = this.NotMyProfileMainPanel.bind(this); } else { MainPanel = this.ProfileNotFoundMainPanel.bind(this); @@ -81,7 +56,7 @@ class Profile extends React.Component { } } else { //not logged in - if (this.state.username !== '') { + if (this.props.profile.username) { MainPanel = this.LoggedOutMainPanel.bind(this); } else { MainPanel = this.ProfileNotFoundMainPanel.bind(this); @@ -107,23 +82,17 @@ class Profile extends React.Component { } //gameplay functions - sendRequest(url, username = this.props.username, role = '') { //NOTE: merged all requests here - //request this profile's info, using my credentials - let formData = new FormData(); - - formData.append('id', this.props.id); - formData.append('token', this.props.token); - - formData.append('username', username); - formData.append('role', role); - + sendRequest(url, args = {}) { //send a unified request, using my credentials //build the XHR let xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status === 200) { let json = JSON.parse(xhr.responseText); - this.storeProfile( + + this.props.storeProfile( json.username, json.gold, json.recruits, @@ -136,27 +105,18 @@ class Profile extends React.Component { this.setWarning(xhr.responseText); } } - } + }; - //send - xhr.open('POST', url, true); - xhr.send(formData); - } - - storeProfile(username, gold, recruits, soldiers, spies, scientists) { - this.setState({ - username: username, - gold: gold, - recruits: recruits, - soldiers: soldiers, - spies: spies, - scientists: scientists - }); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + xhr.send(JSON.stringify({ + id: this.props.account.id, + token: this.props.account.token, + ...args + })); } //panel functions MyProfileSidePanel() { - //return the side panel return (Username:
-{this.state.username}
+{this.props.profile.username}
+Gold:
-{this.state.gold}
-{this.props.profile.gold}
+ +(+1 gold for each recruit every half hour)
Recruits:
-{this.state.recruits}
- +{this.props.profile.recruits}
+ +Soldiers:
-{this.state.soldiers}
- - +{this.props.profile.soldiers}
+ + +Spies:
-{this.state.spies}
- - +{this.props.profile.spies}
+ + +Scientists:
-{this.state.scientists}
- - +{this.props.profile.scientists}
+ + +Username:
-{this.state.username}
- - +{this.props.profile.username}
+ + +Gold:
-{this.state.gold}
- - +{this.props.profile.gold}
+ + +Recruits:
-{this.state.recruits}
-{this.props.profile.recruits}
+ +Soldiers:
-{this.state.soldiers}
- - +{this.props.profile.soldiers}
+ + +Spies:
-{this.state.spies}
- - +{this.props.profile.spies}
+ + +Scientists:
-{this.state.scientists}
- - +{this.props.profile.scientists}
+ + +Username:
-{this.state.username}
- - +{this.props.profile.username}
+ + +Gold:
-{this.state.gold}
- - +{this.props.profile.gold}
+ + +Recruits:
-{this.state.recruits}
- - +{this.props.profile.recruits}
+ + +Soldiers:
-{this.state.soldiers}
- - +{this.props.profile.soldiers}
+ + +Spies:
-{this.state.spies}
- - +{this.props.profile.spies}
+ + +Scientists:
-{this.state.scientists}
- - +{this.props.profile.scientists}
+ + +Profile Not Found!
+{this.state.signupMsg}
); + return ({this.state.signedUp}
); } } diff --git a/src/components/panels/attack_button.jsx b/src/components/panels/attack_button.jsx index 72adfe8..c7a731a 100644 --- a/src/components/panels/attack_button.jsx +++ b/src/components/panels/attack_button.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; class AttackButton extends React.Component { @@ -8,85 +9,86 @@ class AttackButton extends React.Component { message: '' }; - this.sendAttackingStatusRequest(); + this.sendRequest('/attackstatusrequest', {attacker: this.props.attacker}); } render() { - if (this.state.message !== '') { + if (this.state.message) { return ({this.state.message}
); } else { + //inject something extra + let onClick = (e) => { + this.sendRequest('/attackrequest', {attacker: this.props.attacker, defender: this.props.defender}); + if (this.props.onClick) { + this.props.onClick(e); + } + }; + return ( - + ); } } - sendAttackRequest() { + //gameplay functions + sendRequest(url, args = {}) { //send a unified request, using my credentials //build the XHR let xhr = new XMLHttpRequest(); - xhr.open('POST', '/attackrequest', true); + xhr.open('POST', url, true); xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status === 200) { let json = JSON.parse(xhr.responseText); - if (json.status === 'attacking') { - this.setState({ message: `Your soldiers are attacking ${json.defender}` }); - } - } else if (xhr.status === 400) { - if (this.props.setWarning) { - this.props.setWarning(xhr.responseText); - } - } - } - } - xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - xhr.send(JSON.stringify({ - attacker: this.props.attacker, - defender: this.props.defender, - token: this.props.token - })); - - if (this.props.onClick) { - this.props.onClick(); - } - - this.props.setDisabled(true); - } - - sendAttackingStatusRequest() { - let xhr = new XMLHttpRequest(); - xhr.open('POST', '/attackstatusrequest', true); - - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - let json = JSON.parse(xhr.responseText); + //on success if (json.status === 'attacking') { this.setState({ message: `Your soldiers are attacking ${json.defender}` }); } } + else if (xhr.status === 400 && this.props.setWarning) { + this.props.setWarning(xhr.responseText); + } } - } + }; xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); xhr.send(JSON.stringify({ - username: this.props.attacker + id: this.props.id, + token: this.props.token, + ...args })); } }; AttackButton.propTypes = { + id: PropTypes.number.isRequired, + token: PropTypes.number.isRequired, + + attacker: PropTypes.string.isRequired, + defender: PropTypes.string.isRequired, + className: PropTypes.string, style: PropTypes.object, onClick: PropTypes.func, - setWarning: PropTypes.func, - attacker: PropTypes.string.isRequired, - defender: PropTypes.string.isRequired, - token: PropTypes.number.isRequired + setWarning: PropTypes.func }; +const mapStoreToProps = (store) => { + return { + id: store.account.id, + token: store.account.token + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + // + }; +}; + +AttackButton = connect(mapStoreToProps, mapDispatchToProps)(AttackButton); + export default AttackButton; \ No newline at end of file diff --git a/src/components/panels/blurb.jsx b/src/components/panels/blurb.jsx index 52b8b0f..09ebd5b 100644 --- a/src/components/panels/blurb.jsx +++ b/src/components/panels/blurb.jsx @@ -21,5 +21,5 @@ export default class Blurb extends React.Component { ); } -} +}; diff --git a/src/components/panels/combat_log.jsx b/src/components/panels/combat_log.jsx deleted file mode 100644 index e0c0ff8..0000000 --- a/src/components/panels/combat_log.jsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { withRouter } from 'react-router-dom'; - -import PagedCombatLog from './paged_combat_log.jsx'; - -class CombatLog extends React.Component { - constructor(props) { - super(props); - this.state = { - start: parseInt(props.start) || 0, - length: parseInt(props.length) || 20, - fetch: null - }; - } - - componentDidUpdate(prevProps, prevState, snapshot) { - if (JSON.stringify(this.state) !== JSON.stringify(prevState)) { - this.state.fetch(); - } - } - - render() { - let ButtonHeader = this.buttonHeader.bind(this); - - return ( -Return Home
Your Kingdom
Game Ladder
+Combat Log
Change Password
Return Home
Sign Up
Login
Recover Password
-Return Home
Game Ladder
{this.state.warning}