summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgumi <git@gumi.ca>2020-03-15 21:56:57 -0400
committergumi <git@gumi.ca>2020-03-15 21:56:57 -0400
commit8a1302edf0506c92bd9a8e83ed7c1e145b246513 (patch)
treefae4703cb17a829b102c60550acb7cdea0878685
parenta0a35766911f5354487663b0f148b824ca32ba44 (diff)
downloadapi-8a1302edf0506c92bd9a8e83ed7c1e145b246513.tar.gz
api-8a1302edf0506c92bd9a8e83ed7c1e145b246513.tar.bz2
api-8a1302edf0506c92bd9a8e83ed7c1e145b246513.tar.xz
api-8a1302edf0506c92bd9a8e83ed7c1e145b246513.zip
add an Identity type
-rw-r--r--src/routers/vault/index.js5
-rw-r--r--src/routers/vault/middlewares/account.js32
-rw-r--r--src/routers/vault/middlewares/identity.js74
-rw-r--r--src/routers/vault/middlewares/session.js46
-rw-r--r--src/routers/vault/types/Identity.js58
-rw-r--r--src/routers/vault/types/Session.js78
-rw-r--r--src/routers/vault/types/SessionStore.js4
7 files changed, 207 insertions, 90 deletions
diff --git a/src/routers/vault/index.js b/src/routers/vault/index.js
index 831ec93..c0bc788 100644
--- a/src/routers/vault/index.js
+++ b/src/routers/vault/index.js
@@ -6,7 +6,7 @@ const SessionStore = require("./types/SessionStore.js");
const models = {
vault: [
"login", "login_log",
- "identity", "identity_log",
+ /*"identity",*/ "identity_log",
"claimed_game_accounts",
"claimed_legacy_accounts",
"account_log",
@@ -106,6 +106,9 @@ module.exports = exports = class Vault {
console.info(`Vault: loaded models for ${DB}`);
}
+ const Identity = require("./types/Identity.js");
+ this.api.locals.vault.identity = Identity.define(this.sequelize.vault);
+
await this.sequelize.vault.sync({alter: {drop: false}}); // update SQL tables
this.api.locals.sequelize = this.sequelize; // for access to sequelize.fn
diff --git a/src/routers/vault/middlewares/account.js b/src/routers/vault/middlewares/account.js
index 3c1cf52..5a5fa85 100644
--- a/src/routers/vault/middlewares/account.js
+++ b/src/routers/vault/middlewares/account.js
@@ -1,11 +1,9 @@
"use strict";
const validate = require("../utils/validate.js");
-
-const regexes = {
- token: /^[a-zA-Z0-9-_]{6,128}$/, // UUID
-};
+const Session = require("../types/Session.js");
const get_data = async (req, res, next) => {
+ /** @type {Session} */
let session;
try {
@@ -14,14 +12,7 @@ const get_data = async (req, res, next) => {
res.status(200).json({
status: "success",
- data: {
- // TODO: make this a method of Session
- primaryIdentity: session.primaryIdentity,
- allowNonPrimary: session.allowNonPrimary,
- strictIPCheck: session.strictIPCheck,
- requireSecret: true,
- vaultId: session.vault,
- },
+ data: session.getAccountData(),
});
req.app.locals.cooldown(req, 1e3);
};
@@ -35,19 +26,20 @@ const update_account = async (req, res, next) => {
const data = {
primary: +validate.get_prop(req, "primary"),
- allow: !!validate.get_prop(req, "allow"),
- strict: !!validate.get_prop(req, "strict"),
+ allow: validate.get_prop(req, "allow") === "true",
+ strict: validate.get_prop(req, "strict") === "true",
};
const update_fields = {};
- if (session.primaryIdentity !== data.primary) {
+ if (session.primaryIdentity.id !== data.primary) {
// update primary identity
let new_primary = null;
for (const ident of session.identities) {
if (ident.id === data.primary) {
new_primary = ident.id;
+ session.primaryIdentity = ident;
break;
}
}
@@ -81,13 +73,13 @@ const update_account = async (req, res, next) => {
// now update our cache
session.allowNonPrimary = data.allow;
session.strictIPCheck = data.strict;
- session.primaryIdentity = data.primary;
for (const ident of session.identities) {
- if (ident.id === session.primaryIdentity) {
- ident.primary = true;
- } else if (ident.primary === true) {
- ident.primary = false;
+ if (ident.id === session.primaryIdentity.id) {
+ ident.isPrimary = true;
+ session.primaryIdentity = ident;
+ } else if (ident.isPrimary === true) {
+ ident.isPrimary = false;
}
}
diff --git a/src/routers/vault/middlewares/identity.js b/src/routers/vault/middlewares/identity.js
index f65d757..3cae9e5 100644
--- a/src/routers/vault/middlewares/identity.js
+++ b/src/routers/vault/middlewares/identity.js
@@ -3,6 +3,7 @@ const uuidv4 = require("uuid/v4");
const nodemailer = require("nodemailer");
const Claim = require("../utils/claim.js");
const validate = require("../utils/validate.js");
+const Identity = require("../types/Identity.js");
let transporter = nodemailer.createTransport({
sendmail: true,
@@ -19,18 +20,14 @@ const get_identities = async (req, res, next) => {
if (session.identities.length === 0) {
console.info(`Vault.identity: fetching identities <${session.vault}@vault> [${req.ip}]`);
+ /** @type {Identity[]} */
const rows = await req.app.locals.vault.identity.findAll({
where: {userId: session.vault}
});
- for (const row of rows) {
- session.identities.push({
- // TODO: make this a class!
- id: row.id,
- email: row.email,
- added: row.addedDate,
- primary: session.primaryIdentity === row.id,
- });
+ for (const ident of rows) {
+ ident.isPrimary = session.primaryIdentity.id === ident.id;
+ session.identities.push(ident);
}
}
@@ -55,6 +52,7 @@ const add_identity = async (req, res, next) => {
return;
}
+ // TODO: make an IdentityStore type similar to SessionStore and get rid of Ephemeral
const ident = req.app.locals.identity_pending.get(secret);
if (ident === null || ident === undefined) {
@@ -73,6 +71,7 @@ const add_identity = async (req, res, next) => {
return;
}
+ /** @type {Identity} */
const newIdent = await req.app.locals.vault.identity.create({
userId: ident.vault,
email: ident.email,
@@ -87,16 +86,11 @@ const add_identity = async (req, res, next) => {
await Claim.claim_accounts(req, ident.email, ident.vault);
+ /** @type {Session} */
let session = null;
- for (const [key, sess] of req.app.locals.session) {
+ for (const [, sess] of req.app.locals.session) {
if (sess.vault === ident.vault && sess.authenticated) {
- sess.identities.push({
- // TODO: make this a class!
- id: newIdent.id,
- email: newIdent.email,
- added: newIdent.addedDate,
- primary: false,
- });
+ sess.identities.push(newIdent);
session = sess;
break;
}
@@ -119,18 +113,14 @@ const add_identity = async (req, res, next) => {
// request to add
- let session;
+ let session, email;
try {
[, session] = validate.get_session(req, res);
- } catch { return } // already handled
-
- let email;
- try {
email = validate.get_email(req, res);
} catch { return } // already handled
- for (const [key, pending] of req.app.locals.identity_pending) {
+ for (const [, pending] of req.app.locals.identity_pending) {
if (pending.vault === session.vault && pending.email === email) {
res.status(425).json({
status: "error",
@@ -141,24 +131,15 @@ const add_identity = async (req, res, next) => {
}
}
- const find = await req.app.locals.vault.identity.findOne({
- where: {email}
- });
-
- if (find !== null) {
+ if (session.identities.length === 0) {
+ // we did not have enough time to fetch, so cowardly refuse
res.status(409).json({
status: "error",
error: "already assigned",
});
req.app.locals.cooldown(req, 5e3);
return;
- }
-
- const count = await req.app.locals.vault.identity.count({
- where: {userId: session.vault}
- });
-
- if (count >= 20) {
+ } else if (session.identities.length >= 20) {
res.status(416).json({
status: "error",
error: "too many identities",
@@ -167,6 +148,31 @@ const add_identity = async (req, res, next) => {
return;
}
+ /** @type {Identity} */
+ let find = null;
+
+ for (const ident of session.identities) {
+ if (ident.email === email) {
+ find = ident;
+ break;
+ }
+ }
+
+ if (find === null) {
+ find = await req.app.locals.vault.identity.findOne({
+ where: {email}
+ });
+ }
+
+ if (find !== null) {
+ res.status(409).json({
+ status: "error",
+ error: "already assigned",
+ });
+ req.app.locals.cooldown(req, 5e3);
+ return;
+ }
+
let uuid;
do { // avoid collisions
uuid = uuidv4();
diff --git a/src/routers/vault/middlewares/session.js b/src/routers/vault/middlewares/session.js
index a961840..c9b7e13 100644
--- a/src/routers/vault/middlewares/session.js
+++ b/src/routers/vault/middlewares/session.js
@@ -5,6 +5,7 @@ const Claim = require("../utils/claim.js");
const Session = require("../types/Session.js");
const game_accounts = require("../utils/game_accounts.js");
const validate = require("../utils/validate.js");
+const Identity = require("../types/Identity.js");
let transporter = nodemailer.createTransport({
sendmail: true,
@@ -131,6 +132,7 @@ const auth_session = async (req, res) => {
ip: req.app.locals.sequelize.vault.fn("INET6_ATON", req.ip),
});
+ /** @type {Identity} */
const ident = await req.app.locals.vault.identity.create({
userId: user.id,
email: session.email,
@@ -151,16 +153,11 @@ const auth_session = async (req, res) => {
// update current session
session.vault = user.id;
- session.identity = ident.id;
- session.primaryIdentity = ident.id;
+ session.identity = ident;
+ session.primaryIdentity = ident;
session.allowNonPrimary = user.allowNonPrimary;
session.strictIPCheck = user.strictIPCheck;
- session.identities = [{
- // TODO: make this a class!
- email: ident.email,
- added: ident.addedDate,
- primary: true,
- }];
+ session.identities.push(ident);
} else {
if (session.identity !== session.primaryIdentity && !session.allowNonPrimary) {
// unexpected: a session was created when it shouldn't have been
@@ -202,13 +199,11 @@ const auth_session = async (req, res) => {
if (session.identity !== session.primaryIdentity) {
// user did not log in with their primary identity
- const primary = await req.app.locals.vault.identity.findByPk(session.primaryIdentity);
-
- if (primary === null || primary === undefined) {
+ if (session.primaryIdentity === null || session.primaryIdentity === 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}@vault> [${req.ip}]`);
await req.app.locals.vault.login.update({
- primaryIdentity: session.identity,
+ primaryIdentity: session.identity.id,
}, {where: {
id: session.vault,
}});
@@ -216,7 +211,7 @@ const auth_session = async (req, res) => {
} else {
transporter.sendMail({
from: process.env.VAULT__MAILER__FROM,
- to: primary.email,
+ to: session.primaryIdentity.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" +
@@ -236,7 +231,7 @@ const auth_session = async (req, res) => {
key: new_uuid,
secret: session.secret, // give them the session secret (only shared once)
expires: session.expires,
- identity: session.identity,
+ identity: session.identity.id,
},
});
};
@@ -247,6 +242,7 @@ const new_session = async (req, res, next) => {
email = validate.get_email(req, res);
} catch { return } // already handled
+ /** @type {Identity} */
const identity = await req.app.locals.vault.identity.findOne({where: {email: email}});
if (identity === null) {
@@ -305,7 +301,7 @@ const new_session = async (req, res, next) => {
return;
}
} else {
- const account = await req.app.locals.vault.login.findOne({where: {id: identity.userId}});
+ const account = await req.app.locals.vault.login.findByPk(identity.userId);
if (account === null) {
// unexpected: the account was deleted but not its identities
console.log(`Vault.session: removing dangling identity [${req.ip}]`);
@@ -317,13 +313,25 @@ const new_session = async (req, res, next) => {
req.app.locals.cooldown(req, 3e5);
return;
} else {
+ /** @type {Identity} */
+ let primary = null;
+
+ if (identity.id !== account.primaryIdentity) {
+ try {
+ primary = await req.app.locals.vault.identity.findByPk(account.primaryIdentity);
+ } catch {}
+ } else {
+ primary = identity;
+ }
+
// auth flow
- if (account.primaryIdentity === null || account.primaryIdentity === undefined) {
+ if (primary === null) {
// the vault account has no primary identity (bug): let's fix this
console.warn(`Vault.session: fixing account with no primary identity <${account.id}@vault> [${req.ip}]`);
account.primaryIdentity = identity.id;
+ primary = identity;
await account.save();
- } else if (identity.id !== account.primaryIdentity && !account.allowNonPrimary) {
+ } else if (identity.id !== primary.id && !account.allowNonPrimary) {
res.status(423).json({
status: "error",
error: "non-primary login is disabled",
@@ -341,10 +349,10 @@ const new_session = async (req, res, next) => {
const session = new Session(req.ip, email);
session.vault = account.id;
- session.primaryIdentity = account.primaryIdentity;
+ session.primaryIdentity = primary;
session.allowNonPrimary = account.allowNonPrimary;
session.strictIPCheck = account.strictIPCheck;
- session.identity = identity.id;
+ session.identity = identity;
req.app.locals.session.set(uuid, session);
console.log(`Vault.session: starting authentication with identity ${identity.id} [${req.ip}]`);
diff --git a/src/routers/vault/types/Identity.js b/src/routers/vault/types/Identity.js
new file mode 100644
index 0000000..fb5171f
--- /dev/null
+++ b/src/routers/vault/types/Identity.js
@@ -0,0 +1,58 @@
+"use strict";
+
+const { Sequelize, Model } = require("sequelize");
+
+class Identity extends Model {
+ /**
+ * primary key
+ * @type {number}
+ */
+ //id;
+ /**
+ * the Date when the Identity was confirmed
+ * @type {Date}
+ */
+ //addedDate;
+ /**
+ * the email address of the identity
+ * @type {string}
+ */
+ //email;
+ /**
+ * the Vault user id
+ * @type {number}
+ */
+ //userId;
+
+ /**
+ * whether it is the primary identity of the vault account
+ * @virtual
+ */
+ isPrimary = false;
+
+
+ /**
+ * initialize the model (must be called prior to first use)
+ * @param {Sequelize} sequelize - the Sequelize instance
+ */
+ static define (sequelize) {
+ const {fields, options} = require("../models/vault/identity.js");
+ Identity.init(fields, { sequelize, ...options });
+
+ return Identity; // the instantiated Model
+ }
+
+ /**
+ * serialize for sending over the network
+ */
+ toJSON () {
+ return {
+ id: this.id,
+ email: this.email,
+ added: this.addedDate,
+ primary: this.isPrimary,
+ };
+ }
+}
+
+module.exports = Identity;
diff --git a/src/routers/vault/types/Session.js b/src/routers/vault/types/Session.js
index 17c77ef..59737b3 100644
--- a/src/routers/vault/types/Session.js
+++ b/src/routers/vault/types/Session.js
@@ -1,34 +1,72 @@
const uuidv4 = require("uuid/v4");
+const Identity = require("./Identity.js");
+const EvolAccount = require("./EvolAccount.js");
+const LegacyAccount = require("./LegacyAccount.js");
/**
* holds a cache of all the user data fetched from SQL
*/
module.exports = class Session {
- /** expiry Date */
+ /**
+ * expiry date
+ */
expires = new Date();
- /** Vault account id */
+ /**
+ * Vault account id
+ * @type {number}
+ */
vault = null;
- /** whether the user logged in */
+ /**
+ * whether the user is properly authenticated
+ */
authenticated = false;
- /** the identity that was used to log in */
+ /**
+ * the identity that was used to log in
+ * @type {Identity}
+ */
identity = null;
- /** the email address of the identity that was used to log in */
+ /**
+ * the email address of the identity that was used to log in
+ * @type {string}
+ */
email;
- /** the secret that is sent after authentication */
+ /**
+ * the secret that is sent once to the client after authentication
+ * @type {string}
+ */
secret;
- /** cache holding all identities */
+ /**
+ * cache holding all identities
+ * @type {Identity[]}
+ */
identities = [];
- /** the main identity of the account */
+ /**
+ * id of the main identity of the account
+ * @type {number}
+ */
primaryIdentity = null;
- /** whether to allow logging in with a non-primary ident */
+ /**
+ * whether to allow logging in with a non-primary ident
+ */
allowNonPrimary = true;
- /** LegacyAccount[] cache holding all legacy game accounts */
+ /**
+ * cache holding all legacy game accounts
+ * @type {LegacyAccount[]}
+ */
legacyAccounts = [];
- /** EvolAccount[] cache holding all evol game accounts */
+ /**
+ * cache holding all evol game accounts
+ * @type {EvolAccount[]}
+ */
gameAccounts = [];
- /** ip that was used to init the session */
+ /**
+ * ip that was used to init the session
+ * @type {string}
+ */
ip;
- /** refuse to authenticate a session with a different IP */
+ /**
+ * refuse to authenticate a session with a different IP
+ */
strictIPCheck = true;
constructor (ip, email) {
@@ -44,7 +82,19 @@ module.exports = class Session {
toJSON (key) {
return {
expires: this.expires,
- identity: this.identity,
+ identity: this.identity.id,
}
}
+
+ /**
+ * serialize the account settings for sending over the network
+ */
+ getAccountData () {
+ return {
+ primaryIdentity: this.primaryIdentity.id,
+ allowNonPrimary: this.allowNonPrimary,
+ strictIPCheck: this.strictIPCheck,
+ vaultId: this.vault,
+ };
+ }
}
diff --git a/src/routers/vault/types/SessionStore.js b/src/routers/vault/types/SessionStore.js
index daa71c3..2657551 100644
--- a/src/routers/vault/types/SessionStore.js
+++ b/src/routers/vault/types/SessionStore.js
@@ -3,7 +3,7 @@ const Session = require("./Session.js");
/**
* we store the timeout directly in Session instances
- * @type Symbol("session timeout")
+ * @type {Symbol("session timeout")}
*/
const timeout_symbol = Symbol("session timeout");
@@ -13,7 +13,7 @@ const timeout_symbol = Symbol("session timeout");
class SessionStore {
/**
* a Map of all Session instances
- * @type Map<string, Session>
+ * @type {Map<string, Session>}
*/
sessions = new Map();
/** lifetime of an unauthenticated Session, in minutes */