From ecb8bd66d17592346c8855bb021dae802552dabf Mon Sep 17 00:00:00 2001 From: gumi Date: Wed, 4 Mar 2020 21:22:46 -0500 Subject: pre-cache the game accounts on login --- src/routers/vault/middlewares/evol/account.js | 64 +----------- src/routers/vault/middlewares/legacy/account.js | 126 ++++++++---------------- src/routers/vault/middlewares/session.js | 46 +++++---- src/routers/vault/types/Char.js | 34 +++++++ src/routers/vault/types/EvolAccount.js | 21 ++++ src/routers/vault/types/EvolChar.js | 21 ++++ src/routers/vault/types/GameAccount.js | 38 +++++++ src/routers/vault/types/LegacyAccount.js | 21 ++++ src/routers/vault/types/LegacyChar.js | 21 ++++ src/routers/vault/types/Session.js | 44 ++++++--- src/routers/vault/utils/claim.js | 27 +++-- src/routers/vault/utils/game_accounts.js | 123 +++++++++++++++++++++++ 12 files changed, 397 insertions(+), 189 deletions(-) create mode 100644 src/routers/vault/types/Char.js create mode 100644 src/routers/vault/types/EvolAccount.js create mode 100644 src/routers/vault/types/EvolChar.js create mode 100644 src/routers/vault/types/GameAccount.js create mode 100644 src/routers/vault/types/LegacyAccount.js create mode 100644 src/routers/vault/types/LegacyChar.js create mode 100644 src/routers/vault/utils/game_accounts.js diff --git a/src/routers/vault/middlewares/evol/account.js b/src/routers/vault/middlewares/evol/account.js index 80f741d..440367c 100644 --- a/src/routers/vault/middlewares/evol/account.js +++ b/src/routers/vault/middlewares/evol/account.js @@ -1,4 +1,5 @@ "use strict"; +const EvolAccount = require("../../types/EvolAccount.js"); const regexes = { token: /^[a-zA-Z0-9-_]{6,128}$/, // UUID @@ -7,48 +8,6 @@ const regexes = { gid: /^[23][0-9]{6}$/, // account id }; -const get_account_list = async (req, vault_id) => { - const accounts = []; - const claimed = await req.app.locals.vault.claimed_game_accounts.findAll({ - where: {vaultId: vault_id}, - }); - - for (const acc_ of claimed) { - const acc = await req.app.locals.evol.login.findByPk(acc_.accountId); - - if (acc === null || acc === undefined) { - // unexpected: account was deleted - console.info(`Vault.evol.account: unlinking deleted account ${acc_.accountId} {${vault_id}} [${req.ip}]`); - await acc_.destroy(); // un-claim the account - continue; - } - - const chars = []; - const chars_ = await req.app.locals.evol.char.findAll({ - where: {accountId: acc.accountId}, - }); - - for (const char of chars_) { - chars.push({ - // TODO: make this a class - name: char.name, - charId: char.charId, - level: char.baseLevel, - sex: char.sex, - }); - } - - accounts.push({ - // TODO: make this a class - name: acc.userid, - accountId: acc.accountId, - chars, - }); - } - - return accounts; -}; - const get_accounts = async (req, res, next) => { const token = String(req.get("X-VAULT-SESSION") || ""); @@ -83,21 +42,12 @@ const get_accounts = async (req, res, next) => { return; } - let accounts = session.gameAccounts; - - if (accounts.length < 1) { - console.info(`Vault.evol.account: fetching evol accounts {${session.vault}} [${req.ip}]`); - accounts = await get_account_list(req, session.vault); - session.gameAccounts = accounts; - req.app.locals.cooldown(req, 3e3); - } else { - req.app.locals.cooldown(req, 1e3); - } - res.status(200).json({ status: "success", - accounts, + accounts: session.gameAccounts, }); + + req.app.locals.cooldown(req, 1e3); }; const new_account = async (req, res, next) => { @@ -181,11 +131,7 @@ const new_account = async (req, res, next) => { }); // now add it to the evol cache - const account = { - name: evol_acc.userid, - accountId: evol_acc.accountId, - chars: [], - }; + const account = new EvolAccount(evol_acc.accountId, evol_acc.userid); session.gameAccounts.push(account); req.app.locals.logger.info(`Vault.evol.account: created a new game account: ${account.accountId} {${session.vault}} [${req.ip}]`); diff --git a/src/routers/vault/middlewares/legacy/account.js b/src/routers/vault/middlewares/legacy/account.js index fa42ca2..bdd9a84 100644 --- a/src/routers/vault/middlewares/legacy/account.js +++ b/src/routers/vault/middlewares/legacy/account.js @@ -1,6 +1,10 @@ "use strict"; const md5saltcrypt = require("../../utils/md5saltcrypt.js"); const flatfile = require("../../utils/flatfile.js"); +const LegacyAccount = require("../../types/LegacyAccount.js"); +const LegacyChar = require("../../types/LegacyChar.js"); +const EvolAccount = require("../../types/EvolAccount.js"); +const EvolChar = require("../../types/EvolChar.js"); const regexes = { token: /^[a-zA-Z0-9-_]{6,128}$/, // UUID @@ -10,48 +14,6 @@ const regexes = { gid: /^[23][0-9]{6}$/, // account id }; -const get_account_list = async (req, vault_id) => { - const accounts = []; - const claimed = await req.app.locals.vault.claimed_legacy_accounts.findAll({ - where: {vaultId: vault_id}, - }); - - for (const acc_ of claimed) { - const acc = await req.app.locals.legacy.login.findByPk(acc_.accountId); - - if (acc === null || acc === undefined) { - // unexpected: account was deleted - console.info(`Vault.legacy.account: unlinking deleted account ${acc_.accountId} {${vault_id}} [${req.ip}]`); - await acc_.destroy(); // un-claim the account - continue; - } - - const chars = []; - const chars_ = await req.app.locals.legacy.char.findAll({ - where: {accountId: acc.accountId}, - }); - - for (const char of chars_) { - chars.push({ - name: char.name, - charId: char.charId, - revoltId: char.revoltId, - level: char.baseLevel, - sex: char.sex, - }); - } - - accounts.push({ - name: acc.userid, - accountId: acc.accountId, - revoltId: acc.revoltId, - chars, - }); - } - - return accounts; -}; - const get_accounts = async (req, res, next) => { const token = String(req.get("X-VAULT-SESSION") || ""); @@ -86,21 +48,12 @@ const get_accounts = async (req, res, next) => { return; } - let accounts = session.legacyAccounts; - - if (accounts.length < 1) { - console.info(`Vault.legacy.account: fetching legacy accounts {${session.vault}} [${req.ip}]`); - accounts = await get_account_list(req, session.vault); - session.legacyAccounts = accounts; - req.app.locals.cooldown(req, 3e3); - } else { - req.app.locals.cooldown(req, 1e3); - } - res.status(200).json({ status: "success", - accounts, + accounts: session.legacyAccounts, }); + + req.app.locals.cooldown(req, 1e3); }; const claim_by_password = async (req, res, next) => { @@ -228,28 +181,22 @@ const claim_by_password = async (req, res, next) => { }); // now we must update the session cache: - const chars = []; - const chars_ = await req.app.locals.legacy.char.findAll({ + const chars = await req.app.locals.legacy.char.findAll({ where: {accountId: legacy.accountId}, }); - for (const char of chars_) { - chars.push({ - // TODO: make this a class - name: char.name, - charId: char.charId, - revoltId: char.revoltId, - level: char.baseLevel, - sex: char.sex, - }); + const account = new LegacyAccount(legacy.accountId, legacy.userid); + account.revoltId = legacy.revoltId; + + for (const char_ of chars) { + const char = new LegacyChar(account, char_.charId, char_.name); + char.revoltId = char_.revoltId; + char.baseLevel = char_.baseLevel; + char.gender = char_.sex; + + account.chars.push(char); } - const account = { - name: legacy.userid, - accountId: legacy.accountId, - revoltId: legacy.revoltId, - chars, - }; session.legacyAccounts.push(account); res.status(200).json({ @@ -374,14 +321,14 @@ const migrate = async (req, res, next) => { vaultId: session.vault, }); - // now add it to the evol cache - const cache_key = session.gameAccounts.push({ - name: evol_acc.userid, - accountId: evol_acc.accountId, - chars: [], - }) - 1; + const evol_account = new EvolAccount(evol_acc.accountId, evol_acc.userid); + evol_account.legacyId = legacy.accountId; + evol_account.legacyAccount = legacy; + + // update legacy account cache + legacy.revoltId = evol_acc.accountId; + legacy.revoltAccount = evol_acc; - legacy.revoltId = evol_acc.accountId; // update legacy cache await req.app.locals.legacy.login.update({ // update sql revoltId: evol_acc.accountId, }, {where: { @@ -391,6 +338,7 @@ const migrate = async (req, res, next) => { // XXX: ideally we should be using createBulk but we also want to update for (const [num, char] of legacy.chars.entries()) { if (char.revoltId) { + // already migrated continue; } @@ -416,14 +364,17 @@ const migrate = async (req, res, next) => { }); // update the evol cache - session.gameAccounts[cache_key].chars.push({ - name: evol_char.name, - charId: evol_char.charId, - level: 1, - sex: evol_char.sex, - }); + const evol_char_ = new EvolChar(evol_account, evol_char.charId, evol_char.name); + evol_char_.legacyChar = char; + evol_char_.legacyId = char.charId; + evol_char_.gender = evol_char.sex; + + evol_account.chars.push(evol_char_); + + // update legacy cache + char.revoltId = evol_char.charId; + char.revoltAccount = evol_account; - char.revoltId = evol_char.charId; // update legacy cache await req.app.locals.legacy.char.update({ // update sql revoltId: evol_char.charId, }, {where: { @@ -431,11 +382,14 @@ const migrate = async (req, res, next) => { }}); } + session.gameAccounts.push(evol_account); + // TODO: try/catch each of the await operations res.status(200).json({ status: "success", - account: session.gameAccounts[cache_key], + session, + account: evol_account, }); req.app.locals.logger.info(`Vault.legacy.account: migrated Legacy account ${legacy.accountId} {${session.vault}} [${req.ip}]`); diff --git a/src/routers/vault/middlewares/session.js b/src/routers/vault/middlewares/session.js index 0073e90..990de49 100644 --- a/src/routers/vault/middlewares/session.js +++ b/src/routers/vault/middlewares/session.js @@ -3,6 +3,7 @@ const uuidv4 = require("uuid/v4"); const nodemailer = require("nodemailer"); const Claim = require("../utils/claim.js"); const Session = require("../types/Session.js"); +const game_accounts = require("../utils/game_accounts.js"); let transporter = nodemailer.createTransport({ sendmail: true, @@ -82,10 +83,7 @@ const auth_session = async (req, res, next) => { // already authed, tell client res.status(200).json({ status: "success", - session: { - expires: session.expires, - identity: session.identity, - } + session, }); req.app.locals.cooldown(req, 500); return; @@ -154,6 +152,10 @@ const auth_session = async (req, res, next) => { req.app.locals.cooldown(req, 6e4); + // pre-cache the accounts and chars in the session cache + await game_accounts.get_legacy(req, session); + await game_accounts.get_evol(req, session); + // authenticate this session session.authenticated = true; @@ -165,26 +167,32 @@ const auth_session = async (req, res, next) => { if (session.identity !== session.primaryIdentity) { // user did not log in with their primary identity - // TODO: allow to block logging in with non-primary identities const primary = await req.app.locals.vault.identity.findByPk(session.primaryIdentity); - transporter.sendMail({ - from: process.env.VAULT__MAILER__FROM, - to: primary.email, - subject: "The Mana World security notice", - text: "Someone has logged in to your Vault account using an email address that " + - "is not your primary address. If this wasn't you, please contact us immediately.\n\n" + - "To stop receiving login notices, use your primary email address when logging in." - }, (err, info) => {}); - } - // TODO: already cache the identities and accounts in the session + if (primary === null || primary === undefined) { + // the vault account has no primary identity (bug): let's fix this + console.warn(`Vault.session: fixing account with a deleted primary identity {${session.vault}} [${req.ip}]`); + await req.app.locals.vault.login.update({ + primaryIdentity: session.identity, + }, {where: { + id: session.vault, + }}); + session.primaryIdentity = session.identity; + } else { + transporter.sendMail({ + from: process.env.VAULT__MAILER__FROM, + to: primary.email, + subject: "The Mana World security notice", + text: "Someone has logged in to your Vault account using an email address that " + + "is not your primary address. If this wasn't you, please contact us immediately.\n\n" + + "To stop receiving login notices, use your primary email address when logging in." + }, (err, info) => {}); + } + } res.status(200).json({ status: "success", - session: { - expires: session.expires, - identity: session.identity, - } + session, }); }; diff --git a/src/routers/vault/types/Char.js b/src/routers/vault/types/Char.js new file mode 100644 index 0000000..a90b950 --- /dev/null +++ b/src/routers/vault/types/Char.js @@ -0,0 +1,34 @@ +/** + * represents a generic game character + */ +module.exports = class Char { + /** reference to the parent GameAccount */ + account = null; + /** the ID of this char */ + charId = 0; + /** the public name */ + name = ""; + /** the level of the char */ + baseLevel = 1; + /** gender of the char */ + gender = "N"; + + constructor (acc, id, name) { + this.account = acc; + this.charId = id; + this.name = name; + } + + /** + * serialize for sending over the network + * @param {*} key + */ + toJSON (key) { + return { + charId: this.charId, + name: this.name, + level: this.baseLevel, + sex: this.gender, + }; + } +} diff --git a/src/routers/vault/types/EvolAccount.js b/src/routers/vault/types/EvolAccount.js new file mode 100644 index 0000000..6db03bf --- /dev/null +++ b/src/routers/vault/types/EvolAccount.js @@ -0,0 +1,21 @@ +const GameAccount = require("./GameAccount.js"); + +/** + * represents an Evol game account + */ +module.exports = class EvolAccount extends GameAccount { + /** account id of the source legacy account (ported) */ + legacyId = null; + /** reference to the LegacyAccount */ + legacyAccount = null; + + /** + * serialize for sending over the network + * @param {*} key + */ + toJSON (key) { + return Object.assign({ + legacyId: this.legacyId, + }, super.toJSON()); + } +} diff --git a/src/routers/vault/types/EvolChar.js b/src/routers/vault/types/EvolChar.js new file mode 100644 index 0000000..1107d62 --- /dev/null +++ b/src/routers/vault/types/EvolChar.js @@ -0,0 +1,21 @@ +const Char = require("./Char.js"); + +/** + * represents an Evol game char + */ +module.exports = class EvolChar extends Char { + /** char id of the source legacy char (ported) */ + legacyId = null; + /** reference to the LegacyChar */ + legacyChar = null; + + /** + * serialize for sending over the network + * @param {*} key + */ + toJSON (key) { + return Object.assign({ + legacyId: this.legacyId, + }, super.toJSON()); + } +} diff --git a/src/routers/vault/types/GameAccount.js b/src/routers/vault/types/GameAccount.js new file mode 100644 index 0000000..fa94808 --- /dev/null +++ b/src/routers/vault/types/GameAccount.js @@ -0,0 +1,38 @@ +/** + * represents a generic game account + */ +module.exports = class GameAccount { + /** the GID of the account */ + accountId = 0; + /** the login username */ + userid = ""; + /** the email address associated with the account */ + email = null; + /** Char[] */ + chars = []; + /** the last time the account logged in */ + lastLogin = null; + /** the last IP that was used to log in */ + lastIP = null; + /** the total number of times the account logged in */ + loginCount = 0; + /** whether the account is banned */ + banned = false; + + constructor (id, name) { + this.accountId = id; + this.userid = name; + } + + /** + * serialize for sending over the network + * @param {*} key + */ + toJSON (key) { + return { + accountId: this.accountId, + name: this.userid, + chars: this.chars, + }; + } +} diff --git a/src/routers/vault/types/LegacyAccount.js b/src/routers/vault/types/LegacyAccount.js new file mode 100644 index 0000000..747e6df --- /dev/null +++ b/src/routers/vault/types/LegacyAccount.js @@ -0,0 +1,21 @@ +const GameAccount = require("./GameAccount.js"); + +/** + * represents a Legacy game account + */ +module.exports = class LegacyAccount extends GameAccount { + /** account id of the target evol account (ported) */ + revoltId = null; + /** reference to the EvolAccount of the target evol account */ + revoltAccount = null; + + /** + * serialize for sending over the network + * @param {*} key + */ + toJSON (key) { + return Object.assign({ + revoltId: this.revoltId, + }, super.toJSON()); + } +} diff --git a/src/routers/vault/types/LegacyChar.js b/src/routers/vault/types/LegacyChar.js new file mode 100644 index 0000000..b893c3f --- /dev/null +++ b/src/routers/vault/types/LegacyChar.js @@ -0,0 +1,21 @@ +const Char = require("./Char.js"); + +/** + * represents a Legacy game char + */ +module.exports = class LegacyChar extends Char { + /** char id of the target evol char (ported) */ + revoltId = null; + /** reference to the EvolChar */ + revoltChar = null; + + /** + * serialize for sending over the network + * @param {*} key + */ + toJSON (key) { + return Object.assign({ + revoltId: this.revoltId, + }, super.toJSON()); + } +} diff --git a/src/routers/vault/types/Session.js b/src/routers/vault/types/Session.js index 34bd250..1809cac 100644 --- a/src/routers/vault/types/Session.js +++ b/src/routers/vault/types/Session.js @@ -2,20 +2,42 @@ * holds a cache of all the user data fetched from SQL */ module.exports = class Session { - expires = new Date(); // expiry Date - vault = null; // Vault account id - authenticated = false; // whether the user logged in - identity = null; // the identity that was used to log in - email; // the email address of the identity that was used to log in - identities = []; // cache holding all identities - primaryIdentity = null; // the main identity of the account - allowNonPrimary = true; // whether to allow logging in with a non-primary ident - legacyAccounts = []; // cache holding all legacy game accounts - gameAccounts = []; // cache holding all evol game accounts - ip; // ip that was used to init the session + /** expiry Date */ + expires = new Date(); + /** Vault account id */ + vault = null; + /** whether the user logged in */ + authenticated = false; + /** the identity that was used to log in */ + identity = null; + /** the email address of the identity that was used to log in */ + email; + /** cache holding all identities */ + identities = []; + /** the main identity of the account */ + primaryIdentity = null; + /** whether to allow logging in with a non-primary ident */ + allowNonPrimary = true; + /** LegacyAccount[] cache holding all legacy game accounts */ + legacyAccounts = []; + /** EvolAccount[] cache holding all evol game accounts */ + gameAccounts = []; + /** ip that was used to init the session */ + ip; constructor (ip, email) { this.ip = ip; this.email = email; } + + /** + * serialize for sending over the network + * @param {*} key + */ + toJSON (key) { + return { + expires: this.expires, + identity: this.identity, + } + } } diff --git a/src/routers/vault/utils/claim.js b/src/routers/vault/utils/claim.js index b3dbe9d..d28e076 100644 --- a/src/routers/vault/utils/claim.js +++ b/src/routers/vault/utils/claim.js @@ -1,4 +1,6 @@ const { Op } = require("sequelize"); +const LegacyAccount = require("../types/LegacyAccount.js"); +const LegacyChar = require("../types/LegacyChar.js"); // claim by email // TODO: DRY this const claim_accounts = async (req, email, vault_id, session = null) => { @@ -49,27 +51,24 @@ const claim_accounts = async (req, email, vault_id, session = null) => { }); if (session !== null) { - const chars = []; const chars_ = await locals.legacy.char.findAll({ where: {accountId: acc.accountId}, }); + const legacy_account = new LegacyAccount(acc.accountId, acc.userid); + legacy_account.revoltId = acc.revoltId; + for (const char of chars_) { - chars.push({ - name: char.name, - charId: char.charId, - revoltId: char.revoltId, - level: char.baseLevel, - sex: char.sex, - }); + const legacy_char = new LegacyChar(legacy_account, char.charId, char.name); + legacy_char.revoltId = char.revoltId; + legacy_char.baseLevel = char.baseLevel; + legacy_char.gender = char.sex; + + legacy_account.chars.push(legacy_char); } + // add to session cache - session.legacyAccounts.push({ - name: acc.userid, - accountId: acc.accountId, - revoltId: acc.revoltId, - chars, - }); + session.legacyAccounts.push(legacy_account); } locals.logger.info(`Vault.legacy.account: linked Legacy account ${acc.accountId} to Vault account {${vault_id}} [${req.ip}]`); diff --git a/src/routers/vault/utils/game_accounts.js b/src/routers/vault/utils/game_accounts.js new file mode 100644 index 0000000..c19feb5 --- /dev/null +++ b/src/routers/vault/utils/game_accounts.js @@ -0,0 +1,123 @@ +const LegacyAccount = require("../types/LegacyAccount.js"); +const LegacyChar = require("../types/LegacyChar.js"); +const EvolAccount = require("../types/EvolAccount.js"); +const EvolChar = require("../types/EvolChar.js"); + +/** + * fetch the legacy game accounts and cache in the Session + * @param {*} req - the express request + * @param {Session} session - the Session + * @return {Promise} a promise resolving to an array of LegacyAccount + */ +const get_legacy_accounts = async (req, session) => { + const accounts = []; + const claimed = await req.app.locals.vault.claimed_legacy_accounts.findAll({ + where: {vaultId: session.vault}, + }); + + for (const acc_ of claimed) { + const acc = await req.app.locals.legacy.login.findByPk(acc_.accountId); + + if (acc === null || acc === undefined) { + // unexpected: account was deleted + console.info(`Vault.legacy.account: unlinking deleted account ${acc_.accountId} {${session.vault}} [${req.ip}]`); + await acc_.destroy(); // un-claim the account + continue; + } + + const account = new LegacyAccount(acc.accountId, acc.userid); + account.revoltId = acc.revoltId; + + const chars = await req.app.locals.legacy.char.findAll({ + where: {accountId: acc.accountId}, + }); + + for (const char of chars) { + const char_ = new LegacyChar(account, char.charId, char.name); + char_.baseLevel = char.baseLevel; + char_.gender = char.sex; + char_.revoltId = char.revoltId; + + account.chars.push(char_); + } + + accounts.push(account); + } + + session.legacyAccounts = accounts; + return accounts; +}; + +/** + * fetch the evol game accounts and cache in the Session + * @param {*} req - the express request + * @param {Session} session - the Session + * @return {Promise} a promise resolving to an array of EvolAccount + */ +const get_account_list = async (req, session) => { + const accounts = []; + const claimed = await req.app.locals.vault.claimed_game_accounts.findAll({ + where: {vaultId: session.vault}, + }); + + for (const acc_ of claimed) { + const acc = await req.app.locals.evol.login.findByPk(acc_.accountId); + + if (acc === null || acc === undefined) { + // unexpected: account was deleted + console.info(`Vault.evol.account: unlinking deleted account ${acc_.accountId} {${session.vault}} [${req.ip}]`); + await acc_.destroy(); // un-claim the account + continue; + } + + const account = new EvolAccount(acc.accountId, acc.userid); + + // check if this is an imported account + for (const legacy_acc of session.legacyAccounts) { + if (legacy_acc.revoltId === account.accountId) { + account.legacyId = legacy_acc.accountId; + + // two-way binding + account.legacyAccount = legacy_acc; + legacy_acc.revoltAccount = account; + break; + } + } + + const chars = await req.app.locals.evol.char.findAll({ + where: {accountId: acc.accountId}, + }); + + for (const char of chars) { + const char_ = new EvolChar(account, char.charId, char.name); + char_.baseLevel = char.baseLevel; + char_.gender = char.sex; + + // check if this is an imported char + for (const legacy_acc of session.legacyAccounts) { + for (const legacy_char of legacy_acc.chars) { + if (legacy_char.revoltId === char_.charId) { + char_.legacyId = legacy_char.charId; + + // two-way binding + char_.legacyChar = legacy_char; + legacy_char.revoltChar = char_; + break; + } + } + } + + account.chars.push(char_); + } + + accounts.push(account); + } + + session.gameAccounts = accounts; + return accounts; +}; + +module.exports = { + get_evol: get_account_list, + get_legacy: get_legacy_accounts, +}; -- cgit v1.2.3-60-g2f50