"use strict";
const EvolAccount = require("../../types/EvolAccount.js");
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_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;
}
res.status(200).json({
status: "success",
accounts: session.gameAccounts,
});
req.app.locals.cooldown(req, 1e3);
};
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 = 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}@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}@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}@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}@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)
}
};