summaryrefslogtreecommitdiff
path: root/src/routers/vault/middlewares/evol/account.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/routers/vault/middlewares/evol/account.js')
-rw-r--r--src/routers/vault/middlewares/evol/account.js326
1 files changed, 326 insertions, 0 deletions
diff --git a/src/routers/vault/middlewares/evol/account.js b/src/routers/vault/middlewares/evol/account.js
new file mode 100644
index 0000000..5067334
--- /dev/null
+++ b/src/routers/vault/middlewares/evol/account.js
@@ -0,0 +1,326 @@
+"use strict";
+
+const regexes = {
+ token: /^[a-zA-Z0-9-_]{6,128}$/, // UUID
+ any30: /^[^\s][^\t\r\n]{6,28}[^\s]$/, // herc password (this looks scary)
+ alnum23: /^[a-zA-Z0-9_]{4,23}$/, // mostly for username
+ 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 (let acc of claimed) {
+ acc = await req.app.locals.evol.login.findByPk(acc.accountId);
+ 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") || "");
+
+ if (!token.match(regexes.token)) {
+ res.status(400).json({
+ status: "error",
+ error: "missing session key",
+ });
+ req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`);
+ req.app.locals.cooldown(req, 3e5);
+ return;
+ }
+
+ const session = req.app.locals.session.get(token);
+
+ if (session === null || session === undefined) {
+ res.status(410).json({
+ status: "error",
+ error: "session expired",
+ });
+ req.app.locals.cooldown(req, 5e3);
+ return;
+ }
+
+ if (session.authenticated !== true) {
+ res.status(401).json({
+ status: "error",
+ error: "not authenticated",
+ });
+ req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`);
+ req.app.locals.cooldown(req, 3e5);
+ 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,
+ });
+};
+
+const new_account = async (req, res, next) => {
+ const token = String(req.get("X-VAULT-SESSION") || "");
+
+ if (!token.match(regexes.token)) {
+ res.status(400).json({
+ status: "error",
+ error: "missing session key",
+ });
+ req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`);
+ req.app.locals.cooldown(req, 3e5);
+ return;
+ }
+
+ if (!req.body ||
+ !Reflect.has(req.body, "username") || !Reflect.has(req.body, "password") ||
+ !req.body.username.match(regexes.alnum23) ||
+ !req.body.password.match(regexes.any30)) { // FIXME: this is unsafe: can cause a promise rejection if something else than a string is passed (no Number.match() exists)
+ res.status(400).json({
+ status: "error",
+ error: "invalid format",
+ });
+ req.app.locals.cooldown(req, 5e3);
+ return;
+ }
+
+ const session = req.app.locals.session.get(token);
+
+ if (session === null || session === undefined) {
+ res.status(410).json({
+ status: "error",
+ error: "session expired",
+ });
+ req.app.locals.cooldown(req, 5e3);
+ return;
+ }
+
+ if (session.authenticated !== true) {
+ res.status(401).json({
+ status: "error",
+ error: "not authenticated",
+ });
+ req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`);
+ req.app.locals.cooldown(req, 3e5);
+ return;
+ }
+
+ // this check is necessary because login.userid has no UNIQUE constraint
+ const existing = await req.app.locals.evol.login.findOne({
+ where: {userid: req.body.username}
+ });
+
+ if (existing !== null) {
+ res.status(409).json({
+ status: "error",
+ error: "already exists",
+ });
+ req.app.locals.cooldown(1e3);
+ return;
+ }
+
+ const evol_acc = await req.app.locals.evol.login.create({
+ userid: req.body.username,
+ userPass: req.body.password,
+ email: `${session.vault}@vault`, // setting an actual email is pointless
+ });
+
+ req.app.locals.vault.account_log.create({
+ vaultId: session.vault,
+ accountType: "EVOL",
+ actionType: "CREATE",
+ accountId: evol_acc.accountId,
+ ip: req.app.locals.sequelize.vault.fn("INET6_ATON", req.ip),
+ });
+
+ // immediately claim it
+ await req.app.locals.vault.claimed_game_accounts.create({
+ accountId: evol_acc.accountId,
+ vaultId: session.vault,
+ });
+
+ // now add it to the evol cache
+ const account = {
+ name: evol_acc.userid,
+ accountId: evol_acc.accountId,
+ chars: [],
+ };
+ session.gameAccounts.push(account);
+
+ req.app.locals.logger.info(`Vault.evol.account: created a new game account: ${account.accountId} {${session.vault}} [${req.ip}]`);
+
+ res.status(200).json({
+ status: "success",
+ account,
+ });
+
+ req.app.locals.cooldown(5e3);
+};
+
+const update_account = async (req, res, next) => {
+ const token = String(req.get("X-VAULT-SESSION") || "");
+
+ if (!token.match(regexes.token)) {
+ res.status(400).json({
+ status: "error",
+ error: "missing session key",
+ });
+ req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`);
+ req.app.locals.cooldown(req, 3e5);
+ return;
+ }
+
+ if (!req.body || !Reflect.has(req.body, "accountId") ||
+ !String(req.body.accountId).match(regexes.gid) || !(
+ (Reflect.has(req.body, "username") && req.body.username.match(regexes.alnum23)) ||
+ (Reflect.has(req.body, "password") && req.body.password.match(regexes.any30)))) { // FIXME: this is unsafe: can cause a promise rejection if something else than a string is passed (no Number.match() exists)
+ res.status(400).json({
+ status: "error",
+ error: "invalid format",
+ });
+ req.app.locals.cooldown(req, 5e3);
+ return;
+ }
+
+ const session = req.app.locals.session.get(token);
+
+ if (session === null || session === undefined) {
+ res.status(410).json({
+ status: "error",
+ error: "session expired",
+ });
+ req.app.locals.cooldown(req, 5e3);
+ return;
+ }
+
+ if (session.authenticated !== true) {
+ res.status(401).json({
+ status: "error",
+ error: "not authenticated",
+ });
+ req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`);
+ req.app.locals.cooldown(req, 3e5);
+ return;
+ }
+
+ let account = null;
+ for (const acc of session.gameAccounts) {
+ if (acc.accountId === req.body.accountId) {
+ account = acc;
+ break;
+ }
+ }
+
+ if (account === null) {
+ res.status(404).json({
+ status: "error",
+ error: "account not found",
+ });
+ req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to modify a game account not owned by the user {${session.vault}} [${req.ip}]`);
+ req.app.locals.cooldown(req, 3e5);
+ return;
+ }
+
+ let update_fields = {};
+ if (Reflect.has(req.body, "username")) {
+ // check if the name exists
+ const existing = await req.app.locals.evol.login.findOne({
+ where: {userid: req.body.username}
+ });
+
+ if (existing !== null) {
+ res.status(409).json({
+ status: "error",
+ error: "already exists",
+ });
+ req.app.locals.cooldown(req, 500);
+ return;
+ }
+
+ update_fields = {
+ userid: req.body.username,
+ };
+ account.name = req.body.username;
+ req.app.locals.logger.info(`Vault.evol.account: changed username of game account ${account.accountId} {${session.vault}} [${req.ip}]`);
+ req.app.locals.vault.account_log.create({
+ vaultId: session.vault,
+ accountType: "EVOL",
+ actionType: "UPDATE",
+ details: "username",
+ accountId: account.accountId,
+ ip: req.app.locals.sequelize.vault.fn("INET6_ATON", req.ip),
+ });
+ } else {
+ update_fields = {
+ userPass: req.body.password,
+ };
+ req.app.locals.logger.info(`Vault.evol.account: changed password of game account ${account.accountId} {${session.vault}} [${req.ip}]`);
+ req.app.locals.vault.account_log.create({
+ vaultId: session.vault,
+ accountType: "EVOL",
+ actionType: "UPDATE",
+ details: "password",
+ accountId: account.accountId,
+ ip: req.app.locals.sequelize.vault.fn("INET6_ATON", req.ip),
+ });
+ }
+
+ await req.app.locals.evol.login.update(update_fields, {where: {
+ accountId: account.accountId,
+ }});
+
+ res.status(200).json({
+ status: "success",
+ account,
+ });
+
+ req.app.locals.cooldown(req, 5e3);
+};
+
+module.exports = exports = async (req, res, next) => {
+ switch(req.method) {
+ case "GET": // list accounts
+ return await get_accounts(req, res, next);
+ case "POST": // new account
+ return await new_account(req, res, next);
+ case "PATCH": // change username/password
+ return await update_account(req, res, next);
+ // TODO: PUT: move char
+ // TODO: DELETE: delete account and related data
+ default:
+ next(); // fallthrough to default endpoint (404)
+ }
+};