From 757b93105d4fff6910f0957916d97ba7ae5eb81b Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 3 Jun 2016 04:34:18 +0300 Subject: [PATCH 01/38] feat(Adapter Class): The realisation of adapter class and Redis data storage --- src/db/adapter.js | 3 +++ src/db/redisstorage.js | 3 +++ src/db/sandbox/sandbox_activate.js | 33 ++++++++++++++++++++++++++++++ src/db/sandbox/sandbox_ban.js | 19 +++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 src/db/adapter.js create mode 100644 src/db/redisstorage.js create mode 100644 src/db/sandbox/sandbox_activate.js create mode 100644 src/db/sandbox/sandbox_ban.js diff --git a/src/db/adapter.js b/src/db/adapter.js new file mode 100644 index 000000000..219bc487a --- /dev/null +++ b/src/db/adapter.js @@ -0,0 +1,3 @@ +/** + * Created by Stainwoortsel on 30.05.2016. + */ diff --git a/src/db/redisstorage.js b/src/db/redisstorage.js new file mode 100644 index 000000000..efe55d598 --- /dev/null +++ b/src/db/redisstorage.js @@ -0,0 +1,3 @@ +/** + * Created by Stainwoortsel on 31.05.2016. + */ diff --git a/src/db/sandbox/sandbox_activate.js b/src/db/sandbox/sandbox_activate.js new file mode 100644 index 000000000..78a489d7e --- /dev/null +++ b/src/db/sandbox/sandbox_activate.js @@ -0,0 +1,33 @@ +/** + * Created by Stainwoortsel on 30.05.2016. + */ +const Promise = require('bluebird'); +const emailVerification = require('../utils/send-email.js'); +const jwt = require('../utils/jwt.js'); +const Adapter = require('./redisadapter'); + +module.exports = function verifyChallenge(opts) { + // TODO: add security logs + // var remoteip = opts.remoteip; + const { token, namespace, username } = opts; + const { redis, config } = this; + const audience = opts.audience || config.defaultAudience; + + const users = new Adapter(); + + function verifyToken() { + return emailVerification.verify.call(this, token, namespace, config.validation.ttl > 0); + } + + function hook(user) { + return this.hook.call(this, 'users:activate', user, audience); + } + + return Promise + .bind(this, username) + .then(username ? users.userExists : verifyToken) + .tap(users.activateAccount) + .tap(hook) + .then(user => [user, audience]) + .spread(jwt.login); +}; diff --git a/src/db/sandbox/sandbox_ban.js b/src/db/sandbox/sandbox_ban.js new file mode 100644 index 000000000..ead6ba004 --- /dev/null +++ b/src/db/sandbox/sandbox_ban.js @@ -0,0 +1,19 @@ +/** + * Created by Stainwoortsel on 30.05.2016. + */ +const Promise = require('bluebird'); +const Adapter = require('./redisadapter'); +const users = new Adapter(); + +/** + * Bans/unbans existing user + * @param {Object} opts + * @return {Promise} + */ +module.exports = function banUser(opts) { + return Promise + .bind(this, opts.username) + .then(users.userExists) + .then(username => ({ ...opts, username })) + .then(opts.ban ? users.lockUser : users.unlockUser); +}; From 76cc264f8694d3a4b7167985fc3c318eae4fdef1 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 3 Jun 2016 12:20:19 +0300 Subject: [PATCH 02/38] feat(actions, adapter, redisstorage): Updating few actions with new schema --- src/db/sandbox/sandbox_activate.js | 33 ------------------------------ src/db/sandbox/sandbox_ban.js | 19 ----------------- 2 files changed, 52 deletions(-) delete mode 100644 src/db/sandbox/sandbox_activate.js delete mode 100644 src/db/sandbox/sandbox_ban.js diff --git a/src/db/sandbox/sandbox_activate.js b/src/db/sandbox/sandbox_activate.js deleted file mode 100644 index 78a489d7e..000000000 --- a/src/db/sandbox/sandbox_activate.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Created by Stainwoortsel on 30.05.2016. - */ -const Promise = require('bluebird'); -const emailVerification = require('../utils/send-email.js'); -const jwt = require('../utils/jwt.js'); -const Adapter = require('./redisadapter'); - -module.exports = function verifyChallenge(opts) { - // TODO: add security logs - // var remoteip = opts.remoteip; - const { token, namespace, username } = opts; - const { redis, config } = this; - const audience = opts.audience || config.defaultAudience; - - const users = new Adapter(); - - function verifyToken() { - return emailVerification.verify.call(this, token, namespace, config.validation.ttl > 0); - } - - function hook(user) { - return this.hook.call(this, 'users:activate', user, audience); - } - - return Promise - .bind(this, username) - .then(username ? users.userExists : verifyToken) - .tap(users.activateAccount) - .tap(hook) - .then(user => [user, audience]) - .spread(jwt.login); -}; diff --git a/src/db/sandbox/sandbox_ban.js b/src/db/sandbox/sandbox_ban.js deleted file mode 100644 index ead6ba004..000000000 --- a/src/db/sandbox/sandbox_ban.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Created by Stainwoortsel on 30.05.2016. - */ -const Promise = require('bluebird'); -const Adapter = require('./redisadapter'); -const users = new Adapter(); - -/** - * Bans/unbans existing user - * @param {Object} opts - * @return {Promise} - */ -module.exports = function banUser(opts) { - return Promise - .bind(this, opts.username) - .then(users.userExists) - .then(username => ({ ...opts, username })) - .then(opts.ban ? users.lockUser : users.unlockUser); -}; From fd2a23fe4ffa1b1ce32b8a62029566c440070fa0 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 3 Jun 2016 13:02:25 +0300 Subject: [PATCH 03/38] feat(adapter, redisstorage, actions): Making abstraction layer between Redis and the whole code --- .gitignore | 3 + rm.txt | 1 + src/actions/activate-compiled.js | 26 + src/actions/activate-compiled.js.map | 1 + src/actions/activate.js | 31 +- src/actions/alias-compiled.js | 26 + src/actions/alias-compiled.js.map | 1 + src/actions/alias.js | 26 +- src/actions/ban-compiled.js | 30 + src/actions/ban-compiled.js.map | 1 + src/actions/ban.js | 46 +- src/actions/challenge-compiled.js | 17 + src/actions/challenge-compiled.js.map | 1 + src/actions/challenge.js | 7 +- src/actions/getInternalData-compiled.js | 15 + src/actions/getInternalData-compiled.js.map | 1 + src/actions/getInternalData.js | 4 +- src/actions/getMetadata-compiled.js | 13 + src/actions/getMetadata-compiled.js.map | 1 + src/actions/getMetadata.js | 22 +- src/actions/list-compiled.js | 22 + src/actions/list-compiled.js.map | 1 + src/actions/list.js | 65 +- src/db/adapter-compiled.js | 366 ++++++++++ src/db/adapter-compiled.js.map | 1 + src/db/adapter.js | 365 ++++++++++ src/db/redisstorage-compiled.js | 605 ++++++++++++++++ src/db/redisstorage-compiled.js.map | 1 + src/db/redisstorage.js | 669 +++++++++++++++++- src/users.js | 4 +- src/utils/getInternalData.js | 2 +- ...256-compiled-compiled-compiled-compiled.js | 19 + ...compiled-compiled-compiled-compiled.js.map | 1 + .../sha256-compiled-compiled-compiled.js | 17 + .../sha256-compiled-compiled-compiled.js.map | 1 + src/utils/sha256-compiled-compiled.js | 15 + src/utils/sha256-compiled-compiled.js.map | 1 + src/utils/sha256-compiled.js | 13 + src/utils/sha256-compiled.js.map | 1 + test/suites/updateMetadata-compiled.js | 80 +++ test/suites/updateMetadata-compiled.js.map | 1 + 41 files changed, 2362 insertions(+), 161 deletions(-) create mode 100644 rm.txt create mode 100644 src/actions/activate-compiled.js create mode 100644 src/actions/activate-compiled.js.map create mode 100644 src/actions/alias-compiled.js create mode 100644 src/actions/alias-compiled.js.map create mode 100644 src/actions/ban-compiled.js create mode 100644 src/actions/ban-compiled.js.map create mode 100644 src/actions/challenge-compiled.js create mode 100644 src/actions/challenge-compiled.js.map create mode 100644 src/actions/getInternalData-compiled.js create mode 100644 src/actions/getInternalData-compiled.js.map create mode 100644 src/actions/getMetadata-compiled.js create mode 100644 src/actions/getMetadata-compiled.js.map create mode 100644 src/actions/list-compiled.js create mode 100644 src/actions/list-compiled.js.map create mode 100644 src/db/adapter-compiled.js create mode 100644 src/db/adapter-compiled.js.map create mode 100644 src/db/redisstorage-compiled.js create mode 100644 src/db/redisstorage-compiled.js.map create mode 100644 src/utils/sha256-compiled-compiled-compiled-compiled.js create mode 100644 src/utils/sha256-compiled-compiled-compiled-compiled.js.map create mode 100644 src/utils/sha256-compiled-compiled-compiled.js create mode 100644 src/utils/sha256-compiled-compiled-compiled.js.map create mode 100644 src/utils/sha256-compiled-compiled.js create mode 100644 src/utils/sha256-compiled-compiled.js.map create mode 100644 src/utils/sha256-compiled.js create mode 100644 src/utils/sha256-compiled.js.map create mode 100644 test/suites/updateMetadata-compiled.js create mode 100644 test/suites/updateMetadata-compiled.js.map diff --git a/.gitignore b/.gitignore index c9409dfc6..0d529648e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ node_modules # compiled source code lib .env + +src/db/sandbox/ +.idea diff --git a/rm.txt b/rm.txt new file mode 100644 index 000000000..ef3d63228 --- /dev/null +++ b/rm.txt @@ -0,0 +1 @@ +test/docker.sh customizing testable files \ No newline at end of file diff --git a/src/actions/activate-compiled.js b/src/actions/activate-compiled.js new file mode 100644 index 000000000..3cfc743f7 --- /dev/null +++ b/src/actions/activate-compiled.js @@ -0,0 +1,26 @@ +'use strict'; + +const Promise = require('bluebird'); +const emailVerification = require('../utils/send-email.js'); +const jwt = require('../utils/jwt.js'); +const Users = require('../db/adapter'); + +module.exports = function verifyChallenge(opts) { + // TODO: add security logs + // var remoteip = opts.remoteip; + const { token, namespace, username } = opts; + const { config } = this; + const audience = opts.audience || config.defaultAudience; + + function verifyToken() { + return emailVerification.verify.call(this, token, namespace, config.validation.ttl > 0); + } + + function hook(user) { + return this.hook.call(this, 'users:activate', user, audience); + } + + return Promise.bind(this, username).then(username ? Users.isExists : verifyToken).tap(Users.activateAccount).tap(hook).then(user => [user, audience]).spread(jwt.login); +}; + +//# sourceMappingURL=activate-compiled.js.map \ No newline at end of file diff --git a/src/actions/activate-compiled.js.map b/src/actions/activate-compiled.js.map new file mode 100644 index 000000000..d0f990778 --- /dev/null +++ b/src/actions/activate-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["activate.js"],"names":[],"mappings":";;AAAA,MAAM,UAAU,QAAQ,UAAR,CAAV;AACN,MAAM,oBAAoB,QAAQ,wBAAR,CAApB;AACN,MAAM,MAAM,QAAQ,iBAAR,CAAN;AACN,MAAM,QAAQ,QAAQ,eAAR,CAAR;;AAEN,OAAO,OAAP,GAAiB,SAAS,eAAT,CAAyB,IAAzB,EAA+B;;;AAG9C,QAAM,EAAE,KAAF,EAAS,SAAT,EAAoB,QAApB,KAAiC,IAAjC,CAHwC;AAI9C,QAAM,EAAE,MAAF,KAAa,IAAb,CAJwC;AAK9C,QAAM,WAAW,KAAK,QAAL,IAAiB,OAAO,eAAP,CALY;;AAO9C,WAAS,WAAT,GAAuB;AACrB,WAAO,kBAAkB,MAAlB,CAAyB,IAAzB,CAA8B,IAA9B,EAAoC,KAApC,EAA2C,SAA3C,EAAsD,OAAO,UAAP,CAAkB,GAAlB,GAAwB,CAAxB,CAA7D,CADqB;GAAvB;;AAIA,WAAS,IAAT,CAAc,IAAd,EAAoB;AAClB,WAAO,KAAK,IAAL,CAAU,IAAV,CAAe,IAAf,EAAqB,gBAArB,EAAuC,IAAvC,EAA6C,QAA7C,CAAP,CADkB;GAApB;;AAIA,SAAO,QACJ,IADI,CACC,IADD,EACO,QADP,EAEJ,IAFI,CAEC,WAAW,MAAM,QAAN,GAAiB,WAA5B,CAFD,CAGJ,GAHI,CAGA,MAAM,eAAN,CAHA,CAIJ,GAJI,CAIA,IAJA,EAKJ,IALI,CAKC,QAAQ,CAAC,IAAD,EAAO,QAAP,CAAR,CALD,CAMJ,MANI,CAMG,IAAI,KAAJ,CANV,CAf8C;CAA/B","file":"activate-compiled.js","sourcesContent":["const Promise = require('bluebird');\nconst emailVerification = require('../utils/send-email.js');\nconst jwt = require('../utils/jwt.js');\nconst Users = require('../db/adapter');\n\nmodule.exports = function verifyChallenge(opts) {\n // TODO: add security logs\n // var remoteip = opts.remoteip;\n const { token, namespace, username } = opts;\n const { config } = this;\n const audience = opts.audience || config.defaultAudience;\n\n function verifyToken() {\n return emailVerification.verify.call(this, token, namespace, config.validation.ttl > 0);\n }\n\n function hook(user) {\n return this.hook.call(this, 'users:activate', user, audience);\n }\n\n return Promise\n .bind(this, username)\n .then(username ? Users.isExists : verifyToken)\n .tap(Users.activateAccount)\n .tap(hook)\n .then(user => [user, audience])\n .spread(jwt.login);\n};\n"]} \ No newline at end of file diff --git a/src/actions/activate.js b/src/actions/activate.js index 2c6af2c25..136d9f066 100644 --- a/src/actions/activate.js +++ b/src/actions/activate.js @@ -1,50 +1,27 @@ const Promise = require('bluebird'); -const Errors = require('common-errors'); -const redisKey = require('../utils/key.js'); const emailVerification = require('../utils/send-email.js'); const jwt = require('../utils/jwt.js'); -const userExists = require('../utils/userExists.js'); -const { USERS_INDEX, USERS_DATA, USERS_ACTIVE_FLAG } = require('../constants.js'); +const Users = require('../db/adapter'); module.exports = function verifyChallenge(opts) { // TODO: add security logs // var remoteip = opts.remoteip; const { token, namespace, username } = opts; - const { redis, config } = this; + const { config } = this; const audience = opts.audience || config.defaultAudience; function verifyToken() { return emailVerification.verify.call(this, token, namespace, config.validation.ttl > 0); } - function activateAccount(user) { - const userKey = redisKey(user, USERS_DATA); - - // WARNING: `persist` is very important, otherwise we will lose user's information in 30 days - // set to active & persist - return redis - .pipeline() - .hget(userKey, USERS_ACTIVE_FLAG) - .hset(userKey, USERS_ACTIVE_FLAG, 'true') - .persist(userKey) - .sadd(USERS_INDEX, user) - .exec() - .spread(function pipeResponse(isActive) { - const status = isActive[1]; - if (status === 'true') { - throw new Errors.HttpStatusError(417, `Account ${user} was already activated`); - } - }); - } - function hook(user) { return this.hook.call(this, 'users:activate', user, audience); } return Promise .bind(this, username) - .then(username ? userExists : verifyToken) - .tap(activateAccount) + .then(username ? Users.isExists : verifyToken) + .tap(Users.activateAccount) .tap(hook) .then(user => [user, audience]) .spread(jwt.login); diff --git a/src/actions/alias-compiled.js b/src/actions/alias-compiled.js new file mode 100644 index 000000000..2728b1171 --- /dev/null +++ b/src/actions/alias-compiled.js @@ -0,0 +1,26 @@ +'use strict'; + +const Promise = require('bluebird'); +const Errors = require('common-errors'); + +const Users = require('../db/adapter'); + +module.exports = function assignAlias(opts) { + const { username, alias } = opts; + + return Promise.bind(this, username).then(Users.getUser).tap(Users.isActive).tap(Users.isBanned).then(data => { + if (Users.isAliasAssigned(data)) { + throw new Errors.HttpStatusError(417, 'alias is already assigned'); + } + + return Users.storeAlias(username, alias); + }).then(assigned => { + if (assigned === 0) { + throw new Errors.HttpStatusError(409, 'alias was already taken'); + } + + return Users.assignAlias(username, alias); + }); +}; + +//# sourceMappingURL=alias-compiled.js.map \ No newline at end of file diff --git a/src/actions/alias-compiled.js.map b/src/actions/alias-compiled.js.map new file mode 100644 index 000000000..fa2ea9106 --- /dev/null +++ b/src/actions/alias-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["alias.js"],"names":[],"mappings":";;AAAA,MAAM,UAAU,QAAQ,UAAR,CAAV;AACN,MAAM,SAAS,QAAQ,eAAR,CAAT;;AAEN,MAAM,QAAQ,QAAQ,eAAR,CAAR;;AAGN,OAAO,OAAP,GAAiB,SAAS,WAAT,CAAqB,IAArB,EAA2B;AAC1C,QAAM,EAAE,QAAF,EAAY,KAAZ,KAAsB,IAAtB,CADoC;;AAG1C,SAAO,QACJ,IADI,CACC,IADD,EACO,QADP,EAEJ,IAFI,CAEC,MAAM,OAAN,CAFD,CAGJ,GAHI,CAGA,MAAM,QAAN,CAHA,CAIJ,GAJI,CAIA,MAAM,QAAN,CAJA,CAKJ,IALI,CAKC,QAAQ;AACZ,QAAI,MAAM,eAAN,CAAsB,IAAtB,CAAJ,EAAiC;AAC/B,YAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,2BAAhC,CAAN,CAD+B;KAAjC;;AAIA,WAAO,MAAM,UAAN,CAAiB,QAAjB,EAA2B,KAA3B,CAAP,CALY;GAAR,CALD,CAYJ,IAZI,CAYC,YAAY;AAChB,QAAI,aAAa,CAAb,EAAgB;AAClB,YAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,yBAAhC,CAAN,CADkB;KAApB;;AAIA,WAAO,MAAM,WAAN,CAAkB,QAAlB,EAA4B,KAA5B,CAAP,CALgB;GAAZ,CAZR,CAH0C;CAA3B","file":"alias-compiled.js","sourcesContent":["const Promise = require('bluebird');\nconst Errors = require('common-errors');\n\nconst Users = require('../db/adapter');\n\n\nmodule.exports = function assignAlias(opts) {\n const { username, alias } = opts;\n\n return Promise\n .bind(this, username)\n .then(Users.getUser)\n .tap(Users.isActive)\n .tap(Users.isBanned)\n .then(data => {\n if (Users.isAliasAssigned(data)) {\n throw new Errors.HttpStatusError(417, 'alias is already assigned');\n }\n\n return Users.storeAlias(username, alias);\n })\n .then(assigned => {\n if (assigned === 0) {\n throw new Errors.HttpStatusError(409, 'alias was already taken');\n }\n\n return Users.assignAlias(username, alias);\n });\n};\n"]} \ No newline at end of file diff --git a/src/actions/alias.js b/src/actions/alias.js index 08f4cbe7c..be7a7b932 100644 --- a/src/actions/alias.js +++ b/src/actions/alias.js @@ -1,37 +1,29 @@ const Promise = require('bluebird'); const Errors = require('common-errors'); -const getInternalData = require('../utils/getInternalData.js'); -const isActive = require('../utils/isActive.js'); -const isBanned = require('../utils/isBanned.js'); -const key = require('../utils/key.js'); -const { USERS_DATA, USERS_METADATA, USERS_PUBLIC_INDEX, USERS_ALIAS_TO_LOGIN, USERS_ALIAS_FIELD } = require('../constants.js'); + +const Users = require('../db/adapter'); + module.exports = function assignAlias(opts) { - const { redis, config: { jwt: { defaultAudience } } } = this; const { username, alias } = opts; return Promise .bind(this, username) - .then(getInternalData) - .tap(isActive) - .tap(isBanned) + .then(Users.getUser) + .tap(Users.isActive) + .tap(Users.isBanned) .then(data => { - if (data[USERS_ALIAS_FIELD]) { + if (Users.isAliasAssigned(data)) { throw new Errors.HttpStatusError(417, 'alias is already assigned'); } - return redis.hsetnx(USERS_ALIAS_TO_LOGIN, alias, username); + return Users.storeAlias(username, alias); }) .then(assigned => { if (assigned === 0) { throw new Errors.HttpStatusError(409, 'alias was already taken'); } - return redis - .pipeline() - .sadd(USERS_PUBLIC_INDEX, username) - .hset(key(username, USERS_DATA), USERS_ALIAS_FIELD, alias) - .hset(key(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, JSON.stringify(alias)) - .exec(); + return Users.assignAlias(username, alias); }); }; diff --git a/src/actions/ban-compiled.js b/src/actions/ban-compiled.js new file mode 100644 index 000000000..1cdec20d0 --- /dev/null +++ b/src/actions/ban-compiled.js @@ -0,0 +1,30 @@ +'use strict'; + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +const Promise = require('bluebird'); +const Users = require('../db/adapter'); + +function lockUser({ username, reason, whom, remoteip }) { + return Users.lockUser({ + username, + reason: reason || '', + whom: whom || '', + remoteip: remoteip || '' + }); +} + +function unlockUser({ username }) { + return Users.unlockUser({ username }); +} + +/** + * Bans/unbans existing user + * @param {Object} opts + * @return {Promise} + */ +module.exports = function banUser(opts) { + return Promise.bind(this, opts.username).then(Users.isExists).then(username => _extends({}, opts, { username })).then(opts.ban ? lockUser : unlockUser); +}; + +//# sourceMappingURL=ban-compiled.js.map \ No newline at end of file diff --git a/src/actions/ban-compiled.js.map b/src/actions/ban-compiled.js.map new file mode 100644 index 000000000..4a9302ddc --- /dev/null +++ b/src/actions/ban-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["ban.js"],"names":[],"mappings":";;;;AAAA,MAAM,UAAU,QAAQ,UAAR,CAAV;AACN,MAAM,QAAQ,QAAQ,eAAR,CAAR;;AAEN,SAAS,QAAT,CAAkB,EAAE,QAAF,EAAY,MAAZ,EAAoB,IAApB,EAA0B,QAA1B,EAAlB,EAAwD;AACtD,SAAO,MAAM,QAAN,CAAe;AACpB,YADoB;AAEpB,YAAQ,UAAU,EAAV;AACR,UAAM,QAAQ,EAAR;AACN,cAAU,YAAY,EAAZ;GAJL,CAAP,CADsD;CAAxD;;AASA,SAAS,UAAT,CAAoB,EAAE,QAAF,EAApB,EAAkC;AAChC,SAAO,MAAM,UAAN,CAAiB,EAAC,QAAD,EAAjB,CAAP,CADgC;CAAlC;;;;;;;AASA,OAAO,OAAP,GAAiB,SAAS,OAAT,CAAiB,IAAjB,EAAuB;AACtC,SAAO,QACJ,IADI,CACC,IADD,EACO,KAAK,QAAL,CADP,CAEJ,IAFI,CAEC,MAAM,QAAN,CAFD,CAGJ,IAHI,CAGC,yBAAkB,QAAM,WAAxB,CAHD,CAIJ,IAJI,CAIC,KAAK,GAAL,GAAW,QAAX,GAAsB,UAAtB,CAJR,CADsC;CAAvB","file":"ban-compiled.js","sourcesContent":["const Promise = require('bluebird');\nconst Users = require('../db/adapter');\n\nfunction lockUser({ username, reason, whom, remoteip }) {\n return Users.lockUser({\n username,\n reason: reason || '',\n whom: whom || '',\n remoteip: remoteip || ''\n })\n}\n\nfunction unlockUser({ username }) {\n return Users.unlockUser({username});\n}\n\n/**\n * Bans/unbans existing user\n * @param {Object} opts\n * @return {Promise}\n */\nmodule.exports = function banUser(opts) {\n return Promise\n .bind(this, opts.username)\n .then(Users.isExists)\n .then(username => ({ ...opts, username }))\n .then(opts.ban ? lockUser : unlockUser);\n};\n"]} \ No newline at end of file diff --git a/src/actions/ban.js b/src/actions/ban.js index 0d8349b20..6d4a41783 100644 --- a/src/actions/ban.js +++ b/src/actions/ban.js @@ -1,45 +1,17 @@ const Promise = require('bluebird'); -const mapValues = require('lodash/mapValues'); -const stringify = JSON.stringify.bind(JSON); - -const redisKey = require('../utils/key.js'); -const userExists = require('../utils/userExists.js'); -const { - USERS_DATA, USERS_METADATA, - USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA, -} = require('../constants.js'); +const Users = require('../db/adapter'); function lockUser({ username, reason, whom, remoteip }) { - const { redis, config } = this; - const { jwt: { defaultAudience } } = config; - const data = { - banned: true, - [USERS_BANNED_DATA]: { - reason: reason || '', - whom: whom || '', - remoteip: remoteip || '', - }, - }; - - return redis - .pipeline() - .hset(redisKey(username, USERS_DATA), USERS_BANNED_FLAG, 'true') - // set .banned on metadata for filtering & sorting users by that field - .hmset(redisKey(username, USERS_METADATA, defaultAudience), mapValues(data, stringify)) - .del(redisKey(username, USERS_TOKENS)) - .exec(); + return Users.lockUser({ + username, + reason: reason || '', + whom: whom || '', + remoteip: remoteip || '' + }) } function unlockUser({ username }) { - const { redis, config } = this; - const { jwt: { defaultAudience } } = config; - - return redis - .pipeline() - .hdel(redisKey(username, USERS_DATA), USERS_BANNED_FLAG) - // remove .banned on metadata for filtering & sorting users by that field - .hdel(redisKey(username, USERS_METADATA, defaultAudience), 'banned', USERS_BANNED_DATA) - .exec(); + return Users.unlockUser({username}); } /** @@ -50,7 +22,7 @@ function unlockUser({ username }) { module.exports = function banUser(opts) { return Promise .bind(this, opts.username) - .then(userExists) + .then(Users.isExists) .then(username => ({ ...opts, username })) .then(opts.ban ? lockUser : unlockUser); }; diff --git a/src/actions/challenge-compiled.js b/src/actions/challenge-compiled.js new file mode 100644 index 000000000..7afbd77bc --- /dev/null +++ b/src/actions/challenge-compiled.js @@ -0,0 +1,17 @@ +'use strict'; + +const Promise = require('bluebird'); +const Errors = require('common-errors'); +const emailChallenge = require('../utils/send-email.js'); +const Users = require('../db/adapter'); + +module.exports = function sendChallenge(message) { + const { username } = message; + + // TODO: record all attemps + // TODO: add metadata processing on successful email challenge + + return Promise.bind(this, username).then(Users.getUser).tap(Users.isActive).throw(new Errors.HttpStatusError(417, `${ username } is already active`)).catchReturn({ statusCode: 412 }, username).then(emailChallenge.send); +}; + +//# sourceMappingURL=challenge-compiled.js.map \ No newline at end of file diff --git a/src/actions/challenge-compiled.js.map b/src/actions/challenge-compiled.js.map new file mode 100644 index 000000000..8141dec6d --- /dev/null +++ b/src/actions/challenge-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["challenge.js"],"names":[],"mappings":";;AAAA,MAAM,UAAU,QAAQ,UAAR,CAAV;AACN,MAAM,SAAS,QAAQ,eAAR,CAAT;AACN,MAAM,iBAAiB,QAAQ,wBAAR,CAAjB;AACN,MAAM,QAAQ,QAAQ,eAAR,CAAR;;AAEN,OAAO,OAAP,GAAiB,SAAS,aAAT,CAAuB,OAAvB,EAAgC;AAC/C,QAAM,EAAE,QAAF,KAAe,OAAf;;;;;AADyC,SAMxC,QACJ,IADI,CACC,IADD,EACO,QADP,EAEJ,IAFI,CAEC,MAAM,OAAN,CAFD,CAGJ,GAHI,CAGA,MAAM,QAAN,CAHA,CAIJ,KAJI,CAIE,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,CAAC,GAAE,QAAH,EAAY,kBAAZ,CAAhC,CAJF,EAKJ,WALI,CAKQ,EAAE,YAAY,GAAZ,EALV,EAK6B,QAL7B,EAMJ,IANI,CAMC,eAAe,IAAf,CANR,CAN+C;CAAhC","file":"challenge-compiled.js","sourcesContent":["const Promise = require('bluebird');\nconst Errors = require('common-errors');\nconst emailChallenge = require('../utils/send-email.js');\nconst Users = require('../db/adapter');\n\nmodule.exports = function sendChallenge(message) {\n const { username } = message;\n\n // TODO: record all attemps\n // TODO: add metadata processing on successful email challenge\n\n return Promise\n .bind(this, username)\n .then(Users.getUser)\n .tap(Users.isActive)\n .throw(new Errors.HttpStatusError(417, `${username} is already active`))\n .catchReturn({ statusCode: 412 }, username)\n .then(emailChallenge.send);\n};\n"]} \ No newline at end of file diff --git a/src/actions/challenge.js b/src/actions/challenge.js index b077151b1..8198f1546 100644 --- a/src/actions/challenge.js +++ b/src/actions/challenge.js @@ -1,8 +1,7 @@ const Promise = require('bluebird'); const Errors = require('common-errors'); const emailChallenge = require('../utils/send-email.js'); -const getInternalData = require('../utils/getInternalData.js'); -const isActive = require('../utils/isActive.js'); +const Users = require('../db/adapter'); module.exports = function sendChallenge(message) { const { username } = message; @@ -12,8 +11,8 @@ module.exports = function sendChallenge(message) { return Promise .bind(this, username) - .then(getInternalData) - .tap(isActive) + .then(Users.getUser) + .tap(Users.isActive) .throw(new Errors.HttpStatusError(417, `${username} is already active`)) .catchReturn({ statusCode: 412 }, username) .then(emailChallenge.send); diff --git a/src/actions/getInternalData-compiled.js b/src/actions/getInternalData-compiled.js new file mode 100644 index 000000000..915e7280b --- /dev/null +++ b/src/actions/getInternalData-compiled.js @@ -0,0 +1,15 @@ +'use strict'; + +const Promise = require('bluebird'); +const pick = require('lodash/pick'); +const Users = require('../db/adapter'); + +module.exports = function internalData(message) { + const { fields } = message; + + return Promise.bind(this, message.username).then(Users.getUser).then(data => { + return fields ? pick(data, fields) : data; + }); +}; + +//# sourceMappingURL=getInternalData-compiled.js.map \ No newline at end of file diff --git a/src/actions/getInternalData-compiled.js.map b/src/actions/getInternalData-compiled.js.map new file mode 100644 index 000000000..33322490d --- /dev/null +++ b/src/actions/getInternalData-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["getInternalData.js"],"names":[],"mappings":";;AAAA,MAAM,UAAU,QAAQ,UAAR,CAAV;AACN,MAAM,OAAO,QAAQ,aAAR,CAAP;AACN,MAAM,QAAQ,QAAQ,eAAR,CAAR;;AAEN,OAAO,OAAP,GAAiB,SAAS,YAAT,CAAsB,OAAtB,EAA+B;AAC9C,QAAM,EAAE,MAAF,KAAa,OAAb,CADwC;;AAG9C,SAAO,QACJ,IADI,CACC,IADD,EACO,QAAQ,QAAR,CADP,CAEJ,IAFI,CAEC,MAAM,OAAN,CAFD,CAGJ,IAHI,CAGC,QAAQ;AACZ,WAAO,SAAS,KAAK,IAAL,EAAW,MAAX,CAAT,GAA8B,IAA9B,CADK;GAAR,CAHR,CAH8C;CAA/B","file":"getInternalData-compiled.js","sourcesContent":["const Promise = require('bluebird');\nconst pick = require('lodash/pick');\nconst Users = require('../db/adapter');\n\nmodule.exports = function internalData(message) {\n const { fields } = message;\n\n return Promise\n .bind(this, message.username)\n .then(Users.getUser)\n .then(data => {\n return fields ? pick(data, fields) : data;\n });\n};\n"]} \ No newline at end of file diff --git a/src/actions/getInternalData.js b/src/actions/getInternalData.js index ccb59fe4e..aca32ada3 100644 --- a/src/actions/getInternalData.js +++ b/src/actions/getInternalData.js @@ -1,13 +1,13 @@ const Promise = require('bluebird'); -const getInternalData = require('../utils/getInternalData.js'); const pick = require('lodash/pick'); +const Users = require('../db/adapter'); module.exports = function internalData(message) { const { fields } = message; return Promise .bind(this, message.username) - .then(getInternalData) + .then(Users.getUser) .then(data => { return fields ? pick(data, fields) : data; }); diff --git a/src/actions/getMetadata-compiled.js b/src/actions/getMetadata-compiled.js new file mode 100644 index 000000000..f1e5c6d6b --- /dev/null +++ b/src/actions/getMetadata-compiled.js @@ -0,0 +1,13 @@ +'use strict'; + +const Promise = require('bluebird'); +const noop = require('lodash/noop'); +const Users = require('../db/adapter'); + +module.exports = function getMetadataAction(message) { + const { audience, username, fields } = message; + + return Promise.bind(this, username).then(Users.isExists).then(realUsername => [realUsername, audience, fields]).spread(Users.getMetadata).tap(message.public ? Users.isPublic(username, audience) : noop); +}; + +//# sourceMappingURL=getMetadata-compiled.js.map \ No newline at end of file diff --git a/src/actions/getMetadata-compiled.js.map b/src/actions/getMetadata-compiled.js.map new file mode 100644 index 000000000..09ccca463 --- /dev/null +++ b/src/actions/getMetadata-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["getMetadata.js"],"names":[],"mappings":";;AAAA,MAAM,UAAU,QAAQ,UAAR,CAAV;AACN,MAAM,OAAO,QAAQ,aAAR,CAAP;AACN,MAAM,QAAQ,QAAQ,eAAR,CAAR;;AAEN,OAAO,OAAP,GAAiB,SAAS,iBAAT,CAA2B,OAA3B,EAAoC;AACnD,QAAM,EAAE,QAAF,EAAY,QAAZ,EAAsB,MAAtB,KAAiC,OAAjC,CAD6C;;AAGnD,SAAO,QACJ,IADI,CACC,IADD,EACO,QADP,EAEJ,IAFI,CAEC,MAAM,QAAN,CAFD,CAGJ,IAHI,CAGC,gBAAgB,CAAC,YAAD,EAAe,QAAf,EAAyB,MAAzB,CAAhB,CAHD,CAIJ,MAJI,CAIG,MAAM,WAAN,CAJH,CAKJ,GALI,CAKA,QAAQ,MAAR,GAAiB,MAAM,QAAN,CAAe,QAAf,EAAyB,QAAzB,CAAjB,GAAsD,IAAtD,CALP,CAHmD;CAApC","file":"getMetadata-compiled.js","sourcesContent":["const Promise = require('bluebird');\nconst noop = require('lodash/noop');\nconst Users = require('../db/adapter');\n\nmodule.exports = function getMetadataAction(message) {\n const { audience, username, fields } = message;\n\n return Promise\n .bind(this, username)\n .then(Users.isExists)\n .then(realUsername => [realUsername, audience, fields])\n .spread(Users.getMetadata)\n .tap(message.public ? Users.isPublic(username, audience) : noop);\n};\n"]} \ No newline at end of file diff --git a/src/actions/getMetadata.js b/src/actions/getMetadata.js index bdcf948f1..bd5a1c26a 100644 --- a/src/actions/getMetadata.js +++ b/src/actions/getMetadata.js @@ -1,28 +1,14 @@ const Promise = require('bluebird'); -const Errors = require('common-errors'); -const getMetadata = require('../utils/getMetadata.js'); -const userExists = require('../utils/userExists.js'); const noop = require('lodash/noop'); -const get = require('lodash/get'); -const { USERS_ALIAS_FIELD } = require('../constants.js'); - -function isPublic(username, audience) { - return metadata => { - if (get(metadata, [audience, USERS_ALIAS_FIELD]) === username) { - return; - } - - throw new Errors.HttpStatusError(404, 'username was not found'); - }; -} +const Users = require('../db/adapter'); module.exports = function getMetadataAction(message) { const { audience, username, fields } = message; return Promise .bind(this, username) - .then(userExists) + .then(Users.isExists) .then(realUsername => [realUsername, audience, fields]) - .spread(getMetadata) - .tap(message.public ? isPublic(username, audience) : noop); + .spread(Users.getMetadata) + .tap(message.public ? Users.isPublic(username, audience) : noop); }; diff --git a/src/actions/list-compiled.js b/src/actions/list-compiled.js new file mode 100644 index 000000000..136b5b398 --- /dev/null +++ b/src/actions/list-compiled.js @@ -0,0 +1,22 @@ +'use strict'; + +const Users = require('../adapter'); +const fsort = require('redis-filtered-sort'); +const { USERS_INDEX, USERS_PUBLIC_INDEX } = require('../constants.js'); + +module.exports = function iterateOverActiveUsers(opts) { + const { criteria, audience, filter } = opts; + + return Users.getList({ + criteria, + audience, + filter, + index: opts.public ? USERS_PUBLIC_INDEX : USERS_INDEX, + strFilter: typeof filter === 'string' ? filter : fsort.filter(filter || {}), + order: opts.order || 'ASC', + offset: opts.offset || 0, + limit: opts.limit || 10 + }); +}; + +//# sourceMappingURL=list-compiled.js.map \ No newline at end of file diff --git a/src/actions/list-compiled.js.map b/src/actions/list-compiled.js.map new file mode 100644 index 000000000..1cd55afa8 --- /dev/null +++ b/src/actions/list-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["list.js"],"names":[],"mappings":";;AAAA,MAAM,QAAQ,QAAQ,YAAR,CAAR;AACN,MAAM,QAAQ,QAAQ,qBAAR,CAAR;AACN,MAAM,EAAE,WAAF,EAAe,kBAAf,KAAsC,QAAQ,iBAAR,CAAtC;;AAEN,OAAO,OAAP,GAAiB,SAAS,sBAAT,CAAgC,IAAhC,EAAsC;AACrD,QAAM,EAAE,QAAF,EAAY,QAAZ,EAAsB,MAAtB,KAAiC,IAAjC,CAD+C;;AAGrD,SAAO,MAAM,OAAN,CAAc;AACnB,YADmB;AAEnB,YAFmB;AAGnB,UAHmB;AAInB,WAAO,KAAK,MAAL,GAAc,kBAAd,GAAmC,WAAnC;AACP,eAAW,OAAO,MAAP,KAAkB,QAAlB,GAA6B,MAA7B,GAAsC,MAAM,MAAN,CAAa,UAAU,EAAV,CAAnD;AACX,WAAO,KAAK,KAAL,IAAc,KAAd;AACP,YAAQ,KAAK,MAAL,IAAe,CAAf;AACR,WAAO,KAAK,KAAL,IAAc,EAAd;GARF,CAAP,CAHqD;CAAtC","file":"list-compiled.js","sourcesContent":["const Users = require('../adapter');\nconst fsort = require('redis-filtered-sort');\nconst { USERS_INDEX, USERS_PUBLIC_INDEX } = require('../constants.js');\n\nmodule.exports = function iterateOverActiveUsers(opts) {\n const { criteria, audience, filter } = opts;\n\n return Users.getList({\n criteria,\n audience,\n filter,\n index: opts.public ? USERS_PUBLIC_INDEX : USERS_INDEX,\n strFilter: typeof filter === 'string' ? filter : fsort.filter(filter || {}),\n order: opts.order || 'ASC',\n offset: opts.offset || 0,\n limit: opts.limit || 10\n });\n\n};\n"]} \ No newline at end of file diff --git a/src/actions/list.js b/src/actions/list.js index 12bca0211..d23b5a4a5 100644 --- a/src/actions/list.js +++ b/src/actions/list.js @@ -1,60 +1,19 @@ -const Promise = require('bluebird'); -const redisKey = require('../utils/key.js'); -const mapValues = require('lodash/mapValues'); +const Users = require('../adapter'); const fsort = require('redis-filtered-sort'); -const JSONParse = JSON.parse.bind(JSON); -const { USERS_INDEX, USERS_PUBLIC_INDEX, USERS_METADATA } = require('../constants.js'); +const { USERS_INDEX, USERS_PUBLIC_INDEX } = require('../constants.js'); module.exports = function iterateOverActiveUsers(opts) { - const { redis } = this; const { criteria, audience, filter } = opts; - const strFilter = typeof filter === 'string' ? filter : fsort.filter(filter || {}); - const order = opts.order || 'ASC'; - const offset = opts.offset || 0; - const limit = opts.limit || 10; - const metaKey = redisKey('*', USERS_METADATA, audience); - const index = opts.public ? USERS_PUBLIC_INDEX : USERS_INDEX; - return redis - .fsort(index, metaKey, criteria, order, strFilter, offset, limit) - .then(ids => { - const length = +ids.pop(); - if (length === 0 || ids.length === 0) { - return [ - ids || [], - [], - length, - ]; - } + return Users.getList({ + criteria, + audience, + filter, + index: opts.public ? USERS_PUBLIC_INDEX : USERS_INDEX, + strFilter: typeof filter === 'string' ? filter : fsort.filter(filter || {}), + order: opts.order || 'ASC', + offset: opts.offset || 0, + limit: opts.limit || 10 + }); - const pipeline = redis.pipeline(); - ids.forEach(id => { - pipeline.hgetallBuffer(redisKey(id, USERS_METADATA, audience)); - }); - return Promise.all([ - ids, - pipeline.exec(), - length, - ]); - }) - .spread((ids, props, length) => { - const users = ids.map(function remapData(id, idx) { - const data = props[idx][1]; - const account = { - id, - metadata: { - [audience]: data ? mapValues(data, JSONParse) : {}, - }, - }; - - return account; - }); - - return { - users, - cursor: offset + limit, - page: Math.floor(offset / limit + 1), - pages: Math.ceil(length / limit), - }; - }); }; diff --git a/src/db/adapter-compiled.js b/src/db/adapter-compiled.js new file mode 100644 index 000000000..ef595fd6b --- /dev/null +++ b/src/db/adapter-compiled.js @@ -0,0 +1,366 @@ +'use strict'; + +/** + * Created by Stainwoortsel on 30.05.2016. + */ +const RedisStorage = require('./redisstorage'); +const Errors = require('common-errors'); + +class Users { + constructor(adapter) { + + this.adapter = adapter; + + /* + let opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + // init configuration + const config = this._config = _extends({}, defaultOpts, opts); + + // setup hooks + forOwn(config.hooks, (_hooks, eventName) => { + const hooks = Array.isArray(_hooks) ? _hooks : [_hooks]; + each(hooks, hook => this.on(eventName, hook)); + }); + */ + } + + /** + * Initialize connection + * @return {Promise} + */ + connect() {} + // ???? + + /** + * Close connection + * return {Promise} + */ + close() {} + // ???? + + /** + * Lock user + * @param username + * @param reason + * @param whom + * @param remoteip + * @returns {Redis} + */ + lockUser({ username, reason, whom, remoteip }) { + return this.adapter.lockUser({ username, reason, whom, remoteip }); + } + + /** + * Unlock user + * @param username + * @returns {Redis} + */ + unlockUser(username) { + return this.adapter.unlockUser(username); + } + + /** + * Check existance of user + * @param username + * @returns {Redis} + */ + isExists(username) { + return this.adapter.isExists(username); + } + + isAliasExists(alias, thunk) { + return this.adapter.isAliasExists(alias, thunk); + } + + /** + * User is public + * @param username + * @param audience + * @returns {function()} + */ + isPublic(username, audience) { + return this.adapter.isPublic(username, audience); + } + + /** + * Check that user is active + * @param data + * @returns {boolean} + */ + isActive(data) { + return this.adapter.isActive(data); + } + + /** + * Check that user is banned + * @param data + * @returns {Promise} + */ + isBanned(data) { + return this.adapter.isBanned(data); + } + + /** + * Activate user account + * @param user + * @returns {Redis} + */ + activateAccount(user) { + return this.adapter.activateAccount(user); + } + + /** + * Get user internal data + * @param username + * @returns {Object} + */ + getUser(username) { + return this.adapter.getUser(username); + } + + /** + * Get users metadata by username and audience + * @param username + * @param audience + * @returns {Object} + */ + + getMetadata(username, _audiences, fields = {}) { + return this.adapter.getMetadata(username, _audiences, fields); + } + + /** + * Return the list of users by specified params + * @param opts + * @returns {Array} + */ + getList(opts) { + return this.adapter.getList(opts); + } + + /** + * Check existence of alias + * @param data + * @returns {boolean} + */ + isAliasAssigned(data) { + return this.adapter.isAliasAssigned(data); + } + + /** + * Check that user is admin + * @param meta + * @returns {boolean} + */ + isAdmin(meta) { + return this.adapter.isAdmin(meta); + } + + /** + * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN + * @param username + * @param alias + * @returns {Redis} + */ + storeAlias(username, alias) { + return this.adapter.storeAlias(username, alias); + } + + /** + * Assign alias to the user record, marked by username + * @param username + * @param alias + * @returns {Redis} + */ + assignAlias(username, alias) { + return this.adapter.assignAlias(username, alias); + } + + get remoteipKey() { + return this.adapter.remoteipKey; + } + + set remoteipKey(val) { + this.adapter.remoteipKey = val; + } + + generateipKey(username, remoteip) { + return this.adapter.generateipKey(username, remoteip); + } + + get loginAttempts() { + return this.adapter.loginAttempts; + } + + set loginAttempts(val) { + this.adapter.loginAttempts = val; + } + + get options() { + return this.adapter.options; + } + + set options(opts) { + this.adapter.options = opts; + } + + dropAttempts() { + return this.adapter.dropAttempts(); + } + + checkLoginAttempts(data) { + return this.adapter.checkLoginAttempts(data); + } + + /** + * Set user password + * @param username + * @param hash + * @returns {Redis} + */ + setPassword(username, hash) { + return this.adapter.setPassword(username, hash); + } + + /** + * Reset the lock by IP + * @param username + * @param ip + * @returns {Redis} + */ + resetIPLock(username, ip) { + return this.adapter.resetIPLock(username, ip); + } + + /** + * + * @param username + * @param audience + * @param metadata + * @returns {Object} + */ + updateMetadata({ username, audience, metadata }) { + return this.adapter.updateMetadata({ username, audience, metadata }); + } + + /** + * Removing user by username (and data?) + * @param username + * @param data + * @returns {Redis} + */ + removeUser(username, data) { + return this.adapter.removeUser(username, data); + } + + /** + * Verify ip limits + * @param {redisCluster} redis + * @param {Object} registrationLimits + * @param {String} ipaddress + * @return {Function} + */ + checkLimits(registrationLimits, ipaddress) { + return this.adapter.checkLimits(registrationLimits, ipaddress); + } + + /** + * Creates user with a given hash + * @param redis + * @param username + * @param activate + * @param deleteInactiveAccounts + * @param userDataKey + * @returns {Function} + */ + createUser(username, activate, deleteInactiveAccounts) { + return this.adapter.createUser(username, activate, deleteInactiveAccounts); + } + + /** + * Performs captcha check, returns thukn + * @param {String} username + * @param {String} captcha + * @param {Object} captchaConfig + * @return {Function} + */ + checkCaptcha(username, captcha) { + return this.adapter.checkCaptcha(username, captcha); + } + + /** + * Stores username to the index set + * @param username + * @returns {Redis} + */ + storeUsername(username) { + return this.adapter.storeUsername(username); + } + + /** + * Running a custom script or query + * @param script + * @returns {*|Promise} + */ + + customScript(script) { + return this.adapter.customScript(script); + } + + /** + * The error wrapper for the front-level HTTP output + * @param e + */ + static mapErrors(e) { + const err = new Errors.HttpStatusError(e.status_code || 500, e.message); + if (err.status_code >= 500) { + err.message = Errors.HttpStatusError.message_map[500]; //hide the real error from the user + } + } + +} + +module.exports = function modelCreator() { + return new Users(RedisStorage); +}; + +/* + ВОПРОСЫ: + Не превращается ли адаптер в полноценную модель? + Что делать с промисами? Правильно ли частично их пихать в адаптер (по идее, соединение -- ресурс, так что да)? + Архитектура MServices, где берется redis? + Оставить Errors снаружи? + ++ ЭМИТТЕР НЕ НУЖЕН ++ МОЖНО СДЕЛАТЬ ХУКИ, но только если нужно +~ ЭРРОРЫ НАДО ВЫНЕСТИ НАРУЖУ с сообщениями, а внутри генерить женерик-эрроры с кодами, врапить их в экшне в HTTPошикби + sandbox/activate.js -> Если это модель, то оствлять ли всякие verifyToken, emailVerification и хуки снаружи? + СНАРУЖИ ++ sandbox/alias.js -> 18, 25 (запихнуть их в User?) не надо, все верно + sandbox/getMetadata -> волевым решением, логика Metadata вместе с промисами запихнута в метод getMetadata. С точки зрения абстракции всё соблюдено, но правильно ли это для текущей ситуации? + ДА, МОЖНО + но надо сделать разницу между трансформатором данных + и селектором + плюс вытянуть свежую репу + sandbox/list -> метод getList настолько широк, что поглатил в себя всю реализацию этого экшна. разве это хорошо? + ВСЕ ОК, дефолты можно вытащить наружу. подумать над общим форматом ответа и соотв-но вытащить кое что в методы сторожа + sandbox/login -> 25, передаем options в адаптер... что-то не в порядке в королевстве Датском! + МЕНЯЕМ логику работы промисов. чтобы не городить нерабочий огород в адаптере + + redisstorage -> 394 this.log. раньше this брался из экшна, к чему относится метод log? + redisstorage -> 148,157 оборачивать ли эти методы? эти статусы потенциально зависят от адаптера, но должны выводить значение в чистом виде. С другой стороны, в чистом виде значение не используется, а используется промис + ЛОГИЧНЕЕ будет сформить метод с промисом и кидаться ошибками на верхний уровень, собстна в sql логика будет та же + нафига нужны просто флаги -- не понятно + redisstorage -> 105 что на счет методов с thunk'ом? + ПОСМОТРЕТЬ что делает thunk и где он участвует, вожможно придется оставить + + bluebird: tap + + ПОСМОТРЕТЬ levelDB и схожие адаптеры для работы с логикой + + МОЖНО в адаптере сделать трансмиттер ошибок адаптера в ошибки HTTP + + */ + +//# sourceMappingURL=adapter-compiled.js.map \ No newline at end of file diff --git a/src/db/adapter-compiled.js.map b/src/db/adapter-compiled.js.map new file mode 100644 index 000000000..a68ad9a28 --- /dev/null +++ b/src/db/adapter-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["adapter.js"],"names":[],"mappings":"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","file":"adapter-compiled.js","sourcesContent":["/**\n * Created by Stainwoortsel on 30.05.2016.\n */\nconst RedisStorage = require('./redisstorage');\nconst Errors = require('common-errors');\n\nclass Users{\n constructor(adapter){\n\n this.adapter = adapter;\n\n/*\n let opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];\n\n // init configuration\n const config = this._config = _extends({}, defaultOpts, opts);\n\n // setup hooks\n forOwn(config.hooks, (_hooks, eventName) => {\n const hooks = Array.isArray(_hooks) ? _hooks : [_hooks];\n each(hooks, hook => this.on(eventName, hook));\n });\n*/\n\n }\n\n /**\n * Initialize connection\n * @return {Promise}\n */\n connect(){\n // ????\n }\n\n /**\n * Close connection\n * return {Promise}\n */\n close(){\n // ????\n }\n\n\n /**\n * Lock user\n * @param username\n * @param reason\n * @param whom\n * @param remoteip\n * @returns {Redis}\n */\n lockUser({ username, reason, whom, remoteip }){\n return this.adapter.lockUser({ username, reason, whom, remoteip });\n }\n\n /**\n * Unlock user\n * @param username\n * @returns {Redis}\n */\n unlockUser(username){\n return this.adapter.unlockUser(username);\n }\n\n /**\n * Check existance of user\n * @param username\n * @returns {Redis}\n */\n isExists(username){\n return this.adapter.isExists(username);\n }\n\n isAliasExists(alias, thunk){\n return this.adapter.isAliasExists(alias, thunk);\n }\n\n /**\n * User is public\n * @param username\n * @param audience\n * @returns {function()}\n */\n isPublic(username, audience) {\n return this.adapter.isPublic(username, audience);\n }\n\n /**\n * Check that user is active\n * @param data\n * @returns {boolean}\n */\n isActive(data){\n return this.adapter.isActive(data);\n }\n\n /**\n * Check that user is banned\n * @param data\n * @returns {Promise}\n */\n isBanned(data){\n return this.adapter.isBanned(data);\n }\n\n /**\n * Activate user account\n * @param user\n * @returns {Redis}\n */\n activateAccount(user){\n return this.adapter.activateAccount(user);\n }\n\n /**\n * Get user internal data\n * @param username\n * @returns {Object}\n */\n getUser(username){\n return this.adapter.getUser(username);\n }\n\n /**\n * Get users metadata by username and audience\n * @param username\n * @param audience\n * @returns {Object}\n */\n\n getMetadata(username, _audiences, fields = {}) {\n return this.adapter.getMetadata(username, _audiences, fields);\n }\n\n\n /**\n * Return the list of users by specified params\n * @param opts\n * @returns {Array}\n */\n getList(opts){\n return this.adapter.getList(opts);\n }\n\n /**\n * Check existence of alias\n * @param data\n * @returns {boolean}\n */\n isAliasAssigned(data){\n return this.adapter.isAliasAssigned(data);\n }\n\n /**\n * Check that user is admin\n * @param meta\n * @returns {boolean}\n */\n isAdmin(meta){\n return this.adapter.isAdmin(meta);\n }\n\n /**\n * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN\n * @param username\n * @param alias\n * @returns {Redis}\n */\n storeAlias(username, alias){\n return this.adapter.storeAlias(username, alias);\n }\n\n /**\n * Assign alias to the user record, marked by username\n * @param username\n * @param alias\n * @returns {Redis}\n */\n assignAlias(username, alias){\n return this.adapter.assignAlias(username, alias);\n }\n\n get remoteipKey(){\n return this.adapter.remoteipKey;\n }\n\n set remoteipKey(val){\n this.adapter.remoteipKey = val;\n }\n\n generateipKey(username, remoteip){\n return this.adapter.generateipKey(username, remoteip);\n }\n\n get loginAttempts(){\n return this.adapter.loginAttempts;\n }\n\n set loginAttempts(val){\n this.adapter.loginAttempts = val;\n }\n\n get options(){\n return this.adapter.options;\n }\n\n set options(opts){\n this.adapter.options = opts;\n }\n\n dropAttempts(){\n return this.adapter.dropAttempts();\n }\n\n checkLoginAttempts(data) {\n return this.adapter.checkLoginAttempts(data);\n }\n\n /**\n * Set user password\n * @param username\n * @param hash\n * @returns {Redis}\n */\n setPassword(username, hash){\n return this.adapter.setPassword(username, hash);\n }\n\n /**\n * Reset the lock by IP\n * @param username\n * @param ip\n * @returns {Redis}\n */\n resetIPLock(username, ip){\n return this.adapter.resetIPLock(username, ip);\n }\n\n /**\n *\n * @param username\n * @param audience\n * @param metadata\n * @returns {Object}\n */\n updateMetadata({username, audience, metadata}) {\n return this.adapter.updateMetadata({username, audience, metadata});\n }\n\n /**\n * Removing user by username (and data?)\n * @param username\n * @param data\n * @returns {Redis}\n */\n removeUser(username, data){\n return this.adapter.removeUser(username, data);\n }\n\n /**\n * Verify ip limits\n * @param {redisCluster} redis\n * @param {Object} registrationLimits\n * @param {String} ipaddress\n * @return {Function}\n */\n checkLimits(registrationLimits, ipaddress) {\n return this.adapter.checkLimits(registrationLimits, ipaddress);\n }\n\n /**\n * Creates user with a given hash\n * @param redis\n * @param username\n * @param activate\n * @param deleteInactiveAccounts\n * @param userDataKey\n * @returns {Function}\n */\n createUser(username, activate, deleteInactiveAccounts) {\n return this.adapter.createUser(username, activate, deleteInactiveAccounts);\n }\n\n /**\n * Performs captcha check, returns thukn\n * @param {String} username\n * @param {String} captcha\n * @param {Object} captchaConfig\n * @return {Function}\n */\n checkCaptcha(username, captcha) {\n return this.adapter.checkCaptcha(username, captcha);\n }\n\n /**\n * Stores username to the index set\n * @param username\n * @returns {Redis}\n */\n storeUsername(username){\n return this.adapter.storeUsername(username);\n }\n\n /**\n * Running a custom script or query\n * @param script\n * @returns {*|Promise}\n */\n\n customScript(script){\n return this.adapter.customScript(script);\n }\n\n /**\n * The error wrapper for the front-level HTTP output\n * @param e\n */\n static mapErrors(e){\n const err = new Errors.HttpStatusError(e.status_code || 500 , e.message);\n if(err.status_code >= 500) {\n err.message = Errors.HttpStatusError.message_map[500]; //hide the real error from the user\n }\n }\n\n}\n\nmodule.exports = function modelCreator(){\n return new Users(RedisStorage);\n};\n\n\n/*\n ВОПРОСЫ:\n Не превращается ли адаптер в полноценную модель?\n Что делать с промисами? Правильно ли частично их пихать в адаптер (по идее, соединение -- ресурс, так что да)?\n Архитектура MServices, где берется redis?\n Оставить Errors снаружи?\n\n+ ЭМИТТЕР НЕ НУЖЕН\n+ МОЖНО СДЕЛАТЬ ХУКИ, но только если нужно\n~ ЭРРОРЫ НАДО ВЫНЕСТИ НАРУЖУ с сообщениями, а внутри генерить женерик-эрроры с кодами, врапить их в экшне в HTTPошикби\n sandbox/activate.js -> Если это модель, то оствлять ли всякие verifyToken, emailVerification и хуки снаружи?\n СНАРУЖИ\n+ sandbox/alias.js -> 18, 25 (запихнуть их в User?) не надо, все верно\n sandbox/getMetadata -> волевым решением, логика Metadata вместе с промисами запихнута в метод getMetadata. С точки зрения абстракции всё соблюдено, но правильно ли это для текущей ситуации?\n ДА, МОЖНО\n но надо сделать разницу между трансформатором данных\n и селектором\n плюс вытянуть свежую репу\n sandbox/list -> метод getList настолько широк, что поглатил в себя всю реализацию этого экшна. разве это хорошо?\n ВСЕ ОК, дефолты можно вытащить наружу. подумать над общим форматом ответа и соотв-но вытащить кое что в методы сторожа\n sandbox/login -> 25, передаем options в адаптер... что-то не в порядке в королевстве Датском!\n МЕНЯЕМ логику работы промисов. чтобы не городить нерабочий огород в адаптере\n\n redisstorage -> 394 this.log. раньше this брался из экшна, к чему относится метод log?\n redisstorage -> 148,157 оборачивать ли эти методы? эти статусы потенциально зависят от адаптера, но должны выводить значение в чистом виде. С другой стороны, в чистом виде значение не используется, а используется промис\n ЛОГИЧНЕЕ будет сформить метод с промисом и кидаться ошибками на верхний уровень, собстна в sql логика будет та же\n нафига нужны просто флаги -- не понятно\n redisstorage -> 105 что на счет методов с thunk'ом?\n ПОСМОТРЕТЬ что делает thunk и где он участвует, вожможно придется оставить\n\n bluebird: tap\n\n ПОСМОТРЕТЬ levelDB и схожие адаптеры для работы с логикой\n\n МОЖНО в адаптере сделать трансмиттер ошибок адаптера в ошибки HTTP\n\n */\n"]} \ No newline at end of file diff --git a/src/db/adapter.js b/src/db/adapter.js index 219bc487a..5a46cf4c5 100644 --- a/src/db/adapter.js +++ b/src/db/adapter.js @@ -1,3 +1,368 @@ /** * Created by Stainwoortsel on 30.05.2016. */ +const RedisStorage = require('./redisstorage'); +const Errors = require('common-errors'); + +class Users{ + constructor(adapter){ + + this.adapter = adapter; + +/* + let opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + // init configuration + const config = this._config = _extends({}, defaultOpts, opts); + + // setup hooks + forOwn(config.hooks, (_hooks, eventName) => { + const hooks = Array.isArray(_hooks) ? _hooks : [_hooks]; + each(hooks, hook => this.on(eventName, hook)); + }); +*/ + + } + + /** + * Initialize connection + * @return {Promise} + */ + connect(){ + // ???? + } + + /** + * Close connection + * return {Promise} + */ + close(){ + // ???? + } + + + /** + * Lock user + * @param username + * @param reason + * @param whom + * @param remoteip + * @returns {Redis} + */ + lockUser({ username, reason, whom, remoteip }){ + return this.adapter.lockUser({ username, reason, whom, remoteip }); + } + + /** + * Unlock user + * @param username + * @returns {Redis} + */ + unlockUser(username){ + return this.adapter.unlockUser(username); + } + + /** + * Check existance of user + * @param username + * @returns {Redis} + */ + isExists(username){ + return this.adapter.isExists(username); + } + + isAliasExists(alias, thunk){ + return this.adapter.isAliasExists(alias, thunk); + } + + /** + * User is public + * @param username + * @param audience + * @returns {function()} + */ + isPublic(username, audience) { + return this.adapter.isPublic(username, audience); + } + + /** + * Check that user is active + * @param data + * @returns {boolean} + */ + isActive(data){ + return this.adapter.isActive(data); + } + + /** + * Check that user is banned + * @param data + * @returns {Promise} + */ + isBanned(data){ + return this.adapter.isBanned(data); + } + + /** + * Activate user account + * @param user + * @returns {Redis} + */ + activateAccount(user){ + return this.adapter.activateAccount(user); + } + + /** + * Get user internal data + * @param username + * @returns {Object} + */ + getUser(username){ + return this.adapter.getUser(username); + } + + /** + * Get users metadata by username and audience + * @param username + * @param audience + * @returns {Object} + */ + + getMetadata(username, _audiences, fields = {}) { + return this.adapter.getMetadata(username, _audiences, fields); + } + + + /** + * Return the list of users by specified params + * @param opts + * @returns {Array} + */ + getList(opts){ + return this.adapter.getList(opts); + } + + /** + * Check existence of alias + * @param data + * @returns {boolean} + */ + isAliasAssigned(data){ + return this.adapter.isAliasAssigned(data); + } + + /** + * Check that user is admin + * @param meta + * @returns {boolean} + */ + isAdmin(meta){ + return this.adapter.isAdmin(meta); + } + + /** + * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN + * @param username + * @param alias + * @returns {Redis} + */ + storeAlias(username, alias){ + return this.adapter.storeAlias(username, alias); + } + + /** + * Assign alias to the user record, marked by username + * @param username + * @param alias + * @returns {Redis} + */ + assignAlias(username, alias){ + return this.adapter.assignAlias(username, alias); + } + + get remoteipKey(){ + return this.adapter.remoteipKey; + } + + set remoteipKey(val){ + this.adapter.remoteipKey = val; + } + + generateipKey(username, remoteip){ + return this.adapter.generateipKey(username, remoteip); + } + + get loginAttempts(){ + return this.adapter.loginAttempts; + } + + set loginAttempts(val){ + this.adapter.loginAttempts = val; + } + + get options(){ + return this.adapter.options; + } + + set options(opts){ + this.adapter.options = opts; + } + + dropAttempts(){ + return this.adapter.dropAttempts(); + } + + checkLoginAttempts(data) { + return this.adapter.checkLoginAttempts(data); + } + + /** + * Set user password + * @param username + * @param hash + * @returns {Redis} + */ + setPassword(username, hash){ + return this.adapter.setPassword(username, hash); + } + + /** + * Reset the lock by IP + * @param username + * @param ip + * @returns {Redis} + */ + resetIPLock(username, ip){ + return this.adapter.resetIPLock(username, ip); + } + + /** + * + * @param username + * @param audience + * @param metadata + * @returns {Object} + */ + updateMetadata({username, audience, metadata}) { + return this.adapter.updateMetadata({username, audience, metadata}); + } + + /** + * Removing user by username (and data?) + * @param username + * @param data + * @returns {Redis} + */ + removeUser(username, data){ + return this.adapter.removeUser(username, data); + } + + /** + * Verify ip limits + * @param {redisCluster} redis + * @param {Object} registrationLimits + * @param {String} ipaddress + * @return {Function} + */ + checkLimits(registrationLimits, ipaddress) { + return this.adapter.checkLimits(registrationLimits, ipaddress); + } + + /** + * Creates user with a given hash + * @param redis + * @param username + * @param activate + * @param deleteInactiveAccounts + * @param userDataKey + * @returns {Function} + */ + createUser(username, activate, deleteInactiveAccounts) { + return this.adapter.createUser(username, activate, deleteInactiveAccounts); + } + + /** + * Performs captcha check, returns thukn + * @param {String} username + * @param {String} captcha + * @param {Object} captchaConfig + * @return {Function} + */ + checkCaptcha(username, captcha) { + return this.adapter.checkCaptcha(username, captcha); + } + + /** + * Stores username to the index set + * @param username + * @returns {Redis} + */ + storeUsername(username){ + return this.adapter.storeUsername(username); + } + + /** + * Running a custom script or query + * @param script + * @returns {*|Promise} + */ + + customScript(script){ + return this.adapter.customScript(script); + } + + /** + * The error wrapper for the front-level HTTP output + * @param e + */ + static mapErrors(e){ + const err = new Errors.HttpStatusError(e.status_code || 500 , e.message); + if(err.status_code >= 500) { + err.message = Errors.HttpStatusError.message_map[500]; //hide the real error from the user + } + } + +} + +module.exports = function modelCreator(){ + return new Users(RedisStorage); +}; + + +/* + ВОПРОСЫ: + Не превращается ли адаптер в полноценную модель? + Что делать с промисами? Правильно ли частично их пихать в адаптер (по идее, соединение -- ресурс, так что да)? + Архитектура MServices, где берется redis? + Оставить Errors снаружи? + ++ ЭМИТТЕР НЕ НУЖЕН ++ МОЖНО СДЕЛАТЬ ХУКИ, но только если нужно +~ ЭРРОРЫ НАДО ВЫНЕСТИ НАРУЖУ с сообщениями, а внутри генерить женерик-эрроры с кодами, врапить их в экшне в HTTPошикби + sandbox/activate.js -> Если это модель, то оствлять ли всякие verifyToken, emailVerification и хуки снаружи? + СНАРУЖИ ++ sandbox/alias.js -> 18, 25 (запихнуть их в User?) не надо, все верно + sandbox/getMetadata -> волевым решением, логика Metadata вместе с промисами запихнута в метод getMetadata. С точки зрения абстракции всё соблюдено, но правильно ли это для текущей ситуации? + ДА, МОЖНО + но надо сделать разницу между трансформатором данных + и селектором + плюс вытянуть свежую репу + sandbox/list -> метод getList настолько широк, что поглатил в себя всю реализацию этого экшна. разве это хорошо? + ВСЕ ОК, дефолты можно вытащить наружу. подумать над общим форматом ответа и соотв-но вытащить кое что в методы сторожа + sandbox/login -> 25, передаем options в адаптер... что-то не в порядке в королевстве Датском! + МЕНЯЕМ логику работы промисов. чтобы не городить нерабочий огород в адаптере + + redisstorage -> 394 this.log. раньше this брался из экшна, к чему относится метод log? + redisstorage -> 148,157 оборачивать ли эти методы? эти статусы потенциально зависят от адаптера, но должны выводить значение в чистом виде. С другой стороны, в чистом виде значение не используется, а используется промис + ЛОГИЧНЕЕ будет сформить метод с промисом и кидаться ошибками на верхний уровень, собстна в sql логика будет та же + нафига нужны просто флаги -- не понятно + redisstorage -> 105 что на счет методов с thunk'ом? + ПОСМОТРЕТЬ что делает thunk и где он участвует, вожможно придется оставить + + bluebird: tap + + ПОСМОТРЕТЬ levelDB и схожие адаптеры для работы с логикой + + МОЖНО в адаптере сделать трансмиттер ошибок адаптера в ошибки HTTP + + */ diff --git a/src/db/redisstorage-compiled.js b/src/db/redisstorage-compiled.js new file mode 100644 index 000000000..caa67f248 --- /dev/null +++ b/src/db/redisstorage-compiled.js @@ -0,0 +1,605 @@ +'use strict'; + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +/** + * Created by Stainwoortsel on 30.05.2016. + */ +const Promise = require('bluebird'); +const Errors = require('common-errors'); +const mapValues = require('lodash/mapValues'); +const defaults = require('lodash/defaults'); +const get = require('lodash/get'); +const pick = require('lodash/pick'); +const request = require('request-promise'); +const uuid = require('node-uuid'); +const fsort = require('redis-filtered-sort'); +const fmt = require('util').format; +const is = require('is'); +const sha256 = require('./sha256.js'); +const moment = require('moment'); + +const stringify = JSON.stringify.bind(JSON); +const { + USERS_DATA, USERS_METADATA, USERS_ALIAS_TO_LOGIN, + USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA, + USERS_ACTIVE_FLAG, USERS_INDEX, USERS_PUBLIC_INDEX, + USERS_ALIAS_FIELD +} = require('../constants.js'); + +const { redis, captcha: captchaConfig, config } = this; +const { jwt: { lockAfterAttempts, defaultAudience } } = config; + +/** + * Generate hash key string + * @param args + * @returns {string} + */ +const generateKey = (...args) => { + const SEPARATOR = '!'; + return args.join(SEPARATOR); +}; + +module.exports = { + /** + * Lock user + * @param username + * @param reason + * @param whom + * @param remoteip + * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} + */ + lockUser({ username, reason, whom, remoteip }) { + const data = { + banned: true, + [USERS_BANNED_DATA]: { + reason, + whom, + remoteip + } + }; + + return redis.pipeline().hset(generateKey(username, USERS_DATA), USERS_BANNED_FLAG, 'true') + // set .banned on metadata for filtering & sorting users by that field + .hmset(generateKey(username, USERS_METADATA, defaultAudience), mapValues(data, stringify)).del(generateKey(username, USERS_TOKENS)).exec(); + }, + + /** + * Unlock user + * @param username + * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} + */ + unlockUser({ username }) { + return redis.pipeline().hdel(generateKey(username, USERS_DATA), USERS_BANNED_FLAG) + // remove .banned on metadata for filtering & sorting users by that field + .hdel(generateKey(username, USERS_METADATA, defaultAudience), 'banned', USERS_BANNED_DATA).exec(); + }, + + /** + * Check existance of user + * @param username + * @returns {Redis} + */ + isExists(username) { + return redis.pipeline().hget(USERS_ALIAS_TO_LOGIN, username).exists(generateKey(username, USERS_DATA)).exec().spread((alias, exists) => { + if (alias[1]) { + return alias[1]; + } + + if (!exists[1]) { + throw new Errors.HttpStatusError(404, `"${ username }" does not exists`); + } + + return username; + }); + }, + + isAliasExists(alias, thunk) { + function resolveAlias(alias) { + return redis.hget(USERS_ALIAS_TO_LOGIN, alias).then(username => { + if (username) { + throw new Errors.HttpStatusError(409, `"${ alias }" already exists`); + } + + return username; + }); + } + if (thunk) { + return function resolveAliasThunk() { + return resolveAlias(alias); + }; + } + + return resolveAlias(alias); + }, + + /** + * User is public + * @param username + * @param audience + * @returns {function()} + */ + isPublic(username, audience) { + return metadata => { + if (get(metadata, [audience, USERS_ALIAS_FIELD]) === username) { + return; + } + + throw new Errors.HttpStatusError(404, 'username was not found'); + }; + }, + + /** + * Check that user is active + * @param data + * @returns {Promise} + */ + isActive(data) { + if (String(data[USERS_ACTIVE_FLAG]) !== 'true') { + return Promise.reject(new Errors.HttpStatusError(412, 'Account hasn\'t been activated')); + } + + return Promise.resolve(data); + }, + + /** + * Check that user is banned + * @param data + * @returns {Promise} + */ + isBanned(data) { + if (String(data[USERS_BANNED_FLAG]) === 'true') { + return Promise.reject(new Errors.HttpStatusError(423, 'Account has been locked')); + } + + return Promise.resolve(data); + }, + + /** + * Activate user account + * @param user + * @returns {Redis} + */ + activateAccount(user) { + const userKey = generateKey(user, USERS_DATA); + + // WARNING: `persist` is very important, otherwise we will lose user's information in 30 days + // set to active & persist + return redis.pipeline().hget(userKey, USERS_ACTIVE_FLAG).hset(userKey, USERS_ACTIVE_FLAG, 'true').persist(userKey).sadd(USERS_INDEX, user).exec().spread(function pipeResponse(isActive) { + const status = isActive[1]; + if (status === 'true') { + throw new Errors.HttpStatusError(417, `Account ${ user } was already activated`); + } + }); + }, + + /** + * Get user internal data + * @param username + * @returns {Object} + */ + getUser(username) { + const userKey = generateKey(username, USERS_DATA); + + return redis.pipeline().hget(USERS_ALIAS_TO_LOGIN, username).exists(userKey).hgetallBuffer(userKey).exec().spread((aliasToUsername, exists, data) => { + if (aliasToUsername[1]) { + return this.getUser(aliasToUsername[1]); + } + + if (!exists[1]) { + throw new Errors.HttpStatusError(404, `"${ username }" does not exists`); + } + + return _extends({}, data[1], { username }); + }); + }, + + /** + * Get users metadata by username and audience + * @param username + * @param audience + * @returns {Object} + */ + + // getMetadata(username, audience){ + // return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience)); + // }, + + getMetadata(username, _audiences, fields = {}) { + const audiences = Array.isArray(_audiences) ? _audiences : [_audiences]; + + return Promise.map(audiences, audience => { + return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience)); + }).then(function remapAudienceData(data) { + const output = {}; + audiences.forEach(function transform(aud, idx) { + const datum = data[idx]; + + if (datum) { + const pickFields = fields[aud]; + output[aud] = mapValues(datum, JSONParse); + if (pickFields) { + output[aud] = pick(output[aud], pickFields); + } + } else { + output[aud] = {}; + } + }); + + return output; + }); + }, + + /** + * Return the list of users by specified params + * @param opts + * @returns {Array} + */ + getList(opts) { + const { criteria, audience, filter, index, strFilter, order, offset, limit } = opts; + const metaKey = generateKey('*', USERS_METADATA, audience); + + return redis.fsort(index, metaKey, criteria, order, strFilter, offset, limit).then(ids => { + const length = +ids.pop(); + if (length === 0 || ids.length === 0) { + return [ids || [], [], length]; + } + + const pipeline = redis.pipeline(); + ids.forEach(id => { + pipeline.hgetallBuffer(redisKey(id, USERS_METADATA, audience)); + }); + return Promise.all([ids, pipeline.exec(), length]); + }).spread((ids, props, length) => { + const users = ids.map(function remapData(id, idx) { + const data = props[idx][1]; + const account = { + id, + metadata: { + [audience]: data ? mapValues(data, JSONParse) : {} + } + }; + + return account; + }); + + return { + users, + cursor: offset + limit, + page: Math.floor(offset / limit + 1), + pages: Math.ceil(length / limit) + }; + }); + }, + + /** + * Check existence of alias + * @param data + * @returns {boolean} + */ + isAliasAssigned(data) { + return data[USERS_ALIAS_FIELD] !== undefined; // was just `data[USERS_ALIAS_FIELD]` + }, + + /** + * Check that user is admin + * @param meta + * @returns {boolean} + */ + isAdmin(meta) { + const audience = config.jwt.defaultAudience; + return (meta[audience].roles || []).indexOf(USERS_ADMIN_ROLE) >= 0; + }, + + /** + * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN + * @param username + * @param alias + * @returns {Redis} + */ + storeAlias(username, alias) { + return redis.hsetnx(USERS_ALIAS_TO_LOGIN, alias, username); + }, + + /** + * Assign alias to the user record, marked by username + * @param username + * @param alias + * @returns {Redis} + */ + assignAlias(username, alias) { + return redis.pipeline().sadd(USERS_PUBLIC_INDEX, username).hset(generateKey(username, USERS_DATA), USERS_ALIAS_FIELD, alias).hset(generateKey(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, stringify).exec(); + }, + + _remoteipKey: '', + get remoteipKey() { + return this._remoteipKey; + }, + + set remoteipKey(val) { + this._remoteipKey = val; + }, + + generateipKey(username, remoteip) { + return this._remoteipKey = generateKey(username, 'ip', remoteip); + }, + + _loginAttempts: 0, + get loginAttempts() { + return this._loginAttempts; + }, + set loginAttempts(val) { + this._loginAttempts = val; + }, + + _options: {}, + get options() { + return this._options; + }, + + set options(opts) { + this._options = opts; + }, + + dropAttempts() { + this._loginAttempts = 0; + return redis.del(this.key); + }, + checkLoginAttempts(data) { + const pipeline = redis.pipeline(); + const username = data.username; + const remoteipKey = this.generateipKey(username, this._options.remoteip); + + pipeline.incrby(remoteipKey, 1); + if (config.jwt.keepLoginAttempts > 0) { + pipeline.expire(remoteipKey, config.jwt.keepLoginAttempts); + } + + return pipeline.exec().spread(function incremented(incrementValue) { + const err = incrementValue[0]; + if (err) { + this.log.error('Redis error:', err); + return; + } + + this.loginAttempts = incrementValue[1]; + if (this.loginAttempts > lockAfterAttempts) { + const duration = moment().add(config.jwt.keepLoginAttempts, 'seconds').toNow(true); + const msg = `You are locked from making login attempts for the next ${ duration }`; + throw new Errors.HttpStatusError(429, msg); + } + }); + }, + + /** + * Set user password + * @param username + * @param hash + * @returns {Redis} + */ + setPassword(username, hash) { + return redis.hset(generateKey(username, USERS_DATA), 'password', hash).return(username); + }, + + /** + * Reset the lock by IP + * @param username + * @param ip + * @returns {Redis} + */ + resetIPLock(username, ip) { + return redis.del(generateKey(username, 'ip', ip)); + }, + + /** + * + * @param username + * @param audience + * @param metadata + * @returns {Object} + */ + updateMetadata({ username, audience, metadata, script }) { + const audiences = is.array(audience) ? audience : [audience]; + + // keys + const keys = audiences.map(aud => redisKey(username, USERS_METADATA, aud)); + + // if we have meta, then we can + if (metadata) { + const pipe = redis.pipeline(); + const metaOps = is.array(metadata) ? metadata : [metadata]; + const operations = metaOps.map((meta, idx) => handleAudience(pipe, keys[idx], meta)); + return pipe.exec().then(res => mapMetaResponse(operations, res)); + } + + //or... + return this.customScript(script); + }, + + /** + * Removing user by username (and data?) + * @param username + * @param data + * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} + */ + removeUser(username, data) { + const audience = config.jwt.defaultAudience; + const transaction = redis.multi(); + const alias = data[USERS_ALIAS_FIELD]; + if (alias) { + transaction.hdel(USERS_ALIAS_TO_LOGIN, alias); + } + + // clean indices + transaction.srem(USERS_PUBLIC_INDEX, username); + transaction.srem(USERS_INDEX, username); + + // remove metadata & internal data + transaction.del(generateKey(username, USERS_DATA)); + transaction.del(generateKey(username, USERS_METADATA, audience)); + + // remove auth tokens + transaction.del(generateKey(username, USERS_TOKENS)); + + // complete it + return transaction.exec(); + }, + + /** + * Verify ip limits + * @param {redisCluster} redis + * @param {Object} registrationLimits + * @param {String} ipaddress + * @return {Function} + */ + checkLimits(registrationLimits, ipaddress) { + const { ip: { time, times } } = registrationLimits; + const ipaddressLimitKey = generateKey('reg-limit', ipaddress); + const now = Date.now(); + const old = now - time; + + return function iplimits() { + return redis.pipeline().zadd(ipaddressLimitKey, now, uuid.v4()).pexpire(ipaddressLimitKey, time).zremrangebyscore(ipaddressLimitKey, '-inf', old).zcard(ipaddressLimitKey).exec().then(props => { + const cardinality = props[3][1]; + if (cardinality > times) { + const msg = 'You can\'t register more users from your ipaddress now'; + throw new Errors.HttpStatusError(429, msg); + } + }); + }; + }, + + /** + * Creates user with a given hash + * @param redis + * @param username + * @param activate + * @param deleteInactiveAccounts + * @param userDataKey + * @returns {Function} + */ + createUser(username, activate, deleteInactiveAccounts) { + /** + * Input from scrypt.hash + */ + const userDataKey = generateKey(username, USERS_DATA); + + return function create(hash) { + const pipeline = redis.pipeline(); + + pipeline.hsetnx(userDataKey, 'password', hash); + pipeline.hsetnx(userDataKey, USERS_ACTIVE_FLAG, activate); + + return pipeline.exec().spread(function insertedUserData(passwordSetResponse) { + if (passwordSetResponse[1] === 0) { + throw new Errors.HttpStatusError(412, `User "${ username }" already exists`); + } + + if (!activate && deleteInactiveAccounts >= 0) { + // WARNING: IF USER IS NOT VERIFIED WITHIN + // [by default 30] DAYS - IT WILL BE REMOVED FROM DATABASE + return redis.expire(userDataKey, deleteInactiveAccounts); + } + + return null; + }); + }; + }, + + /** + * Performs captcha check, returns thukn + * @param {String} username + * @param {String} captcha + * @param {Object} captchaConfig + * @return {Function} + */ + checkCaptcha(username, captcha) { + const { secret, ttl, uri } = captchaConfig; + return function checkCaptcha() { + const captchaCacheKey = captcha.response; + return redis.pipeline().set(captchaCacheKey, username, 'EX', ttl, 'NX').get(captchaCacheKey).exec().spread(function captchaCacheResponse(setResponse, getResponse) { + if (getResponse[1] !== username) { + const msg = 'Captcha challenge you\'ve solved can not be used, please complete it again'; + throw new Errors.HttpStatusError(412, msg); + } + }).then(function verifyGoogleCaptcha() { + return request.post({ uri, qs: defaults(captcha, { secret }), json: true }).then(function captchaSuccess(body) { + if (!body.success) { + return Promise.reject({ statusCode: 200, error: body }); + } + + return true; + }).catch(function captchaError(err) { + const errData = JSON.stringify(pick(err, ['statusCode', 'error'])); + throw new Errors.HttpStatusError(412, fmt('Captcha response: %s', errData)); + }); + }); + }; + }, + + /** + * Stores username to the index set + * @param username + * @returns {Redis} + */ + storeUsername(username) { + return redis.sadd(USERS_INDEX, username); + }, + + /** + * Execute custom script on LUA + * @param script + * @returns {Promise} + */ + customScript(script) { + // dynamic scripts + const $scriptKeys = Object.keys(script); + const scripts = $scriptKeys.map(scriptName => { + const { lua, argv = [] } = script[scriptName]; + const sha = sha256(lua); + const name = `ms_users_${ sha }`; + if (!is.fn(redis[name])) { + redis.defineCommand(name, { lua }); + } + return redis[name](keys.length, keys, argv); + }); + + return Promise.all(scripts).then(res => { + const output = {}; + $scriptKeys.forEach((fieldName, idx) => { + output[fieldName] = res[idx]; + }); + return output; + }); + }, + + handleAudience(key, metadata) { + const pipeline = redis.pipeline(); + const $remove = metadata.$remove; + const $removeOps = $remove && $remove.length || 0; + if ($removeOps > 0) { + pipeline.hdel(key, $remove); + } + + const $set = metadata.$set; + const $setKeys = $set && Object.keys($set); + const $setLength = $setKeys && $setKeys.length || 0; + if ($setLength > 0) { + pipeline.hmset(key, mapValues($set, stringify)); + } + + const $incr = metadata.$incr; + const $incrFields = $incr && Object.keys($incr); + const $incrLength = $incrFields && $incrFields.length || 0; + if ($incrLength > 0) { + $incrFields.forEach(fieldName => { + pipeline.hincrby(key, fieldName, $incr[fieldName]); + }); + } + + return { $removeOps, $setLength, $incrLength, $incrFields }; + } + +}; + +//# sourceMappingURL=redisstorage-compiled.js.map \ No newline at end of file diff --git a/src/db/redisstorage-compiled.js.map b/src/db/redisstorage-compiled.js.map new file mode 100644 index 000000000..45a5ac4b0 --- /dev/null +++ b/src/db/redisstorage-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["redisstorage.js"],"names":[],"mappings":";;;;;;;AAGA,MAAM,UAAU,QAAQ,UAAR,CAAV;AACN,MAAM,SAAS,QAAQ,eAAR,CAAT;AACN,MAAM,YAAY,QAAQ,kBAAR,CAAZ;AACN,MAAM,WAAW,QAAQ,iBAAR,CAAX;AACN,MAAM,MAAM,QAAQ,YAAR,CAAN;AACN,MAAM,OAAO,QAAQ,aAAR,CAAP;AACN,MAAM,UAAU,QAAQ,iBAAR,CAAV;AACN,MAAM,OAAO,QAAQ,WAAR,CAAP;AACN,MAAM,QAAQ,QAAQ,qBAAR,CAAR;AACN,MAAM,MAAM,QAAQ,MAAR,EAAgB,MAAhB;AACZ,MAAM,KAAK,QAAQ,IAAR,CAAL;AACN,MAAM,SAAS,QAAQ,aAAR,CAAT;AACN,MAAM,SAAS,QAAQ,QAAR,CAAT;;AAEN,MAAM,YAAY,KAAK,SAAL,CAAe,IAAf,CAAoB,IAApB,CAAZ;AACN,MAAM;AACJ,YADI,EACQ,cADR,EACwB,oBADxB;AAEJ,mBAFI,EAEe,YAFf,EAE6B,iBAF7B;AAGJ,mBAHI,EAGe,WAHf,EAG4B,kBAH5B;AAIJ,mBAJI;IAKF,QAAQ,iBAAR,CALE;;AAON,MAAM,EAAE,KAAF,EAAS,SAAS,aAAT,EAAwB,MAAjC,KAA4C,IAA5C;AACN,MAAM,EAAE,KAAK,EAAE,iBAAF,EAAqB,eAArB,EAAL,EAAF,GAAkD,MAAlD;;;;;;;AAQN,MAAM,cAAc,CAAC,GAAG,IAAH,KAAY;AAC/B,QAAM,YAAY,GAAZ,CADyB;AAE/B,SAAO,KAAK,IAAL,CAAU,SAAV,CAAP,CAF+B;CAAb;;AAKpB,OAAO,OAAP,GAAiB;;;;;;;;;AASf,WAAS,EAAE,QAAF,EAAY,MAAZ,EAAoB,IAApB,EAA0B,QAA1B,EAAT,EAA8C;AAC5C,UAAM,OAAO;AACX,cAAQ,IAAR;AACA,OAAC,iBAAD,GAAqB;AACnB,cADmB;AAEnB,YAFmB;AAGnB,gBAHmB;OAArB;KAFI,CADsC;;AAU5C,WAAO,MACJ,QADI,GAEJ,IAFI,CAEC,YAAY,QAAZ,EAAsB,UAAtB,CAFD,EAEoC,iBAFpC,EAEuD,MAFvD;;KAIJ,KAJI,CAIE,YAAY,QAAZ,EAAsB,cAAtB,EAAsC,eAAtC,CAJF,EAI0D,UAAU,IAAV,EAAgB,SAAhB,CAJ1D,EAKJ,GALI,CAKA,YAAY,QAAZ,EAAsB,YAAtB,CALA,EAMJ,IANI,EAAP,CAV4C;GAA9C;;;;;;;AAwBA,aAAW,EAAC,QAAD,EAAX,EAAsB;AACpB,WAAO,MACJ,QADI,GAEJ,IAFI,CAEC,YAAY,QAAZ,EAAsB,UAAtB,CAFD,EAEoC,iBAFpC;;KAIJ,IAJI,CAIC,YAAY,QAAZ,EAAsB,cAAtB,EAAsC,eAAtC,CAJD,EAIyD,QAJzD,EAImE,iBAJnE,EAKJ,IALI,EAAP,CADoB;GAAtB;;;;;;;AAeA,WAAS,QAAT,EAAkB;AAChB,WAAO,MACJ,QADI,GAEJ,IAFI,CAEC,oBAFD,EAEuB,QAFvB,EAGJ,MAHI,CAGG,YAAY,QAAZ,EAAsB,UAAtB,CAHH,EAIJ,IAJI,GAKJ,MALI,CAKG,CAAC,KAAD,EAAQ,MAAR,KAAmB;AACzB,UAAI,MAAM,CAAN,CAAJ,EAAc;AACZ,eAAO,MAAM,CAAN,CAAP,CADY;OAAd;;AAIA,UAAI,CAAC,OAAO,CAAP,CAAD,EAAY;AACd,cAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,CAAC,CAAD,GAAI,QAAJ,EAAa,iBAAb,CAAhC,CAAN,CADc;OAAhB;;AAIA,aAAO,QAAP,CATyB;KAAnB,CALV,CADgB;GAAlB;;AAmBA,gBAAc,KAAd,EAAqB,KAArB,EAA2B;AACzB,aAAS,YAAT,CAAsB,KAAtB,EAA6B;AAC3B,aAAO,MACJ,IADI,CACC,oBADD,EACuB,KADvB,EAEJ,IAFI,CAEC,YAAY;AAChB,YAAI,QAAJ,EAAc;AACZ,gBAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,CAAC,CAAD,GAAI,KAAJ,EAAU,gBAAV,CAAhC,CAAN,CADY;SAAd;;AAIA,eAAO,QAAP,CALgB;OAAZ,CAFR,CAD2B;KAA7B;AAWA,QAAI,KAAJ,EAAW;AACT,aAAO,SAAS,iBAAT,GAA6B;AAClC,eAAO,aAAa,KAAb,CAAP,CADkC;OAA7B,CADE;KAAX;;AAMA,WAAO,aAAa,KAAb,CAAP,CAlByB;GAA3B;;;;;;;;AA2BA,WAAS,QAAT,EAAmB,QAAnB,EAA6B;AAC3B,WAAO,YAAY;AACjB,UAAI,IAAI,QAAJ,EAAc,CAAC,QAAD,EAAW,iBAAX,CAAd,MAAiD,QAAjD,EAA2D;AAC7D,eAD6D;OAA/D;;AAIA,YAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,wBAAhC,CAAN,CALiB;KAAZ,CADoB;GAA7B;;;;;;;AAgBA,WAAS,IAAT,EAAc;AACZ,QAAI,OAAO,KAAK,iBAAL,CAAP,MAAoC,MAApC,EAA4C;AAC9C,aAAO,QAAQ,MAAR,CAAe,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,gCAAhC,CAAf,CAAP,CAD8C;KAAhD;;AAIA,WAAO,QAAQ,OAAR,CAAgB,IAAhB,CAAP,CALY;GAAd;;;;;;;AAaA,WAAS,IAAT,EAAc;AACZ,QAAG,OAAO,KAAK,iBAAL,CAAP,MAAoC,MAApC,EAA4C;AAC7C,aAAO,QAAQ,MAAR,CAAe,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,yBAAhC,CAAf,CAAP,CAD6C;KAA/C;;AAIA,WAAO,QAAQ,OAAR,CAAgB,IAAhB,CAAP,CALY;GAAd;;;;;;;AAaA,kBAAgB,IAAhB,EAAqB;AACnB,UAAM,UAAU,YAAY,IAAZ,EAAkB,UAAlB,CAAV;;;;AADa,WAKZ,MACJ,QADI,GAEJ,IAFI,CAEC,OAFD,EAEU,iBAFV,EAGJ,IAHI,CAGC,OAHD,EAGU,iBAHV,EAG6B,MAH7B,EAIJ,OAJI,CAII,OAJJ,EAKJ,IALI,CAKC,WALD,EAKc,IALd,EAMJ,IANI,GAOJ,MAPI,CAOG,SAAS,YAAT,CAAsB,QAAtB,EAAgC;AACtC,YAAM,SAAS,SAAS,CAAT,CAAT,CADgC;AAEtC,UAAI,WAAW,MAAX,EAAmB;AACrB,cAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,CAAC,QAAD,GAAW,IAAX,EAAgB,sBAAhB,CAAhC,CAAN,CADqB;OAAvB;KAFM,CAPV,CALmB;GAArB;;;;;;;AAyBA,UAAQ,QAAR,EAAiB;AACf,UAAM,UAAU,YAAY,QAAZ,EAAsB,UAAtB,CAAV,CADS;;AAGf,WAAO,MACJ,QADI,GAEJ,IAFI,CAEC,oBAFD,EAEuB,QAFvB,EAGJ,MAHI,CAGG,OAHH,EAIJ,aAJI,CAIU,OAJV,EAKJ,IALI,GAMJ,MANI,CAMG,CAAC,eAAD,EAAkB,MAAlB,EAA0B,IAA1B,KAAmC;AACzC,UAAI,gBAAgB,CAAhB,CAAJ,EAAwB;AACtB,eAAQ,KAAK,OAAL,CAAa,gBAAgB,CAAhB,CAAb,CAAR,CADsB;OAAxB;;AAIA,UAAI,CAAC,OAAO,CAAP,CAAD,EAAY;AACd,cAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,CAAC,CAAD,GAAI,QAAJ,EAAa,iBAAb,CAAhC,CAAN,CADc;OAAhB;;AAIA,0BAAY,KAAK,CAAL,KAAS,WAArB,CATyC;KAAnC,CANV,CAHe;GAAjB;;;;;;;;;;;;;AAiCA,cAAY,QAAZ,EAAsB,UAAtB,EAAkC,SAAS,EAAT,EAAa;AAC7C,UAAM,YAAY,MAAM,OAAN,CAAc,UAAd,IAA4B,UAA5B,GAAyC,CAAC,UAAD,CAAzC,CAD2B;;AAG7C,WAAO,QAAQ,GAAR,CAAY,SAAZ,EAAuB,YAAY;AACxC,aAAO,MAAM,aAAN,CAAoB,YAAY,QAAZ,EAAsB,cAAtB,EAAsC,QAAtC,CAApB,CAAP,CADwC;KAAZ,CAAvB,CAGJ,IAHI,CAGC,SAAS,iBAAT,CAA2B,IAA3B,EAAiC;AACrC,YAAM,SAAS,EAAT,CAD+B;AAErC,gBAAU,OAAV,CAAkB,SAAS,SAAT,CAAmB,GAAnB,EAAwB,GAAxB,EAA6B;AAC7C,cAAM,QAAQ,KAAK,GAAL,CAAR,CADuC;;AAG7C,YAAI,KAAJ,EAAW;AACT,gBAAM,aAAa,OAAO,GAAP,CAAb,CADG;AAET,iBAAO,GAAP,IAAc,UAAU,KAAV,EAAiB,SAAjB,CAAd,CAFS;AAGT,cAAI,UAAJ,EAAgB;AACd,mBAAO,GAAP,IAAc,KAAK,OAAO,GAAP,CAAL,EAAkB,UAAlB,CAAd,CADc;WAAhB;SAHF,MAMO;AACL,iBAAO,GAAP,IAAc,EAAd,CADK;SANP;OAHgB,CAAlB,CAFqC;;AAgBrC,aAAO,MAAP,CAhBqC;KAAjC,CAHR,CAH6C;GAA/C;;;;;;;AAgCA,UAAQ,IAAR,EAAa;AACX,UAAM,EAAE,QAAF,EAAY,QAAZ,EAAsB,MAAtB,EAA8B,KAA9B,EAAqC,SAArC,EAAgD,KAAhD,EAAuD,MAAvD,EAA+D,KAA/D,KAAyE,IAAzE,CADK;AAEX,UAAM,UAAU,YAAY,GAAZ,EAAiB,cAAjB,EAAiC,QAAjC,CAAV,CAFK;;AAIX,WAAO,MACJ,KADI,CACE,KADF,EACS,OADT,EACkB,QADlB,EAC4B,KAD5B,EACmC,SADnC,EAC8C,MAD9C,EACsD,KADtD,EAEJ,IAFI,CAEC,OAAO;AACX,YAAM,SAAS,CAAC,IAAI,GAAJ,EAAD,CADJ;AAEX,UAAI,WAAW,CAAX,IAAgB,IAAI,MAAJ,KAAe,CAAf,EAAkB;AACpC,eAAO,CACL,OAAO,EAAP,EACA,EAFK,EAGL,MAHK,CAAP,CADoC;OAAtC;;AAQA,YAAM,WAAW,MAAM,QAAN,EAAX,CAVK;AAWX,UAAI,OAAJ,CAAY,MAAM;AAChB,iBAAS,aAAT,CAAuB,SAAS,EAAT,EAAa,cAAb,EAA6B,QAA7B,CAAvB,EADgB;OAAN,CAAZ,CAXW;AAcX,aAAO,QAAQ,GAAR,CAAY,CACjB,GADiB,EAEjB,SAAS,IAAT,EAFiB,EAGjB,MAHiB,CAAZ,CAAP,CAdW;KAAP,CAFD,CAsBJ,MAtBI,CAsBG,CAAC,GAAD,EAAM,KAAN,EAAa,MAAb,KAAwB;AAC9B,YAAM,QAAQ,IAAI,GAAJ,CAAQ,SAAS,SAAT,CAAmB,EAAnB,EAAuB,GAAvB,EAA4B;AAChD,cAAM,OAAO,MAAM,GAAN,EAAW,CAAX,CAAP,CAD0C;AAEhD,cAAM,UAAU;AACd,YADc;AAEd,oBAAU;AACR,aAAC,QAAD,GAAY,OAAO,UAAU,IAAV,EAAgB,SAAhB,CAAP,GAAoC,EAApC;WADd;SAFI,CAF0C;;AAShD,eAAO,OAAP,CATgD;OAA5B,CAAhB,CADwB;;AAa9B,aAAO;AACL,aADK;AAEL,gBAAQ,SAAS,KAAT;AACR,cAAM,KAAK,KAAL,CAAW,SAAS,KAAT,GAAiB,CAAjB,CAAjB;AACA,eAAO,KAAK,IAAL,CAAU,SAAS,KAAT,CAAjB;OAJF,CAb8B;KAAxB,CAtBV,CAJW;GAAb;;;;;;;AAqDA,kBAAgB,IAAhB,EAAqB;AACnB,WAAO,KAAK,iBAAL,MAA4B,SAA5B;AADY,GAArB;;;;;;;AASA,UAAQ,IAAR,EAAa;AACX,UAAM,WAAW,OAAO,GAAP,CAAW,eAAX,CADN;AAEX,WAAM,CAAC,KAAK,QAAL,EAAe,KAAf,IAAwB,EAAxB,CAAD,CAA6B,OAA7B,CAAqC,gBAArC,KAA0D,CAA1D,CAFK;GAAb;;;;;;;;AAWA,aAAW,QAAX,EAAqB,KAArB,EAA2B;AACzB,WAAO,MAAM,MAAN,CAAa,oBAAb,EAAmC,KAAnC,EAA0C,QAA1C,CAAP,CADyB;GAA3B;;;;;;;;AAUA,cAAY,QAAZ,EAAsB,KAAtB,EAA4B;AAC1B,WAAO,MACJ,QADI,GAEJ,IAFI,CAEC,kBAFD,EAEqB,QAFrB,EAGJ,IAHI,CAGC,YAAY,QAAZ,EAAsB,UAAtB,CAHD,EAGoC,iBAHpC,EAGuD,KAHvD,EAIJ,IAJI,CAIC,YAAY,QAAZ,EAAsB,cAAtB,EAAsC,eAAtC,CAJD,EAIyD,iBAJzD,EAI4E,SAJ5E,EAKJ,IALI,EAAP,CAD0B;GAA5B;;AASA,gBAAc,EAAd;AACA,MAAI,WAAJ,GAAiB;AACf,WAAO,KAAK,YAAL,CADQ;GAAjB;;AAIA,MAAI,WAAJ,CAAgB,GAAhB,EAAoB;AAClB,SAAK,YAAL,GAAoB,GAApB,CADkB;GAApB;;AAIA,gBAAc,QAAd,EAAwB,QAAxB,EAAiC;AAC/B,WAAO,KAAK,YAAL,GAAoB,YAAY,QAAZ,EAAsB,IAAtB,EAA4B,QAA5B,CAApB,CADwB;GAAjC;;AAIA,kBAAgB,CAAhB;AACA,MAAI,aAAJ,GAAmB;AACjB,WAAO,KAAK,cAAL,CADU;GAAnB;AAGA,MAAI,aAAJ,CAAkB,GAAlB,EAAsB;AACpB,SAAK,cAAL,GAAsB,GAAtB,CADoB;GAAtB;;AAIA,YAAU,EAAV;AACA,MAAI,OAAJ,GAAa;AACX,WAAO,KAAK,QAAL,CADI;GAAb;;AAIA,MAAI,OAAJ,CAAY,IAAZ,EAAiB;AACf,SAAK,QAAL,GAAgB,IAAhB,CADe;GAAjB;;AAIA,iBAAc;AACZ,SAAK,cAAL,GAAsB,CAAtB,CADY;AAEZ,WAAO,MAAM,GAAN,CAAU,KAAK,GAAL,CAAjB,CAFY;GAAd;AAIA,qBAAmB,IAAnB,EAAyB;AACvB,UAAM,WAAW,MAAM,QAAN,EAAX,CADiB;AAEvB,UAAM,WAAW,KAAK,QAAL,CAFM;AAGvB,UAAM,cAAc,KAAK,aAAL,CAAmB,QAAnB,EAA6B,KAAK,QAAL,CAAc,QAAd,CAA3C,CAHiB;;AAKvB,aAAS,MAAT,CAAgB,WAAhB,EAA6B,CAA7B,EALuB;AAMvB,QAAI,OAAO,GAAP,CAAW,iBAAX,GAA+B,CAA/B,EAAkC;AACpC,eAAS,MAAT,CAAgB,WAAhB,EAA6B,OAAO,GAAP,CAAW,iBAAX,CAA7B,CADoC;KAAtC;;AAIA,WAAO,SACJ,IADI,GAEJ,MAFI,CAEG,SAAS,WAAT,CAAqB,cAArB,EAAqC;AAC3C,YAAM,MAAM,eAAe,CAAf,CAAN,CADqC;AAE3C,UAAI,GAAJ,EAAS;AACP,aAAK,GAAL,CAAS,KAAT,CAAe,cAAf,EAA+B,GAA/B,EADO;AAEP,eAFO;OAAT;;AAKA,WAAK,aAAL,GAAqB,eAAe,CAAf,CAArB,CAP2C;AAQ3C,UAAI,KAAK,aAAL,GAAqB,iBAArB,EAAwC;AAC1C,cAAM,WAAW,SAAS,GAAT,CAAa,OAAO,GAAP,CAAW,iBAAX,EAA8B,SAA3C,EAAsD,KAAtD,CAA4D,IAA5D,CAAX,CADoC;AAE1C,cAAM,MAAM,CAAC,uDAAD,GAA0D,QAA1D,EAAmE,CAAzE,CAFoC;AAG1C,cAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,GAAhC,CAAN,CAH0C;OAA5C;KARM,CAFV,CAVuB;GAAzB;;;;;;;;AAkCA,cAAY,QAAZ,EAAsB,IAAtB,EAA2B;AACzB,WAAO,MACJ,IADI,CACC,YAAY,QAAZ,EAAsB,UAAtB,CADD,EACoC,UADpC,EACgD,IADhD,EAEJ,MAFI,CAEG,QAFH,CAAP,CADyB;GAA3B;;;;;;;;AAYA,cAAY,QAAZ,EAAsB,EAAtB,EAAyB;AACvB,WAAO,MAAM,GAAN,CAAU,YAAY,QAAZ,EAAsB,IAAtB,EAA4B,EAA5B,CAAV,CAAP,CADuB;GAAzB;;;;;;;;;AAWA,iBAAe,EAAE,QAAF,EAAY,QAAZ,EAAsB,QAAtB,EAAgC,MAAhC,EAAf,EAAyD;AACvD,UAAM,YAAY,GAAG,KAAH,CAAS,QAAT,IAAqB,QAArB,GAAgC,CAAC,QAAD,CAAhC;;;AADqC,UAIjD,OAAO,UAAU,GAAV,CAAc,OAAO,SAAS,QAAT,EAAmB,cAAnB,EAAmC,GAAnC,CAAP,CAArB;;;AAJiD,QAOnD,QAAJ,EAAc;AACZ,YAAM,OAAO,MAAM,QAAN,EAAP,CADM;AAEZ,YAAM,UAAU,GAAG,KAAH,CAAS,QAAT,IAAqB,QAArB,GAAgC,CAAC,QAAD,CAAhC,CAFJ;AAGZ,YAAM,aAAa,QAAQ,GAAR,CAAY,CAAC,IAAD,EAAO,GAAP,KAAe,eAAe,IAAf,EAAqB,KAAK,GAAL,CAArB,EAAgC,IAAhC,CAAf,CAAzB,CAHM;AAIZ,aAAO,KAAK,IAAL,GAAY,IAAZ,CAAiB,OAAO,gBAAgB,UAAhB,EAA4B,GAA5B,CAAP,CAAxB,CAJY;KAAd;;;AAPuD,WAehD,KAAK,YAAL,CAAkB,MAAlB,CAAP,CAfuD;GAAzD;;;;;;;;AAwBA,aAAW,QAAX,EAAqB,IAArB,EAA0B;AACxB,UAAM,WAAW,OAAO,GAAP,CAAW,eAAX,CADO;AAExB,UAAM,cAAc,MAAM,KAAN,EAAd,CAFkB;AAGxB,UAAM,QAAQ,KAAK,iBAAL,CAAR,CAHkB;AAIxB,QAAI,KAAJ,EAAW;AACT,kBAAY,IAAZ,CAAiB,oBAAjB,EAAuC,KAAvC,EADS;KAAX;;;AAJwB,eASxB,CAAY,IAAZ,CAAiB,kBAAjB,EAAqC,QAArC,EATwB;AAUxB,gBAAY,IAAZ,CAAiB,WAAjB,EAA8B,QAA9B;;;AAVwB,eAaxB,CAAY,GAAZ,CAAgB,YAAY,QAAZ,EAAsB,UAAtB,CAAhB,EAbwB;AAcxB,gBAAY,GAAZ,CAAgB,YAAY,QAAZ,EAAsB,cAAtB,EAAsC,QAAtC,CAAhB;;;AAdwB,eAiBxB,CAAY,GAAZ,CAAgB,YAAY,QAAZ,EAAsB,YAAtB,CAAhB;;;AAjBwB,WAoBjB,YAAY,IAAZ,EAAP,CApBwB;GAA1B;;;;;;;;;AA8BA,cAAY,kBAAZ,EAAgC,SAAhC,EAA2C;AACzC,UAAM,EAAC,IAAI,EAAC,IAAD,EAAO,KAAP,EAAJ,EAAD,GAAsB,kBAAtB,CADmC;AAEzC,UAAM,oBAAoB,YAAY,WAAZ,EAAyB,SAAzB,CAApB,CAFmC;AAGzC,UAAM,MAAM,KAAK,GAAL,EAAN,CAHmC;AAIzC,UAAM,MAAM,MAAM,IAAN,CAJ6B;;AAMzC,WAAO,SAAS,QAAT,GAAoB;AACzB,aAAO,MACJ,QADI,GAEJ,IAFI,CAEC,iBAFD,EAEoB,GAFpB,EAEyB,KAAK,EAAL,EAFzB,EAGJ,OAHI,CAGI,iBAHJ,EAGuB,IAHvB,EAIJ,gBAJI,CAIa,iBAJb,EAIgC,MAJhC,EAIwC,GAJxC,EAKJ,KALI,CAKE,iBALF,EAMJ,IANI,GAOJ,IAPI,CAOC,SAAS;AACb,cAAM,cAAc,MAAM,CAAN,EAAS,CAAT,CAAd,CADO;AAEb,YAAI,cAAc,KAAd,EAAqB;AACvB,gBAAM,MAAM,wDAAN,CADiB;AAEvB,gBAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,GAAhC,CAAN,CAFuB;SAAzB;OAFI,CAPR,CADyB;KAApB,CANkC;GAA3C;;;;;;;;;;;AAiCA,aAAW,QAAX,EAAqB,QAArB,EAA+B,sBAA/B,EAAuD;;;;AAIrD,UAAM,cAAc,YAAY,QAAZ,EAAsB,UAAtB,CAAd,CAJ+C;;AAMrD,WAAO,SAAS,MAAT,CAAgB,IAAhB,EAAsB;AAC3B,YAAM,WAAW,MAAM,QAAN,EAAX,CADqB;;AAG3B,eAAS,MAAT,CAAgB,WAAhB,EAA6B,UAA7B,EAAyC,IAAzC,EAH2B;AAI3B,eAAS,MAAT,CAAgB,WAAhB,EAA6B,iBAA7B,EAAgD,QAAhD,EAJ2B;;AAM3B,aAAO,SACJ,IADI,GAEJ,MAFI,CAEG,SAAS,gBAAT,CAA0B,mBAA1B,EAA+C;AACrD,YAAI,oBAAoB,CAApB,MAA2B,CAA3B,EAA8B;AAChC,gBAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,CAAC,MAAD,GAAS,QAAT,EAAkB,gBAAlB,CAAhC,CAAN,CADgC;SAAlC;;AAIA,YAAI,CAAC,QAAD,IAAa,0BAA0B,CAA1B,EAA6B;;;AAG5C,iBAAO,MAAM,MAAN,CAAa,WAAb,EAA0B,sBAA1B,CAAP,CAH4C;SAA9C;;AAMA,eAAO,IAAP,CAXqD;OAA/C,CAFV,CAN2B;KAAtB,CAN8C;GAAvD;;;;;;;;;AAqCA,eAAa,QAAb,EAAuB,OAAvB,EAAgC;AAC9B,UAAM,EAAC,MAAD,EAAS,GAAT,EAAc,GAAd,KAAqB,aAArB,CADwB;AAE9B,WAAO,SAAS,YAAT,GAAwB;AAC7B,YAAM,kBAAkB,QAAQ,QAAR,CADK;AAE7B,aAAO,MACJ,QADI,GAEJ,GAFI,CAEA,eAFA,EAEiB,QAFjB,EAE2B,IAF3B,EAEiC,GAFjC,EAEsC,IAFtC,EAGJ,GAHI,CAGA,eAHA,EAIJ,IAJI,GAKJ,MALI,CAKG,SAAS,oBAAT,CAA8B,WAA9B,EAA2C,WAA3C,EAAwD;AAC9D,YAAI,YAAY,CAAZ,MAAmB,QAAnB,EAA6B;AAC/B,gBAAM,MAAM,4EAAN,CADyB;AAE/B,gBAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,GAAhC,CAAN,CAF+B;SAAjC;OADM,CALH,CAWJ,IAXI,CAWC,SAAS,mBAAT,GAA+B;AACnC,eAAO,QACJ,IADI,CACC,EAAC,GAAD,EAAM,IAAI,SAAS,OAAT,EAAkB,EAAC,MAAD,EAAlB,CAAJ,EAAiC,MAAM,IAAN,EADxC,EAEJ,IAFI,CAEC,SAAS,cAAT,CAAwB,IAAxB,EAA8B;AAClC,cAAI,CAAC,KAAK,OAAL,EAAc;AACjB,mBAAO,QAAQ,MAAR,CAAe,EAAC,YAAY,GAAZ,EAAiB,OAAO,IAAP,EAAjC,CAAP,CADiB;WAAnB;;AAIA,iBAAO,IAAP,CALkC;SAA9B,CAFD,CASJ,KATI,CASE,SAAS,YAAT,CAAsB,GAAtB,EAA2B;AAChC,gBAAM,UAAU,KAAK,SAAL,CAAe,KAAK,GAAL,EAAU,CAAC,YAAD,EAAe,OAAf,CAAV,CAAf,CAAV,CAD0B;AAEhC,gBAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,IAAI,sBAAJ,EAA4B,OAA5B,CAAhC,CAAN,CAFgC;SAA3B,CATT,CADmC;OAA/B,CAXR,CAF6B;KAAxB,CAFuB;GAAhC;;;;;;;AAsCA,gBAAc,QAAd,EAAuB;AACrB,WAAO,MAAM,IAAN,CAAW,WAAX,EAAwB,QAAxB,CAAP,CADqB;GAAvB;;;;;;;AASA,eAAa,MAAb,EAAoB;;AAElB,UAAM,cAAc,OAAO,IAAP,CAAY,MAAZ,CAAd,CAFY;AAGlB,UAAM,UAAU,YAAY,GAAZ,CAAgB,cAAc;AAC5C,YAAM,EAAE,GAAF,EAAO,OAAO,EAAP,EAAP,GAAqB,OAAO,UAAP,CAArB,CADsC;AAE5C,YAAM,MAAM,OAAO,GAAP,CAAN,CAFsC;AAG5C,YAAM,OAAO,CAAC,SAAD,GAAY,GAAZ,EAAgB,CAAvB,CAHsC;AAI5C,UAAI,CAAC,GAAG,EAAH,CAAM,MAAM,IAAN,CAAN,CAAD,EAAqB;AACvB,cAAM,aAAN,CAAoB,IAApB,EAA0B,EAAE,GAAF,EAA1B,EADuB;OAAzB;AAGA,aAAO,MAAM,IAAN,EAAY,KAAK,MAAL,EAAa,IAAzB,EAA+B,IAA/B,CAAP,CAP4C;KAAd,CAA1B,CAHY;;AAalB,WAAO,QAAQ,GAAR,CAAY,OAAZ,EAAqB,IAArB,CAA0B,OAAO;AACpC,YAAM,SAAS,EAAT,CAD8B;AAEpC,kBAAY,OAAZ,CAAoB,CAAC,SAAD,EAAY,GAAZ,KAAoB;AACtC,eAAO,SAAP,IAAoB,IAAI,GAAJ,CAApB,CADsC;OAApB,CAApB,CAFoC;AAKpC,aAAO,MAAP,CALoC;KAAP,CAAjC,CAbkB;GAApB;;AAsBA,iBAAe,GAAf,EAAoB,QAApB,EAA8B;AAC5B,UAAM,WAAW,MAAM,QAAN,EAAX,CADsB;AAE5B,UAAM,UAAU,SAAS,OAAT,CAFY;AAG5B,UAAM,aAAa,WAAW,QAAQ,MAAR,IAAkB,CAA7B,CAHS;AAI5B,QAAI,aAAa,CAAb,EAAgB;AAClB,eAAS,IAAT,CAAc,GAAd,EAAmB,OAAnB,EADkB;KAApB;;AAIA,UAAM,OAAO,SAAS,IAAT,CARe;AAS5B,UAAM,WAAW,QAAQ,OAAO,IAAP,CAAY,IAAZ,CAAR,CATW;AAU5B,UAAM,aAAa,YAAY,SAAS,MAAT,IAAmB,CAA/B,CAVS;AAW5B,QAAI,aAAa,CAAb,EAAgB;AAClB,eAAS,KAAT,CAAe,GAAf,EAAoB,UAAU,IAAV,EAAgB,SAAhB,CAApB,EADkB;KAApB;;AAIA,UAAM,QAAQ,SAAS,KAAT,CAfc;AAgB5B,UAAM,cAAc,SAAS,OAAO,IAAP,CAAY,KAAZ,CAAT,CAhBQ;AAiB5B,UAAM,cAAc,eAAe,YAAY,MAAZ,IAAsB,CAArC,CAjBQ;AAkB5B,QAAI,cAAc,CAAd,EAAiB;AACnB,kBAAY,OAAZ,CAAoB,aAAa;AAC/B,iBAAS,OAAT,CAAiB,GAAjB,EAAsB,SAAtB,EAAiC,MAAM,SAAN,CAAjC,EAD+B;OAAb,CAApB,CADmB;KAArB;;AAMA,WAAO,EAAE,UAAF,EAAc,UAAd,EAA0B,WAA1B,EAAuC,WAAvC,EAAP,CAxB4B;GAA9B;;CA1lBF","file":"redisstorage-compiled.js","sourcesContent":["/**\n * Created by Stainwoortsel on 30.05.2016.\n */\nconst Promise = require('bluebird');\nconst Errors = require('common-errors');\nconst mapValues = require('lodash/mapValues');\nconst defaults = require('lodash/defaults');\nconst get = require('lodash/get');\nconst pick = require('lodash/pick');\nconst request = require('request-promise');\nconst uuid = require('node-uuid');\nconst fsort = require('redis-filtered-sort');\nconst fmt = require('util').format;\nconst is = require('is');\nconst sha256 = require('./sha256.js');\nconst moment = require('moment');\n\nconst stringify = JSON.stringify.bind(JSON);\nconst {\n USERS_DATA, USERS_METADATA, USERS_ALIAS_TO_LOGIN,\n USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA,\n USERS_ACTIVE_FLAG, USERS_INDEX, USERS_PUBLIC_INDEX,\n USERS_ALIAS_FIELD\n} = require('../constants.js');\n\nconst { redis, captcha: captchaConfig, config } = this;\nconst { jwt: { lockAfterAttempts, defaultAudience } } = config;\n\n\n/**\n * Generate hash key string\n * @param args\n * @returns {string}\n */\nconst generateKey = (...args) => {\n const SEPARATOR = '!';\n return args.join(SEPARATOR);\n};\n\nmodule.exports = {\n /**\n * Lock user\n * @param username\n * @param reason\n * @param whom\n * @param remoteip\n * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}}\n */\n lockUser({ username, reason, whom, remoteip }){\n const data = {\n banned: true,\n [USERS_BANNED_DATA]: {\n reason,\n whom,\n remoteip\n }\n };\n\n return redis\n .pipeline()\n .hset(generateKey(username, USERS_DATA), USERS_BANNED_FLAG, 'true')\n // set .banned on metadata for filtering & sorting users by that field\n .hmset(generateKey(username, USERS_METADATA, defaultAudience), mapValues(data, stringify))\n .del(generateKey(username, USERS_TOKENS))\n .exec();\n },\n\n /**\n * Unlock user\n * @param username\n * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}}\n */\n unlockUser({username}){\n return redis\n .pipeline()\n .hdel(generateKey(username, USERS_DATA), USERS_BANNED_FLAG)\n // remove .banned on metadata for filtering & sorting users by that field\n .hdel(generateKey(username, USERS_METADATA, defaultAudience), 'banned', USERS_BANNED_DATA)\n .exec();\n\n },\n\n /**\n * Check existance of user\n * @param username\n * @returns {Redis}\n */\n isExists(username){\n return redis\n .pipeline()\n .hget(USERS_ALIAS_TO_LOGIN, username)\n .exists(generateKey(username, USERS_DATA))\n .exec()\n .spread((alias, exists) => {\n if (alias[1]) {\n return alias[1];\n }\n\n if (!exists[1]) {\n throw new Errors.HttpStatusError(404, `\"${username}\" does not exists`);\n }\n\n return username;\n });\n },\n\n isAliasExists(alias, thunk){\n function resolveAlias(alias) {\n return redis\n .hget(USERS_ALIAS_TO_LOGIN, alias)\n .then(username => {\n if (username) {\n throw new Errors.HttpStatusError(409, `\"${alias}\" already exists`);\n }\n\n return username;\n });\n }\n if (thunk) {\n return function resolveAliasThunk() {\n return resolveAlias(alias);\n };\n }\n\n return resolveAlias(alias);\n },\n\n /**\n * User is public\n * @param username\n * @param audience\n * @returns {function()}\n */\n isPublic(username, audience) {\n return metadata => {\n if (get(metadata, [audience, USERS_ALIAS_FIELD]) === username) {\n return;\n }\n\n throw new Errors.HttpStatusError(404, 'username was not found');\n };\n },\n\n\n /**\n * Check that user is active\n * @param data\n * @returns {Promise}\n */\n isActive(data){\n if (String(data[USERS_ACTIVE_FLAG]) !== 'true') {\n return Promise.reject(new Errors.HttpStatusError(412, 'Account hasn\\'t been activated'));\n }\n\n return Promise.resolve(data);\n },\n\n /**\n * Check that user is banned\n * @param data\n * @returns {Promise}\n */\n isBanned(data){\n if(String(data[USERS_BANNED_FLAG]) === 'true') {\n return Promise.reject(new Errors.HttpStatusError(423, 'Account has been locked'));\n }\n\n return Promise.resolve(data);\n },\n\n /**\n * Activate user account\n * @param user\n * @returns {Redis}\n */\n activateAccount(user){\n const userKey = generateKey(user, USERS_DATA);\n\n // WARNING: `persist` is very important, otherwise we will lose user's information in 30 days\n // set to active & persist\n return redis\n .pipeline()\n .hget(userKey, USERS_ACTIVE_FLAG)\n .hset(userKey, USERS_ACTIVE_FLAG, 'true')\n .persist(userKey)\n .sadd(USERS_INDEX, user)\n .exec()\n .spread(function pipeResponse(isActive) {\n const status = isActive[1];\n if (status === 'true') {\n throw new Errors.HttpStatusError(417, `Account ${user} was already activated`);\n }\n });\n },\n\n /**\n * Get user internal data\n * @param username\n * @returns {Object}\n */\n getUser(username){\n const userKey = generateKey(username, USERS_DATA);\n\n return redis\n .pipeline()\n .hget(USERS_ALIAS_TO_LOGIN, username)\n .exists(userKey)\n .hgetallBuffer(userKey)\n .exec()\n .spread((aliasToUsername, exists, data) => {\n if (aliasToUsername[1]) {\n return this.getUser(aliasToUsername[1]);\n }\n\n if (!exists[1]) {\n throw new Errors.HttpStatusError(404, `\"${username}\" does not exists`);\n }\n\n return { ...data[1], username };\n });\n },\n\n /**\n * Get users metadata by username and audience\n * @param username\n * @param audience\n * @returns {Object}\n */\n\n // getMetadata(username, audience){\n // return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience));\n // },\n\n getMetadata(username, _audiences, fields = {}) {\n const audiences = Array.isArray(_audiences) ? _audiences : [_audiences];\n\n return Promise.map(audiences, audience => {\n return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience));\n })\n .then(function remapAudienceData(data) {\n const output = {};\n audiences.forEach(function transform(aud, idx) {\n const datum = data[idx];\n\n if (datum) {\n const pickFields = fields[aud];\n output[aud] = mapValues(datum, JSONParse);\n if (pickFields) {\n output[aud] = pick(output[aud], pickFields);\n }\n } else {\n output[aud] = {};\n }\n });\n\n return output;\n });\n },\n\n\n /**\n * Return the list of users by specified params\n * @param opts\n * @returns {Array}\n */\n getList(opts){\n const { criteria, audience, filter, index, strFilter, order, offset, limit } = opts;\n const metaKey = generateKey('*', USERS_METADATA, audience);\n\n return redis\n .fsort(index, metaKey, criteria, order, strFilter, offset, limit)\n .then(ids => {\n const length = +ids.pop();\n if (length === 0 || ids.length === 0) {\n return [\n ids || [],\n [],\n length,\n ];\n }\n\n const pipeline = redis.pipeline();\n ids.forEach(id => {\n pipeline.hgetallBuffer(redisKey(id, USERS_METADATA, audience));\n });\n return Promise.all([\n ids,\n pipeline.exec(),\n length,\n ]);\n })\n .spread((ids, props, length) => {\n const users = ids.map(function remapData(id, idx) {\n const data = props[idx][1];\n const account = {\n id,\n metadata: {\n [audience]: data ? mapValues(data, JSONParse) : {},\n },\n };\n\n return account;\n });\n\n return {\n users,\n cursor: offset + limit,\n page: Math.floor(offset / limit + 1),\n pages: Math.ceil(length / limit),\n };\n });\n },\n\n /**\n * Check existence of alias\n * @param data\n * @returns {boolean}\n */\n isAliasAssigned(data){\n return data[USERS_ALIAS_FIELD] !== undefined; // was just `data[USERS_ALIAS_FIELD]`\n },\n\n /**\n * Check that user is admin\n * @param meta\n * @returns {boolean}\n */\n isAdmin(meta){\n const audience = config.jwt.defaultAudience;\n return(meta[audience].roles || []).indexOf(USERS_ADMIN_ROLE) >= 0;\n },\n\n /**\n * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN\n * @param username\n * @param alias\n * @returns {Redis}\n */\n storeAlias(username, alias){\n return redis.hsetnx(USERS_ALIAS_TO_LOGIN, alias, username);\n },\n\n /**\n * Assign alias to the user record, marked by username\n * @param username\n * @param alias\n * @returns {Redis}\n */\n assignAlias(username, alias){\n return redis\n .pipeline()\n .sadd(USERS_PUBLIC_INDEX, username)\n .hset(generateKey(username, USERS_DATA), USERS_ALIAS_FIELD, alias)\n .hset(generateKey(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, stringify)\n .exec();\n },\n\n _remoteipKey: '',\n get remoteipKey(){\n return this._remoteipKey;\n },\n\n set remoteipKey(val){\n this._remoteipKey = val;\n },\n\n generateipKey(username, remoteip){\n return this._remoteipKey = generateKey(username, 'ip', remoteip);\n },\n\n _loginAttempts: 0,\n get loginAttempts(){\n return this._loginAttempts;\n },\n set loginAttempts(val){\n this._loginAttempts = val;\n },\n\n _options: {},\n get options(){\n return this._options;\n },\n\n set options(opts){\n this._options = opts;\n },\n\n dropAttempts(){\n this._loginAttempts = 0;\n return redis.del(this.key);\n },\n checkLoginAttempts(data) {\n const pipeline = redis.pipeline();\n const username = data.username;\n const remoteipKey = this.generateipKey(username, this._options.remoteip);\n\n pipeline.incrby(remoteipKey, 1);\n if (config.jwt.keepLoginAttempts > 0) {\n pipeline.expire(remoteipKey, config.jwt.keepLoginAttempts);\n }\n\n return pipeline\n .exec()\n .spread(function incremented(incrementValue) {\n const err = incrementValue[0];\n if (err) {\n this.log.error('Redis error:', err);\n return;\n }\n\n this.loginAttempts = incrementValue[1];\n if (this.loginAttempts > lockAfterAttempts) {\n const duration = moment().add(config.jwt.keepLoginAttempts, 'seconds').toNow(true);\n const msg = `You are locked from making login attempts for the next ${duration}`;\n throw new Errors.HttpStatusError(429, msg);\n }\n });\n },\n\n /**\n * Set user password\n * @param username\n * @param hash\n * @returns {Redis}\n */\n setPassword(username, hash){\n return redis\n .hset(generateKey(username, USERS_DATA), 'password', hash)\n .return(username);\n },\n\n /**\n * Reset the lock by IP\n * @param username\n * @param ip\n * @returns {Redis}\n */\n resetIPLock(username, ip){\n return redis.del(generateKey(username, 'ip', ip));\n },\n\n /**\n *\n * @param username\n * @param audience\n * @param metadata\n * @returns {Object}\n */\n updateMetadata({ username, audience, metadata, script }) {\n const audiences = is.array(audience) ? audience : [audience];\n\n // keys\n const keys = audiences.map(aud => redisKey(username, USERS_METADATA, aud));\n\n // if we have meta, then we can\n if (metadata) {\n const pipe = redis.pipeline();\n const metaOps = is.array(metadata) ? metadata : [metadata];\n const operations = metaOps.map((meta, idx) => handleAudience(pipe, keys[idx], meta));\n return pipe.exec().then(res => mapMetaResponse(operations, res));\n }\n\n //or...\n return this.customScript(script)\n },\n\n /**\n * Removing user by username (and data?)\n * @param username\n * @param data\n * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}}\n */\n removeUser(username, data){\n const audience = config.jwt.defaultAudience;\n const transaction = redis.multi();\n const alias = data[USERS_ALIAS_FIELD];\n if (alias) {\n transaction.hdel(USERS_ALIAS_TO_LOGIN, alias);\n }\n\n // clean indices\n transaction.srem(USERS_PUBLIC_INDEX, username);\n transaction.srem(USERS_INDEX, username);\n\n // remove metadata & internal data\n transaction.del(generateKey(username, USERS_DATA));\n transaction.del(generateKey(username, USERS_METADATA, audience));\n\n // remove auth tokens\n transaction.del(generateKey(username, USERS_TOKENS));\n\n // complete it\n return transaction.exec();\n },\n\n /**\n * Verify ip limits\n * @param {redisCluster} redis\n * @param {Object} registrationLimits\n * @param {String} ipaddress\n * @return {Function}\n */\n checkLimits(registrationLimits, ipaddress) {\n const {ip: {time, times}} = registrationLimits;\n const ipaddressLimitKey = generateKey('reg-limit', ipaddress);\n const now = Date.now();\n const old = now - time;\n\n return function iplimits() {\n return redis\n .pipeline()\n .zadd(ipaddressLimitKey, now, uuid.v4())\n .pexpire(ipaddressLimitKey, time)\n .zremrangebyscore(ipaddressLimitKey, '-inf', old)\n .zcard(ipaddressLimitKey)\n .exec()\n .then(props => {\n const cardinality = props[3][1];\n if (cardinality > times) {\n const msg = 'You can\\'t register more users from your ipaddress now';\n throw new Errors.HttpStatusError(429, msg);\n }\n });\n }\n },\n\n /**\n * Creates user with a given hash\n * @param redis\n * @param username\n * @param activate\n * @param deleteInactiveAccounts\n * @param userDataKey\n * @returns {Function}\n */\n createUser(username, activate, deleteInactiveAccounts) {\n /**\n * Input from scrypt.hash\n */\n const userDataKey = generateKey(username, USERS_DATA);\n\n return function create(hash) {\n const pipeline = redis.pipeline();\n\n pipeline.hsetnx(userDataKey, 'password', hash);\n pipeline.hsetnx(userDataKey, USERS_ACTIVE_FLAG, activate);\n\n return pipeline\n .exec()\n .spread(function insertedUserData(passwordSetResponse) {\n if (passwordSetResponse[1] === 0) {\n throw new Errors.HttpStatusError(412, `User \"${username}\" already exists`);\n }\n\n if (!activate && deleteInactiveAccounts >= 0) {\n // WARNING: IF USER IS NOT VERIFIED WITHIN \n // [by default 30] DAYS - IT WILL BE REMOVED FROM DATABASE\n return redis.expire(userDataKey, deleteInactiveAccounts);\n }\n\n return null;\n });\n };\n },\n\n /**\n * Performs captcha check, returns thukn\n * @param {String} username\n * @param {String} captcha\n * @param {Object} captchaConfig\n * @return {Function}\n */\n checkCaptcha(username, captcha) {\n const {secret, ttl, uri} = captchaConfig;\n return function checkCaptcha() {\n const captchaCacheKey = captcha.response;\n return redis\n .pipeline()\n .set(captchaCacheKey, username, 'EX', ttl, 'NX')\n .get(captchaCacheKey)\n .exec()\n .spread(function captchaCacheResponse(setResponse, getResponse) {\n if (getResponse[1] !== username) {\n const msg = 'Captcha challenge you\\'ve solved can not be used, please complete it again';\n throw new Errors.HttpStatusError(412, msg);\n }\n })\n .then(function verifyGoogleCaptcha() {\n return request\n .post({uri, qs: defaults(captcha, {secret}), json: true})\n .then(function captchaSuccess(body) {\n if (!body.success) {\n return Promise.reject({statusCode: 200, error: body});\n }\n\n return true;\n })\n .catch(function captchaError(err) {\n const errData = JSON.stringify(pick(err, ['statusCode', 'error']));\n throw new Errors.HttpStatusError(412, fmt('Captcha response: %s', errData));\n });\n });\n };\n },\n\n /**\n * Stores username to the index set\n * @param username\n * @returns {Redis}\n */\n storeUsername(username){\n return redis.sadd(USERS_INDEX, username);\n },\n\n /**\n * Execute custom script on LUA\n * @param script\n * @returns {Promise}\n */\n customScript(script){\n // dynamic scripts\n const $scriptKeys = Object.keys(script);\n const scripts = $scriptKeys.map(scriptName => {\n const { lua, argv = [] } = script[scriptName];\n const sha = sha256(lua);\n const name = `ms_users_${sha}`;\n if (!is.fn(redis[name])) {\n redis.defineCommand(name, { lua });\n }\n return redis[name](keys.length, keys, argv);\n });\n\n return Promise.all(scripts).then(res => {\n const output = {};\n $scriptKeys.forEach((fieldName, idx) => {\n output[fieldName] = res[idx];\n });\n return output;\n });\n },\n\n handleAudience(key, metadata) {\n const pipeline = redis.pipeline();\n const $remove = metadata.$remove;\n const $removeOps = $remove && $remove.length || 0;\n if ($removeOps > 0) {\n pipeline.hdel(key, $remove);\n }\n\n const $set = metadata.$set;\n const $setKeys = $set && Object.keys($set);\n const $setLength = $setKeys && $setKeys.length || 0;\n if ($setLength > 0) {\n pipeline.hmset(key, mapValues($set, stringify));\n }\n\n const $incr = metadata.$incr;\n const $incrFields = $incr && Object.keys($incr);\n const $incrLength = $incrFields && $incrFields.length || 0;\n if ($incrLength > 0) {\n $incrFields.forEach(fieldName => {\n pipeline.hincrby(key, fieldName, $incr[fieldName]);\n });\n }\n\n return { $removeOps, $setLength, $incrLength, $incrFields };\n }\n\n\n};\n"]} \ No newline at end of file diff --git a/src/db/redisstorage.js b/src/db/redisstorage.js index efe55d598..3147bb3fb 100644 --- a/src/db/redisstorage.js +++ b/src/db/redisstorage.js @@ -1,3 +1,670 @@ /** - * Created by Stainwoortsel on 31.05.2016. + * Created by Stainwoortsel on 30.05.2016. */ +const Promise = require('bluebird'); +const Errors = require('common-errors'); +const mapValues = require('lodash/mapValues'); +const defaults = require('lodash/defaults'); +const get = require('lodash/get'); +const pick = require('lodash/pick'); +const request = require('request-promise'); +const uuid = require('node-uuid'); +const fsort = require('redis-filtered-sort'); +const fmt = require('util').format; +const is = require('is'); +const sha256 = require('./sha256.js'); +const moment = require('moment'); + +const stringify = JSON.stringify.bind(JSON); +const { + USERS_DATA, USERS_METADATA, USERS_ALIAS_TO_LOGIN, + USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA, + USERS_ACTIVE_FLAG, USERS_INDEX, USERS_PUBLIC_INDEX, + USERS_ALIAS_FIELD +} = require('../constants.js'); + +const { redis, captcha: captchaConfig, config } = this; +const { jwt: { lockAfterAttempts, defaultAudience } } = config; + + +/** + * Generate hash key string + * @param args + * @returns {string} + */ +const generateKey = (...args) => { + const SEPARATOR = '!'; + return args.join(SEPARATOR); +}; + +module.exports = { + /** + * Lock user + * @param username + * @param reason + * @param whom + * @param remoteip + * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} + */ + lockUser({ username, reason, whom, remoteip }){ + const data = { + banned: true, + [USERS_BANNED_DATA]: { + reason, + whom, + remoteip + } + }; + + return redis + .pipeline() + .hset(generateKey(username, USERS_DATA), USERS_BANNED_FLAG, 'true') + // set .banned on metadata for filtering & sorting users by that field + .hmset(generateKey(username, USERS_METADATA, defaultAudience), mapValues(data, stringify)) + .del(generateKey(username, USERS_TOKENS)) + .exec(); + }, + + /** + * Unlock user + * @param username + * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} + */ + unlockUser({username}){ + return redis + .pipeline() + .hdel(generateKey(username, USERS_DATA), USERS_BANNED_FLAG) + // remove .banned on metadata for filtering & sorting users by that field + .hdel(generateKey(username, USERS_METADATA, defaultAudience), 'banned', USERS_BANNED_DATA) + .exec(); + + }, + + /** + * Check existance of user + * @param username + * @returns {Redis} + */ + isExists(username){ + return redis + .pipeline() + .hget(USERS_ALIAS_TO_LOGIN, username) + .exists(generateKey(username, USERS_DATA)) + .exec() + .spread((alias, exists) => { + if (alias[1]) { + return alias[1]; + } + + if (!exists[1]) { + throw new Errors.HttpStatusError(404, `"${username}" does not exists`); + } + + return username; + }); + }, + + isAliasExists(alias, thunk){ + function resolveAlias(alias) { + return redis + .hget(USERS_ALIAS_TO_LOGIN, alias) + .then(username => { + if (username) { + throw new Errors.HttpStatusError(409, `"${alias}" already exists`); + } + + return username; + }); + } + if (thunk) { + return function resolveAliasThunk() { + return resolveAlias(alias); + }; + } + + return resolveAlias(alias); + }, + + /** + * User is public + * @param username + * @param audience + * @returns {function()} + */ + isPublic(username, audience) { + return metadata => { + if (get(metadata, [audience, USERS_ALIAS_FIELD]) === username) { + return; + } + + throw new Errors.HttpStatusError(404, 'username was not found'); + }; + }, + + + /** + * Check that user is active + * @param data + * @returns {Promise} + */ + isActive(data){ + if (String(data[USERS_ACTIVE_FLAG]) !== 'true') { + return Promise.reject(new Errors.HttpStatusError(412, 'Account hasn\'t been activated')); + } + + return Promise.resolve(data); + }, + + /** + * Check that user is banned + * @param data + * @returns {Promise} + */ + isBanned(data){ + if(String(data[USERS_BANNED_FLAG]) === 'true') { + return Promise.reject(new Errors.HttpStatusError(423, 'Account has been locked')); + } + + return Promise.resolve(data); + }, + + /** + * Activate user account + * @param user + * @returns {Redis} + */ + activateAccount(user){ + const userKey = generateKey(user, USERS_DATA); + + // WARNING: `persist` is very important, otherwise we will lose user's information in 30 days + // set to active & persist + return redis + .pipeline() + .hget(userKey, USERS_ACTIVE_FLAG) + .hset(userKey, USERS_ACTIVE_FLAG, 'true') + .persist(userKey) + .sadd(USERS_INDEX, user) + .exec() + .spread(function pipeResponse(isActive) { + const status = isActive[1]; + if (status === 'true') { + throw new Errors.HttpStatusError(417, `Account ${user} was already activated`); + } + }); + }, + + /** + * Get user internal data + * @param username + * @returns {Object} + */ + getUser(username){ + const userKey = generateKey(username, USERS_DATA); + + return redis + .pipeline() + .hget(USERS_ALIAS_TO_LOGIN, username) + .exists(userKey) + .hgetallBuffer(userKey) + .exec() + .spread((aliasToUsername, exists, data) => { + if (aliasToUsername[1]) { + return this.getUser(aliasToUsername[1]); + } + + if (!exists[1]) { + throw new Errors.HttpStatusError(404, `"${username}" does not exists`); + } + + return { ...data[1], username }; + }); + }, + + /** + * Get users metadata by username and audience + * @param username + * @param audience + * @returns {Object} + */ + + // getMetadata(username, audience){ + // return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience)); + // }, + + getMetadata(username, _audiences, fields = {}) { + const audiences = Array.isArray(_audiences) ? _audiences : [_audiences]; + + return Promise.map(audiences, audience => { + return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience)); + }) + .then(function remapAudienceData(data) { + const output = {}; + audiences.forEach(function transform(aud, idx) { + const datum = data[idx]; + + if (datum) { + const pickFields = fields[aud]; + output[aud] = mapValues(datum, JSONParse); + if (pickFields) { + output[aud] = pick(output[aud], pickFields); + } + } else { + output[aud] = {}; + } + }); + + return output; + }); + }, + + + /** + * Return the list of users by specified params + * @param opts + * @returns {Array} + */ + getList(opts){ + const { criteria, audience, filter, index, strFilter, order, offset, limit } = opts; + const metaKey = generateKey('*', USERS_METADATA, audience); + + return redis + .fsort(index, metaKey, criteria, order, strFilter, offset, limit) + .then(ids => { + const length = +ids.pop(); + if (length === 0 || ids.length === 0) { + return [ + ids || [], + [], + length, + ]; + } + + const pipeline = redis.pipeline(); + ids.forEach(id => { + pipeline.hgetallBuffer(redisKey(id, USERS_METADATA, audience)); + }); + return Promise.all([ + ids, + pipeline.exec(), + length, + ]); + }) + .spread((ids, props, length) => { + const users = ids.map(function remapData(id, idx) { + const data = props[idx][1]; + const account = { + id, + metadata: { + [audience]: data ? mapValues(data, JSONParse) : {}, + }, + }; + + return account; + }); + + return { + users, + cursor: offset + limit, + page: Math.floor(offset / limit + 1), + pages: Math.ceil(length / limit), + }; + }); + }, + + /** + * Check existence of alias + * @param data + * @returns {boolean} + */ + isAliasAssigned(data){ + return data[USERS_ALIAS_FIELD] !== undefined; // was just `data[USERS_ALIAS_FIELD]` + }, + + /** + * Check that user is admin + * @param meta + * @returns {boolean} + */ + isAdmin(meta){ + const audience = config.jwt.defaultAudience; + return(meta[audience].roles || []).indexOf(USERS_ADMIN_ROLE) >= 0; + }, + + /** + * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN + * @param username + * @param alias + * @returns {Redis} + */ + storeAlias(username, alias){ + return redis.hsetnx(USERS_ALIAS_TO_LOGIN, alias, username); + }, + + /** + * Assign alias to the user record, marked by username + * @param username + * @param alias + * @returns {Redis} + */ + assignAlias(username, alias){ + return redis + .pipeline() + .sadd(USERS_PUBLIC_INDEX, username) + .hset(generateKey(username, USERS_DATA), USERS_ALIAS_FIELD, alias) + .hset(generateKey(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, stringify) + .exec(); + }, + + _remoteipKey: '', + get remoteipKey(){ + return this._remoteipKey; + }, + + set remoteipKey(val){ + this._remoteipKey = val; + }, + + generateipKey(username, remoteip){ + return this._remoteipKey = generateKey(username, 'ip', remoteip); + }, + + _loginAttempts: 0, + get loginAttempts(){ + return this._loginAttempts; + }, + set loginAttempts(val){ + this._loginAttempts = val; + }, + + _options: {}, + get options(){ + return this._options; + }, + + set options(opts){ + this._options = opts; + }, + + dropAttempts(){ + this._loginAttempts = 0; + return redis.del(this.key); + }, + checkLoginAttempts(data) { + const pipeline = redis.pipeline(); + const username = data.username; + const remoteipKey = this.generateipKey(username, this._options.remoteip); + + pipeline.incrby(remoteipKey, 1); + if (config.jwt.keepLoginAttempts > 0) { + pipeline.expire(remoteipKey, config.jwt.keepLoginAttempts); + } + + return pipeline + .exec() + .spread(function incremented(incrementValue) { + const err = incrementValue[0]; + if (err) { + this.log.error('Redis error:', err); + return; + } + + this.loginAttempts = incrementValue[1]; + if (this.loginAttempts > lockAfterAttempts) { + const duration = moment().add(config.jwt.keepLoginAttempts, 'seconds').toNow(true); + const msg = `You are locked from making login attempts for the next ${duration}`; + throw new Errors.HttpStatusError(429, msg); + } + }); + }, + + /** + * Set user password + * @param username + * @param hash + * @returns {Redis} + */ + setPassword(username, hash){ + return redis + .hset(generateKey(username, USERS_DATA), 'password', hash) + .return(username); + }, + + /** + * Reset the lock by IP + * @param username + * @param ip + * @returns {Redis} + */ + resetIPLock(username, ip){ + return redis.del(generateKey(username, 'ip', ip)); + }, + + /** + * + * @param username + * @param audience + * @param metadata + * @returns {Object} + */ + updateMetadata({ username, audience, metadata, script }) { + const audiences = is.array(audience) ? audience : [audience]; + + // keys + const keys = audiences.map(aud => redisKey(username, USERS_METADATA, aud)); + + // if we have meta, then we can + if (metadata) { + const pipe = redis.pipeline(); + const metaOps = is.array(metadata) ? metadata : [metadata]; + const operations = metaOps.map((meta, idx) => handleAudience(pipe, keys[idx], meta)); + return pipe.exec().then(res => mapMetaResponse(operations, res)); + } + + //or... + return this.customScript(script) + }, + + /** + * Removing user by username (and data?) + * @param username + * @param data + * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} + */ + removeUser(username, data){ + const audience = config.jwt.defaultAudience; + const transaction = redis.multi(); + const alias = data[USERS_ALIAS_FIELD]; + if (alias) { + transaction.hdel(USERS_ALIAS_TO_LOGIN, alias); + } + + // clean indices + transaction.srem(USERS_PUBLIC_INDEX, username); + transaction.srem(USERS_INDEX, username); + + // remove metadata & internal data + transaction.del(generateKey(username, USERS_DATA)); + transaction.del(generateKey(username, USERS_METADATA, audience)); + + // remove auth tokens + transaction.del(generateKey(username, USERS_TOKENS)); + + // complete it + return transaction.exec(); + }, + + /** + * Verify ip limits + * @param {redisCluster} redis + * @param {Object} registrationLimits + * @param {String} ipaddress + * @return {Function} + */ + checkLimits(registrationLimits, ipaddress) { + const {ip: {time, times}} = registrationLimits; + const ipaddressLimitKey = generateKey('reg-limit', ipaddress); + const now = Date.now(); + const old = now - time; + + return function iplimits() { + return redis + .pipeline() + .zadd(ipaddressLimitKey, now, uuid.v4()) + .pexpire(ipaddressLimitKey, time) + .zremrangebyscore(ipaddressLimitKey, '-inf', old) + .zcard(ipaddressLimitKey) + .exec() + .then(props => { + const cardinality = props[3][1]; + if (cardinality > times) { + const msg = 'You can\'t register more users from your ipaddress now'; + throw new Errors.HttpStatusError(429, msg); + } + }); + } + }, + + /** + * Creates user with a given hash + * @param redis + * @param username + * @param activate + * @param deleteInactiveAccounts + * @param userDataKey + * @returns {Function} + */ + createUser(username, activate, deleteInactiveAccounts) { + /** + * Input from scrypt.hash + */ + const userDataKey = generateKey(username, USERS_DATA); + + return function create(hash) { + const pipeline = redis.pipeline(); + + pipeline.hsetnx(userDataKey, 'password', hash); + pipeline.hsetnx(userDataKey, USERS_ACTIVE_FLAG, activate); + + return pipeline + .exec() + .spread(function insertedUserData(passwordSetResponse) { + if (passwordSetResponse[1] === 0) { + throw new Errors.HttpStatusError(412, `User "${username}" already exists`); + } + + if (!activate && deleteInactiveAccounts >= 0) { + // WARNING: IF USER IS NOT VERIFIED WITHIN + // [by default 30] DAYS - IT WILL BE REMOVED FROM DATABASE + return redis.expire(userDataKey, deleteInactiveAccounts); + } + + return null; + }); + }; + }, + + /** + * Performs captcha check, returns thukn + * @param {String} username + * @param {String} captcha + * @param {Object} captchaConfig + * @return {Function} + */ + checkCaptcha(username, captcha) { + const {secret, ttl, uri} = captchaConfig; + return function checkCaptcha() { + const captchaCacheKey = captcha.response; + return redis + .pipeline() + .set(captchaCacheKey, username, 'EX', ttl, 'NX') + .get(captchaCacheKey) + .exec() + .spread(function captchaCacheResponse(setResponse, getResponse) { + if (getResponse[1] !== username) { + const msg = 'Captcha challenge you\'ve solved can not be used, please complete it again'; + throw new Errors.HttpStatusError(412, msg); + } + }) + .then(function verifyGoogleCaptcha() { + return request + .post({uri, qs: defaults(captcha, {secret}), json: true}) + .then(function captchaSuccess(body) { + if (!body.success) { + return Promise.reject({statusCode: 200, error: body}); + } + + return true; + }) + .catch(function captchaError(err) { + const errData = JSON.stringify(pick(err, ['statusCode', 'error'])); + throw new Errors.HttpStatusError(412, fmt('Captcha response: %s', errData)); + }); + }); + }; + }, + + /** + * Stores username to the index set + * @param username + * @returns {Redis} + */ + storeUsername(username){ + return redis.sadd(USERS_INDEX, username); + }, + + /** + * Execute custom script on LUA + * @param script + * @returns {Promise} + */ + customScript(script){ + // dynamic scripts + const $scriptKeys = Object.keys(script); + const scripts = $scriptKeys.map(scriptName => { + const { lua, argv = [] } = script[scriptName]; + const sha = sha256(lua); + const name = `ms_users_${sha}`; + if (!is.fn(redis[name])) { + redis.defineCommand(name, { lua }); + } + return redis[name](keys.length, keys, argv); + }); + + return Promise.all(scripts).then(res => { + const output = {}; + $scriptKeys.forEach((fieldName, idx) => { + output[fieldName] = res[idx]; + }); + return output; + }); + }, + + handleAudience(key, metadata) { + const pipeline = redis.pipeline(); + const $remove = metadata.$remove; + const $removeOps = $remove && $remove.length || 0; + if ($removeOps > 0) { + pipeline.hdel(key, $remove); + } + + const $set = metadata.$set; + const $setKeys = $set && Object.keys($set); + const $setLength = $setKeys && $setKeys.length || 0; + if ($setLength > 0) { + pipeline.hmset(key, mapValues($set, stringify)); + } + + const $incr = metadata.$incr; + const $incrFields = $incr && Object.keys($incr); + const $incrLength = $incrFields && $incrFields.length || 0; + if ($incrLength > 0) { + $incrFields.forEach(fieldName => { + pipeline.hincrby(key, fieldName, $incr[fieldName]); + }); + } + + return { $removeOps, $setLength, $incrLength, $incrFields }; + } + + +}; diff --git a/src/users.js b/src/users.js index dfea8b384..aaf23366f 100644 --- a/src/users.js +++ b/src/users.js @@ -31,7 +31,7 @@ module.exports = class Users extends Mservice { this.log.fatal('Invalid configuration:', error.toJSON()); throw error; } - +//повесить логику выбора адаптера можно повесить сюда, экшн долежн выполниться позже, можно лочить принятие сообщений внутри роута this.on('plugin:connect:amqp', (amqp) => { this._mailer = new Mailer(amqp, config.mailer); }); @@ -60,6 +60,7 @@ module.exports = class Users extends Mservice { */ get config() { return this._config; + } /** @@ -75,3 +76,4 @@ module.exports = class Users extends Mservice { initFakeAccounts = require('./accounts/init-dev.js'); }; + diff --git a/src/utils/getInternalData.js b/src/utils/getInternalData.js index ef5e0506e..4a50c3d6a 100644 --- a/src/utils/getInternalData.js +++ b/src/utils/getInternalData.js @@ -5,7 +5,7 @@ const { USERS_DATA, USERS_ALIAS_TO_LOGIN } = require('../constants.js'); module.exports = function getInternalData(username) { const { redis } = this; const userKey = redisKey(username, USERS_DATA); - + return redis .pipeline() .hget(USERS_ALIAS_TO_LOGIN, username) diff --git a/src/utils/sha256-compiled-compiled-compiled-compiled.js b/src/utils/sha256-compiled-compiled-compiled-compiled.js new file mode 100644 index 000000000..a00f10752 --- /dev/null +++ b/src/utils/sha256-compiled-compiled-compiled-compiled.js @@ -0,0 +1,19 @@ +'use strict'; + +const crypto = require('crypto'); + +/** + * Shorthand for sha256 + * @param {String} data + */ +module.exports = function digest(data) { + return crypto.createHash('sha256').update(data, 'utf8').digest(); +}; + +//# sourceMappingURL=sha256-compiled.js.map + +//# sourceMappingURL=sha256-compiled-compiled.js.map + +//# sourceMappingURL=sha256-compiled-compiled-compiled.js.map + +//# sourceMappingURL=sha256-compiled-compiled-compiled-compiled.js.map \ No newline at end of file diff --git a/src/utils/sha256-compiled-compiled-compiled-compiled.js.map b/src/utils/sha256-compiled-compiled-compiled-compiled.js.map new file mode 100644 index 000000000..bcd578859 --- /dev/null +++ b/src/utils/sha256-compiled-compiled-compiled-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["sha256-compiled-compiled-compiled.js"],"names":[],"mappings":"AAAA;;AAEA,MAAM,SAAS,QAAQ,QAAR,CAAT;;;;;;AAMN,OAAO,OAAP,GAAiB,SAAS,MAAT,CAAgB,IAAhB,EAAsB;AACrC,SAAO,OAAO,UAAP,CAAkB,QAAlB,EAA4B,MAA5B,CAAmC,IAAnC,EAAyC,MAAzC,EAAiD,MAAjD,EAAP,CADqC;CAAtB","file":"sha256-compiled-compiled-compiled-compiled.js","sourcesContent":["'use strict';\n\nconst crypto = require('crypto');\n\n/**\r\n * Shorthand for sha256\r\n * @param {String} data\r\n */\nmodule.exports = function digest(data) {\n return crypto.createHash('sha256').update(data, 'utf8').digest();\n};\n\n//# sourceMappingURL=sha256-compiled.js.map\n\n//# sourceMappingURL=sha256-compiled-compiled.js.map\n\n//# sourceMappingURL=sha256-compiled-compiled-compiled.js.map"]} \ No newline at end of file diff --git a/src/utils/sha256-compiled-compiled-compiled.js b/src/utils/sha256-compiled-compiled-compiled.js new file mode 100644 index 000000000..c7236bf9b --- /dev/null +++ b/src/utils/sha256-compiled-compiled-compiled.js @@ -0,0 +1,17 @@ +'use strict'; + +const crypto = require('crypto'); + +/** + * Shorthand for sha256 + * @param {String} data + */ +module.exports = function digest(data) { + return crypto.createHash('sha256').update(data, 'utf8').digest(); +}; + +//# sourceMappingURL=sha256-compiled.js.map + +//# sourceMappingURL=sha256-compiled-compiled.js.map + +//# sourceMappingURL=sha256-compiled-compiled-compiled.js.map \ No newline at end of file diff --git a/src/utils/sha256-compiled-compiled-compiled.js.map b/src/utils/sha256-compiled-compiled-compiled.js.map new file mode 100644 index 000000000..fcace4c7e --- /dev/null +++ b/src/utils/sha256-compiled-compiled-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["sha256-compiled-compiled.js"],"names":[],"mappings":"AAAA;;AAEA,MAAM,SAAS,QAAQ,QAAR,CAAT;;;;;;AAMN,OAAO,OAAP,GAAiB,SAAS,MAAT,CAAgB,IAAhB,EAAsB;AACrC,SAAO,OAAO,UAAP,CAAkB,QAAlB,EAA4B,MAA5B,CAAmC,IAAnC,EAAyC,MAAzC,EAAiD,MAAjD,EAAP,CADqC;CAAtB","file":"sha256-compiled-compiled-compiled.js","sourcesContent":["'use strict';\n\nconst crypto = require('crypto');\n\n/**\r\n * Shorthand for sha256\r\n * @param {String} data\r\n */\nmodule.exports = function digest(data) {\n return crypto.createHash('sha256').update(data, 'utf8').digest();\n};\n\n//# sourceMappingURL=sha256-compiled.js.map\n\n//# sourceMappingURL=sha256-compiled-compiled.js.map"]} \ No newline at end of file diff --git a/src/utils/sha256-compiled-compiled.js b/src/utils/sha256-compiled-compiled.js new file mode 100644 index 000000000..13dbcc19c --- /dev/null +++ b/src/utils/sha256-compiled-compiled.js @@ -0,0 +1,15 @@ +'use strict'; + +const crypto = require('crypto'); + +/** + * Shorthand for sha256 + * @param {String} data + */ +module.exports = function digest(data) { + return crypto.createHash('sha256').update(data, 'utf8').digest(); +}; + +//# sourceMappingURL=sha256-compiled.js.map + +//# sourceMappingURL=sha256-compiled-compiled.js.map \ No newline at end of file diff --git a/src/utils/sha256-compiled-compiled.js.map b/src/utils/sha256-compiled-compiled.js.map new file mode 100644 index 000000000..d8efc2253 --- /dev/null +++ b/src/utils/sha256-compiled-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["sha256-compiled.js"],"names":[],"mappings":"AAAA;;AAEA,MAAM,SAAS,QAAQ,QAAR,CAAT;;;;;;AAMN,OAAO,OAAP,GAAiB,SAAS,MAAT,CAAgB,IAAhB,EAAsB;AACrC,SAAO,OAAO,UAAP,CAAkB,QAAlB,EAA4B,MAA5B,CAAmC,IAAnC,EAAyC,MAAzC,EAAiD,MAAjD,EAAP,CADqC;CAAtB","file":"sha256-compiled-compiled.js","sourcesContent":["'use strict';\n\nconst crypto = require('crypto');\n\n/**\r\n * Shorthand for sha256\r\n * @param {String} data\r\n */\nmodule.exports = function digest(data) {\n return crypto.createHash('sha256').update(data, 'utf8').digest();\n};\n\n//# sourceMappingURL=sha256-compiled.js.map"]} \ No newline at end of file diff --git a/src/utils/sha256-compiled.js b/src/utils/sha256-compiled.js new file mode 100644 index 000000000..79a013c87 --- /dev/null +++ b/src/utils/sha256-compiled.js @@ -0,0 +1,13 @@ +'use strict'; + +const crypto = require('crypto'); + +/** + * Shorthand for sha256 + * @param {String} data + */ +module.exports = function digest(data) { + return crypto.createHash('sha256').update(data, 'utf8').digest(); +}; + +//# sourceMappingURL=sha256-compiled.js.map \ No newline at end of file diff --git a/src/utils/sha256-compiled.js.map b/src/utils/sha256-compiled.js.map new file mode 100644 index 000000000..da263c24d --- /dev/null +++ b/src/utils/sha256-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["sha256.js"],"names":[],"mappings":";;AAAA,MAAM,SAAS,QAAQ,QAAR,CAAT;;;;;;AAMN,OAAO,OAAP,GAAiB,SAAS,MAAT,CAAgB,IAAhB,EAAsB;AACrC,SAAO,OAAO,UAAP,CAAkB,QAAlB,EAA4B,MAA5B,CAAmC,IAAnC,EAAyC,MAAzC,EAAiD,MAAjD,EAAP,CADqC;CAAtB","file":"sha256-compiled.js","sourcesContent":["const crypto = require('crypto');\r\n\r\n/**\r\n * Shorthand for sha256\r\n * @param {String} data\r\n */\r\nmodule.exports = function digest(data) {\r\n return crypto.createHash('sha256').update(data, 'utf8').digest();\r\n};\r\n"]} \ No newline at end of file diff --git a/test/suites/updateMetadata-compiled.js b/test/suites/updateMetadata-compiled.js new file mode 100644 index 000000000..2764b1c35 --- /dev/null +++ b/test/suites/updateMetadata-compiled.js @@ -0,0 +1,80 @@ +'use strict'; + +/* global inspectPromise */ +const { expect } = require('chai'); + +describe('#updateMetadata', function getMetadataSuite() { + const headers = { routingKey: 'users.updateMetadata' }; + const username = 'v@makeomatic.ru'; + const audience = '*.localhost'; + const extra = 'extra.localhost'; + + beforeEach(global.startService); + afterEach(global.clearRedis); + + beforeEach(function pretest() { + return this.users.router({ username, password: '123', audience }, { routingKey: 'users.register' }); + }); + + it('must reject updating metadata on a non-existing user', function test() { + return this.users.router({ username: 'ok google', audience, metadata: { $remove: ['test'] } }, headers).reflect().then(inspectPromise(false)).then(getMetadata => { + expect(getMetadata.name).to.be.eq('HttpStatusError'); + expect(getMetadata.statusCode).to.be.eq(404); + }); + }); + + it('must be able to add metadata for a single audience of an existing user', function test() { + return this.users.router({ username, audience, metadata: { $set: { x: 10 } } }, headers).reflect().then(inspectPromise()); + }); + + it('must be able to remove metadata for a single audience of an existing user', function test() { + return this.users.router({ username, audience, metadata: { $remove: ['x'] } }, headers).reflect().then(inspectPromise()).then(data => { + expect(data.$remove).to.be.eq(0); + }); + }); + + it('rejects on mismatch of audience & metadata arrays', function test() { + return this.users.router({ + username, audience: [audience], + metadata: [{ $set: { x: 10 } }, { $remove: ['x'] }] + }, headers).reflect().then(inspectPromise(false)); + }); + + it('must be able to perform batch operations for multiple audiences of an existing user', function test() { + return this.users.router({ + username, + audience: [audience, extra], + metadata: [{ + $set: { + x: 10 + }, + $incr: { + b: 2 + } + }, { + $incr: { + b: 3 + } + }] + }, headers).reflect().then(inspectPromise()).then(data => { + const [mainData, extraData] = data; + + expect(mainData.$set).to.be.eq('OK'); + expect(mainData.$incr.b).to.be.eq(2); + expect(extraData.$incr.b).to.be.eq(3); + }); + }); + + it('must be able to run dynamic scripts', function test() { + return this.users.router({ username, audience: [audience, extra], script: { + balance: { + lua: 'return {KEYS[1],KEYS[2],ARGV[1]}', + argv: ['nom-nom'] + } + } }, headers).reflect().then(inspectPromise()).then(data => { + expect(data.balance).to.be.deep.eq([`{ms-users}v@makeomatic.ru!metadata!${ audience }`, `{ms-users}v@makeomatic.ru!metadata!${ extra }`, 'nom-nom']); + }); + }); +}); + +//# sourceMappingURL=updateMetadata-compiled.js.map \ No newline at end of file diff --git a/test/suites/updateMetadata-compiled.js.map b/test/suites/updateMetadata-compiled.js.map new file mode 100644 index 000000000..23817a900 --- /dev/null +++ b/test/suites/updateMetadata-compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["updateMetadata.js"],"names":[],"mappings":"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","file":"updateMetadata-compiled.js","sourcesContent":["/* global inspectPromise */\r\nconst { expect } = require('chai');\r\n\r\ndescribe('#updateMetadata', function getMetadataSuite() {\r\n const headers = { routingKey: 'users.updateMetadata' };\r\n const username = 'v@makeomatic.ru';\r\n const audience = '*.localhost';\r\n const extra = 'extra.localhost';\r\n\r\n beforeEach(global.startService);\r\n afterEach(global.clearRedis);\r\n\r\n beforeEach(function pretest() {\r\n return this.users.router({ username, password: '123', audience }, { routingKey: 'users.register' });\r\n });\r\n\r\n it('must reject updating metadata on a non-existing user', function test() {\r\n return this.users.router({ username: 'ok google', audience, metadata: { $remove: ['test'] } }, headers)\r\n .reflect()\r\n .then(inspectPromise(false))\r\n .then(getMetadata => {\r\n expect(getMetadata.name).to.be.eq('HttpStatusError');\r\n expect(getMetadata.statusCode).to.be.eq(404);\r\n });\r\n });\r\n\r\n it('must be able to add metadata for a single audience of an existing user', function test() {\r\n return this.users.router({ username, audience, metadata: { $set: { x: 10 } } }, headers)\r\n .reflect()\r\n .then(inspectPromise());\r\n });\r\n\r\n it('must be able to remove metadata for a single audience of an existing user', function test() {\r\n return this.users\r\n .router({ username, audience, metadata: { $remove: ['x'] } }, headers)\r\n .reflect()\r\n .then(inspectPromise())\r\n .then(data => {\r\n expect(data.$remove).to.be.eq(0);\r\n });\r\n });\r\n\r\n it('rejects on mismatch of audience & metadata arrays', function test() {\r\n return this.users\r\n .router({\r\n username, audience: [audience],\r\n metadata: [{ $set: { x: 10 } }, { $remove: ['x'] }],\r\n }, headers)\r\n .reflect()\r\n .then(inspectPromise(false));\r\n });\r\n\r\n it('must be able to perform batch operations for multiple audiences of an existing user', function test() {\r\n return this.users\r\n .router({\r\n username,\r\n audience: [\r\n audience,\r\n extra,\r\n ],\r\n metadata: [\r\n {\r\n $set: {\r\n x: 10,\r\n },\r\n $incr: {\r\n b: 2,\r\n },\r\n },\r\n {\r\n $incr: {\r\n b: 3,\r\n },\r\n },\r\n ],\r\n }, headers)\r\n .reflect()\r\n .then(inspectPromise())\r\n .then(data => {\r\n const [mainData, extraData] = data;\r\n\r\n expect(mainData.$set).to.be.eq('OK');\r\n expect(mainData.$incr.b).to.be.eq(2);\r\n expect(extraData.$incr.b).to.be.eq(3);\r\n });\r\n });\r\n\r\n it('must be able to run dynamic scripts', function test() {\r\n return this.users.router({ username, audience: [audience, extra], script: {\r\n balance: {\r\n lua: 'return {KEYS[1],KEYS[2],ARGV[1]}',\r\n argv: ['nom-nom'],\r\n },\r\n } }, headers)\r\n .reflect()\r\n .then(inspectPromise())\r\n .then(data => {\r\n expect(data.balance).to.be.deep.eq([\r\n `{ms-users}v@makeomatic.ru!metadata!${audience}`,\r\n `{ms-users}v@makeomatic.ru!metadata!${extra}`,\r\n 'nom-nom',\r\n ]);\r\n });\r\n });\r\n});\r\n"]} \ No newline at end of file From c4ebf71e56ec361c9e58da9d582081d535e0c006 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 3 Jun 2016 13:04:15 +0300 Subject: [PATCH 04/38] style(gitignore): --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 0d529648e..83f510dc1 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ lib src/db/sandbox/ .idea +*-compiled.js +*-compiled.js.map \ No newline at end of file From 4b7a5c62d0085218b8d9f9808da4a6597fa0d3c6 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 3 Jun 2016 13:07:47 +0300 Subject: [PATCH 05/38] style(delete compiled and map files): --- .gitignore | 2 +- src/actions/activate-compiled.js | 26 - src/actions/activate-compiled.js.map | 1 - src/actions/alias-compiled.js | 26 - src/actions/alias-compiled.js.map | 1 - src/actions/ban-compiled.js | 30 - src/actions/ban-compiled.js.map | 1 - src/actions/challenge-compiled.js | 17 - src/actions/challenge-compiled.js.map | 1 - src/actions/getInternalData-compiled.js | 15 - src/actions/getInternalData-compiled.js.map | 1 - src/actions/getMetadata-compiled.js | 13 - src/actions/getMetadata-compiled.js.map | 1 - src/actions/list-compiled.js | 22 - src/actions/list-compiled.js.map | 1 - src/db/adapter-compiled.js | 366 ----------- src/db/adapter-compiled.js.map | 1 - src/db/redisstorage-compiled.js | 605 ------------------ src/db/redisstorage-compiled.js.map | 1 - ...256-compiled-compiled-compiled-compiled.js | 19 - ...compiled-compiled-compiled-compiled.js.map | 1 - .../sha256-compiled-compiled-compiled.js | 17 - .../sha256-compiled-compiled-compiled.js.map | 1 - src/utils/sha256-compiled-compiled.js | 15 - src/utils/sha256-compiled-compiled.js.map | 1 - src/utils/sha256-compiled.js | 13 - src/utils/sha256-compiled.js.map | 1 - test/suites/updateMetadata-compiled.js | 80 --- test/suites/updateMetadata-compiled.js.map | 1 - 29 files changed, 1 insertion(+), 1279 deletions(-) delete mode 100644 src/actions/activate-compiled.js delete mode 100644 src/actions/activate-compiled.js.map delete mode 100644 src/actions/alias-compiled.js delete mode 100644 src/actions/alias-compiled.js.map delete mode 100644 src/actions/ban-compiled.js delete mode 100644 src/actions/ban-compiled.js.map delete mode 100644 src/actions/challenge-compiled.js delete mode 100644 src/actions/challenge-compiled.js.map delete mode 100644 src/actions/getInternalData-compiled.js delete mode 100644 src/actions/getInternalData-compiled.js.map delete mode 100644 src/actions/getMetadata-compiled.js delete mode 100644 src/actions/getMetadata-compiled.js.map delete mode 100644 src/actions/list-compiled.js delete mode 100644 src/actions/list-compiled.js.map delete mode 100644 src/db/adapter-compiled.js delete mode 100644 src/db/adapter-compiled.js.map delete mode 100644 src/db/redisstorage-compiled.js delete mode 100644 src/db/redisstorage-compiled.js.map delete mode 100644 src/utils/sha256-compiled-compiled-compiled-compiled.js delete mode 100644 src/utils/sha256-compiled-compiled-compiled-compiled.js.map delete mode 100644 src/utils/sha256-compiled-compiled-compiled.js delete mode 100644 src/utils/sha256-compiled-compiled-compiled.js.map delete mode 100644 src/utils/sha256-compiled-compiled.js delete mode 100644 src/utils/sha256-compiled-compiled.js.map delete mode 100644 src/utils/sha256-compiled.js delete mode 100644 src/utils/sha256-compiled.js.map delete mode 100644 test/suites/updateMetadata-compiled.js delete mode 100644 test/suites/updateMetadata-compiled.js.map diff --git a/.gitignore b/.gitignore index 83f510dc1..71bd95669 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,4 @@ lib src/db/sandbox/ .idea *-compiled.js -*-compiled.js.map \ No newline at end of file +*-compiled.js.map diff --git a/src/actions/activate-compiled.js b/src/actions/activate-compiled.js deleted file mode 100644 index 3cfc743f7..000000000 --- a/src/actions/activate-compiled.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const Promise = require('bluebird'); -const emailVerification = require('../utils/send-email.js'); -const jwt = require('../utils/jwt.js'); -const Users = require('../db/adapter'); - -module.exports = function verifyChallenge(opts) { - // TODO: add security logs - // var remoteip = opts.remoteip; - const { token, namespace, username } = opts; - const { config } = this; - const audience = opts.audience || config.defaultAudience; - - function verifyToken() { - return emailVerification.verify.call(this, token, namespace, config.validation.ttl > 0); - } - - function hook(user) { - return this.hook.call(this, 'users:activate', user, audience); - } - - return Promise.bind(this, username).then(username ? Users.isExists : verifyToken).tap(Users.activateAccount).tap(hook).then(user => [user, audience]).spread(jwt.login); -}; - -//# sourceMappingURL=activate-compiled.js.map \ No newline at end of file diff --git a/src/actions/activate-compiled.js.map b/src/actions/activate-compiled.js.map deleted file mode 100644 index d0f990778..000000000 --- a/src/actions/activate-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["activate.js"],"names":[],"mappings":";;AAAA,MAAM,UAAU,QAAQ,UAAR,CAAV;AACN,MAAM,oBAAoB,QAAQ,wBAAR,CAApB;AACN,MAAM,MAAM,QAAQ,iBAAR,CAAN;AACN,MAAM,QAAQ,QAAQ,eAAR,CAAR;;AAEN,OAAO,OAAP,GAAiB,SAAS,eAAT,CAAyB,IAAzB,EAA+B;;;AAG9C,QAAM,EAAE,KAAF,EAAS,SAAT,EAAoB,QAApB,KAAiC,IAAjC,CAHwC;AAI9C,QAAM,EAAE,MAAF,KAAa,IAAb,CAJwC;AAK9C,QAAM,WAAW,KAAK,QAAL,IAAiB,OAAO,eAAP,CALY;;AAO9C,WAAS,WAAT,GAAuB;AACrB,WAAO,kBAAkB,MAAlB,CAAyB,IAAzB,CAA8B,IAA9B,EAAoC,KAApC,EAA2C,SAA3C,EAAsD,OAAO,UAAP,CAAkB,GAAlB,GAAwB,CAAxB,CAA7D,CADqB;GAAvB;;AAIA,WAAS,IAAT,CAAc,IAAd,EAAoB;AAClB,WAAO,KAAK,IAAL,CAAU,IAAV,CAAe,IAAf,EAAqB,gBAArB,EAAuC,IAAvC,EAA6C,QAA7C,CAAP,CADkB;GAApB;;AAIA,SAAO,QACJ,IADI,CACC,IADD,EACO,QADP,EAEJ,IAFI,CAEC,WAAW,MAAM,QAAN,GAAiB,WAA5B,CAFD,CAGJ,GAHI,CAGA,MAAM,eAAN,CAHA,CAIJ,GAJI,CAIA,IAJA,EAKJ,IALI,CAKC,QAAQ,CAAC,IAAD,EAAO,QAAP,CAAR,CALD,CAMJ,MANI,CAMG,IAAI,KAAJ,CANV,CAf8C;CAA/B","file":"activate-compiled.js","sourcesContent":["const Promise = require('bluebird');\nconst emailVerification = require('../utils/send-email.js');\nconst jwt = require('../utils/jwt.js');\nconst Users = require('../db/adapter');\n\nmodule.exports = function verifyChallenge(opts) {\n // TODO: add security logs\n // var remoteip = opts.remoteip;\n const { token, namespace, username } = opts;\n const { config } = this;\n const audience = opts.audience || config.defaultAudience;\n\n function verifyToken() {\n return emailVerification.verify.call(this, token, namespace, config.validation.ttl > 0);\n }\n\n function hook(user) {\n return this.hook.call(this, 'users:activate', user, audience);\n }\n\n return Promise\n .bind(this, username)\n .then(username ? Users.isExists : verifyToken)\n .tap(Users.activateAccount)\n .tap(hook)\n .then(user => [user, audience])\n .spread(jwt.login);\n};\n"]} \ No newline at end of file diff --git a/src/actions/alias-compiled.js b/src/actions/alias-compiled.js deleted file mode 100644 index 2728b1171..000000000 --- a/src/actions/alias-compiled.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const Promise = require('bluebird'); -const Errors = require('common-errors'); - -const Users = require('../db/adapter'); - -module.exports = function assignAlias(opts) { - const { username, alias } = opts; - - return Promise.bind(this, username).then(Users.getUser).tap(Users.isActive).tap(Users.isBanned).then(data => { - if (Users.isAliasAssigned(data)) { - throw new Errors.HttpStatusError(417, 'alias is already assigned'); - } - - return Users.storeAlias(username, alias); - }).then(assigned => { - if (assigned === 0) { - throw new Errors.HttpStatusError(409, 'alias was already taken'); - } - - return Users.assignAlias(username, alias); - }); -}; - -//# sourceMappingURL=alias-compiled.js.map \ No newline at end of file diff --git a/src/actions/alias-compiled.js.map b/src/actions/alias-compiled.js.map deleted file mode 100644 index fa2ea9106..000000000 --- a/src/actions/alias-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["alias.js"],"names":[],"mappings":";;AAAA,MAAM,UAAU,QAAQ,UAAR,CAAV;AACN,MAAM,SAAS,QAAQ,eAAR,CAAT;;AAEN,MAAM,QAAQ,QAAQ,eAAR,CAAR;;AAGN,OAAO,OAAP,GAAiB,SAAS,WAAT,CAAqB,IAArB,EAA2B;AAC1C,QAAM,EAAE,QAAF,EAAY,KAAZ,KAAsB,IAAtB,CADoC;;AAG1C,SAAO,QACJ,IADI,CACC,IADD,EACO,QADP,EAEJ,IAFI,CAEC,MAAM,OAAN,CAFD,CAGJ,GAHI,CAGA,MAAM,QAAN,CAHA,CAIJ,GAJI,CAIA,MAAM,QAAN,CAJA,CAKJ,IALI,CAKC,QAAQ;AACZ,QAAI,MAAM,eAAN,CAAsB,IAAtB,CAAJ,EAAiC;AAC/B,YAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,2BAAhC,CAAN,CAD+B;KAAjC;;AAIA,WAAO,MAAM,UAAN,CAAiB,QAAjB,EAA2B,KAA3B,CAAP,CALY;GAAR,CALD,CAYJ,IAZI,CAYC,YAAY;AAChB,QAAI,aAAa,CAAb,EAAgB;AAClB,YAAM,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,yBAAhC,CAAN,CADkB;KAApB;;AAIA,WAAO,MAAM,WAAN,CAAkB,QAAlB,EAA4B,KAA5B,CAAP,CALgB;GAAZ,CAZR,CAH0C;CAA3B","file":"alias-compiled.js","sourcesContent":["const Promise = require('bluebird');\nconst Errors = require('common-errors');\n\nconst Users = require('../db/adapter');\n\n\nmodule.exports = function assignAlias(opts) {\n const { username, alias } = opts;\n\n return Promise\n .bind(this, username)\n .then(Users.getUser)\n .tap(Users.isActive)\n .tap(Users.isBanned)\n .then(data => {\n if (Users.isAliasAssigned(data)) {\n throw new Errors.HttpStatusError(417, 'alias is already assigned');\n }\n\n return Users.storeAlias(username, alias);\n })\n .then(assigned => {\n if (assigned === 0) {\n throw new Errors.HttpStatusError(409, 'alias was already taken');\n }\n\n return Users.assignAlias(username, alias);\n });\n};\n"]} \ No newline at end of file diff --git a/src/actions/ban-compiled.js b/src/actions/ban-compiled.js deleted file mode 100644 index 1cdec20d0..000000000 --- a/src/actions/ban-compiled.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -const Promise = require('bluebird'); -const Users = require('../db/adapter'); - -function lockUser({ username, reason, whom, remoteip }) { - return Users.lockUser({ - username, - reason: reason || '', - whom: whom || '', - remoteip: remoteip || '' - }); -} - -function unlockUser({ username }) { - return Users.unlockUser({ username }); -} - -/** - * Bans/unbans existing user - * @param {Object} opts - * @return {Promise} - */ -module.exports = function banUser(opts) { - return Promise.bind(this, opts.username).then(Users.isExists).then(username => _extends({}, opts, { username })).then(opts.ban ? lockUser : unlockUser); -}; - -//# sourceMappingURL=ban-compiled.js.map \ No newline at end of file diff --git a/src/actions/ban-compiled.js.map b/src/actions/ban-compiled.js.map deleted file mode 100644 index 4a9302ddc..000000000 --- a/src/actions/ban-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["ban.js"],"names":[],"mappings":";;;;AAAA,MAAM,UAAU,QAAQ,UAAR,CAAV;AACN,MAAM,QAAQ,QAAQ,eAAR,CAAR;;AAEN,SAAS,QAAT,CAAkB,EAAE,QAAF,EAAY,MAAZ,EAAoB,IAApB,EAA0B,QAA1B,EAAlB,EAAwD;AACtD,SAAO,MAAM,QAAN,CAAe;AACpB,YADoB;AAEpB,YAAQ,UAAU,EAAV;AACR,UAAM,QAAQ,EAAR;AACN,cAAU,YAAY,EAAZ;GAJL,CAAP,CADsD;CAAxD;;AASA,SAAS,UAAT,CAAoB,EAAE,QAAF,EAApB,EAAkC;AAChC,SAAO,MAAM,UAAN,CAAiB,EAAC,QAAD,EAAjB,CAAP,CADgC;CAAlC;;;;;;;AASA,OAAO,OAAP,GAAiB,SAAS,OAAT,CAAiB,IAAjB,EAAuB;AACtC,SAAO,QACJ,IADI,CACC,IADD,EACO,KAAK,QAAL,CADP,CAEJ,IAFI,CAEC,MAAM,QAAN,CAFD,CAGJ,IAHI,CAGC,yBAAkB,QAAM,WAAxB,CAHD,CAIJ,IAJI,CAIC,KAAK,GAAL,GAAW,QAAX,GAAsB,UAAtB,CAJR,CADsC;CAAvB","file":"ban-compiled.js","sourcesContent":["const Promise = require('bluebird');\nconst Users = require('../db/adapter');\n\nfunction lockUser({ username, reason, whom, remoteip }) {\n return Users.lockUser({\n username,\n reason: reason || '',\n whom: whom || '',\n remoteip: remoteip || ''\n })\n}\n\nfunction unlockUser({ username }) {\n return Users.unlockUser({username});\n}\n\n/**\n * Bans/unbans existing user\n * @param {Object} opts\n * @return {Promise}\n */\nmodule.exports = function banUser(opts) {\n return Promise\n .bind(this, opts.username)\n .then(Users.isExists)\n .then(username => ({ ...opts, username }))\n .then(opts.ban ? lockUser : unlockUser);\n};\n"]} \ No newline at end of file diff --git a/src/actions/challenge-compiled.js b/src/actions/challenge-compiled.js deleted file mode 100644 index 7afbd77bc..000000000 --- a/src/actions/challenge-compiled.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -const Promise = require('bluebird'); -const Errors = require('common-errors'); -const emailChallenge = require('../utils/send-email.js'); -const Users = require('../db/adapter'); - -module.exports = function sendChallenge(message) { - const { username } = message; - - // TODO: record all attemps - // TODO: add metadata processing on successful email challenge - - return Promise.bind(this, username).then(Users.getUser).tap(Users.isActive).throw(new Errors.HttpStatusError(417, `${ username } is already active`)).catchReturn({ statusCode: 412 }, username).then(emailChallenge.send); -}; - -//# sourceMappingURL=challenge-compiled.js.map \ No newline at end of file diff --git a/src/actions/challenge-compiled.js.map b/src/actions/challenge-compiled.js.map deleted file mode 100644 index 8141dec6d..000000000 --- a/src/actions/challenge-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["challenge.js"],"names":[],"mappings":";;AAAA,MAAM,UAAU,QAAQ,UAAR,CAAV;AACN,MAAM,SAAS,QAAQ,eAAR,CAAT;AACN,MAAM,iBAAiB,QAAQ,wBAAR,CAAjB;AACN,MAAM,QAAQ,QAAQ,eAAR,CAAR;;AAEN,OAAO,OAAP,GAAiB,SAAS,aAAT,CAAuB,OAAvB,EAAgC;AAC/C,QAAM,EAAE,QAAF,KAAe,OAAf;;;;;AADyC,SAMxC,QACJ,IADI,CACC,IADD,EACO,QADP,EAEJ,IAFI,CAEC,MAAM,OAAN,CAFD,CAGJ,GAHI,CAGA,MAAM,QAAN,CAHA,CAIJ,KAJI,CAIE,IAAI,OAAO,eAAP,CAAuB,GAA3B,EAAgC,CAAC,GAAE,QAAH,EAAY,kBAAZ,CAAhC,CAJF,EAKJ,WALI,CAKQ,EAAE,YAAY,GAAZ,EALV,EAK6B,QAL7B,EAMJ,IANI,CAMC,eAAe,IAAf,CANR,CAN+C;CAAhC","file":"challenge-compiled.js","sourcesContent":["const Promise = require('bluebird');\nconst Errors = require('common-errors');\nconst emailChallenge = require('../utils/send-email.js');\nconst Users = require('../db/adapter');\n\nmodule.exports = function sendChallenge(message) {\n const { username } = message;\n\n // TODO: record all attemps\n // TODO: add metadata processing on successful email challenge\n\n return Promise\n .bind(this, username)\n .then(Users.getUser)\n .tap(Users.isActive)\n .throw(new Errors.HttpStatusError(417, `${username} is already active`))\n .catchReturn({ statusCode: 412 }, username)\n .then(emailChallenge.send);\n};\n"]} \ No newline at end of file diff --git a/src/actions/getInternalData-compiled.js b/src/actions/getInternalData-compiled.js deleted file mode 100644 index 915e7280b..000000000 --- a/src/actions/getInternalData-compiled.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const Promise = require('bluebird'); -const pick = require('lodash/pick'); -const Users = require('../db/adapter'); - -module.exports = function internalData(message) { - const { fields } = message; - - return Promise.bind(this, message.username).then(Users.getUser).then(data => { - return fields ? pick(data, fields) : data; - }); -}; - -//# sourceMappingURL=getInternalData-compiled.js.map \ No newline at end of file diff --git a/src/actions/getInternalData-compiled.js.map b/src/actions/getInternalData-compiled.js.map deleted file mode 100644 index 33322490d..000000000 --- a/src/actions/getInternalData-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["getInternalData.js"],"names":[],"mappings":";;AAAA,MAAM,UAAU,QAAQ,UAAR,CAAV;AACN,MAAM,OAAO,QAAQ,aAAR,CAAP;AACN,MAAM,QAAQ,QAAQ,eAAR,CAAR;;AAEN,OAAO,OAAP,GAAiB,SAAS,YAAT,CAAsB,OAAtB,EAA+B;AAC9C,QAAM,EAAE,MAAF,KAAa,OAAb,CADwC;;AAG9C,SAAO,QACJ,IADI,CACC,IADD,EACO,QAAQ,QAAR,CADP,CAEJ,IAFI,CAEC,MAAM,OAAN,CAFD,CAGJ,IAHI,CAGC,QAAQ;AACZ,WAAO,SAAS,KAAK,IAAL,EAAW,MAAX,CAAT,GAA8B,IAA9B,CADK;GAAR,CAHR,CAH8C;CAA/B","file":"getInternalData-compiled.js","sourcesContent":["const Promise = require('bluebird');\nconst pick = require('lodash/pick');\nconst Users = require('../db/adapter');\n\nmodule.exports = function internalData(message) {\n const { fields } = message;\n\n return Promise\n .bind(this, message.username)\n .then(Users.getUser)\n .then(data => {\n return fields ? pick(data, fields) : data;\n });\n};\n"]} \ No newline at end of file diff --git a/src/actions/getMetadata-compiled.js b/src/actions/getMetadata-compiled.js deleted file mode 100644 index f1e5c6d6b..000000000 --- a/src/actions/getMetadata-compiled.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -const Promise = require('bluebird'); -const noop = require('lodash/noop'); -const Users = require('../db/adapter'); - -module.exports = function getMetadataAction(message) { - const { audience, username, fields } = message; - - return Promise.bind(this, username).then(Users.isExists).then(realUsername => [realUsername, audience, fields]).spread(Users.getMetadata).tap(message.public ? Users.isPublic(username, audience) : noop); -}; - -//# sourceMappingURL=getMetadata-compiled.js.map \ No newline at end of file diff --git a/src/actions/getMetadata-compiled.js.map b/src/actions/getMetadata-compiled.js.map deleted file mode 100644 index 09ccca463..000000000 --- a/src/actions/getMetadata-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["getMetadata.js"],"names":[],"mappings":";;AAAA,MAAM,UAAU,QAAQ,UAAR,CAAV;AACN,MAAM,OAAO,QAAQ,aAAR,CAAP;AACN,MAAM,QAAQ,QAAQ,eAAR,CAAR;;AAEN,OAAO,OAAP,GAAiB,SAAS,iBAAT,CAA2B,OAA3B,EAAoC;AACnD,QAAM,EAAE,QAAF,EAAY,QAAZ,EAAsB,MAAtB,KAAiC,OAAjC,CAD6C;;AAGnD,SAAO,QACJ,IADI,CACC,IADD,EACO,QADP,EAEJ,IAFI,CAEC,MAAM,QAAN,CAFD,CAGJ,IAHI,CAGC,gBAAgB,CAAC,YAAD,EAAe,QAAf,EAAyB,MAAzB,CAAhB,CAHD,CAIJ,MAJI,CAIG,MAAM,WAAN,CAJH,CAKJ,GALI,CAKA,QAAQ,MAAR,GAAiB,MAAM,QAAN,CAAe,QAAf,EAAyB,QAAzB,CAAjB,GAAsD,IAAtD,CALP,CAHmD;CAApC","file":"getMetadata-compiled.js","sourcesContent":["const Promise = require('bluebird');\nconst noop = require('lodash/noop');\nconst Users = require('../db/adapter');\n\nmodule.exports = function getMetadataAction(message) {\n const { audience, username, fields } = message;\n\n return Promise\n .bind(this, username)\n .then(Users.isExists)\n .then(realUsername => [realUsername, audience, fields])\n .spread(Users.getMetadata)\n .tap(message.public ? Users.isPublic(username, audience) : noop);\n};\n"]} \ No newline at end of file diff --git a/src/actions/list-compiled.js b/src/actions/list-compiled.js deleted file mode 100644 index 136b5b398..000000000 --- a/src/actions/list-compiled.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -const Users = require('../adapter'); -const fsort = require('redis-filtered-sort'); -const { USERS_INDEX, USERS_PUBLIC_INDEX } = require('../constants.js'); - -module.exports = function iterateOverActiveUsers(opts) { - const { criteria, audience, filter } = opts; - - return Users.getList({ - criteria, - audience, - filter, - index: opts.public ? USERS_PUBLIC_INDEX : USERS_INDEX, - strFilter: typeof filter === 'string' ? filter : fsort.filter(filter || {}), - order: opts.order || 'ASC', - offset: opts.offset || 0, - limit: opts.limit || 10 - }); -}; - -//# sourceMappingURL=list-compiled.js.map \ No newline at end of file diff --git a/src/actions/list-compiled.js.map b/src/actions/list-compiled.js.map deleted file mode 100644 index 1cd55afa8..000000000 --- a/src/actions/list-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["list.js"],"names":[],"mappings":";;AAAA,MAAM,QAAQ,QAAQ,YAAR,CAAR;AACN,MAAM,QAAQ,QAAQ,qBAAR,CAAR;AACN,MAAM,EAAE,WAAF,EAAe,kBAAf,KAAsC,QAAQ,iBAAR,CAAtC;;AAEN,OAAO,OAAP,GAAiB,SAAS,sBAAT,CAAgC,IAAhC,EAAsC;AACrD,QAAM,EAAE,QAAF,EAAY,QAAZ,EAAsB,MAAtB,KAAiC,IAAjC,CAD+C;;AAGrD,SAAO,MAAM,OAAN,CAAc;AACnB,YADmB;AAEnB,YAFmB;AAGnB,UAHmB;AAInB,WAAO,KAAK,MAAL,GAAc,kBAAd,GAAmC,WAAnC;AACP,eAAW,OAAO,MAAP,KAAkB,QAAlB,GAA6B,MAA7B,GAAsC,MAAM,MAAN,CAAa,UAAU,EAAV,CAAnD;AACX,WAAO,KAAK,KAAL,IAAc,KAAd;AACP,YAAQ,KAAK,MAAL,IAAe,CAAf;AACR,WAAO,KAAK,KAAL,IAAc,EAAd;GARF,CAAP,CAHqD;CAAtC","file":"list-compiled.js","sourcesContent":["const Users = require('../adapter');\nconst fsort = require('redis-filtered-sort');\nconst { USERS_INDEX, USERS_PUBLIC_INDEX } = require('../constants.js');\n\nmodule.exports = function iterateOverActiveUsers(opts) {\n const { criteria, audience, filter } = opts;\n\n return Users.getList({\n criteria,\n audience,\n filter,\n index: opts.public ? USERS_PUBLIC_INDEX : USERS_INDEX,\n strFilter: typeof filter === 'string' ? filter : fsort.filter(filter || {}),\n order: opts.order || 'ASC',\n offset: opts.offset || 0,\n limit: opts.limit || 10\n });\n\n};\n"]} \ No newline at end of file diff --git a/src/db/adapter-compiled.js b/src/db/adapter-compiled.js deleted file mode 100644 index ef595fd6b..000000000 --- a/src/db/adapter-compiled.js +++ /dev/null @@ -1,366 +0,0 @@ -'use strict'; - -/** - * Created by Stainwoortsel on 30.05.2016. - */ -const RedisStorage = require('./redisstorage'); -const Errors = require('common-errors'); - -class Users { - constructor(adapter) { - - this.adapter = adapter; - - /* - let opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - // init configuration - const config = this._config = _extends({}, defaultOpts, opts); - - // setup hooks - forOwn(config.hooks, (_hooks, eventName) => { - const hooks = Array.isArray(_hooks) ? _hooks : [_hooks]; - each(hooks, hook => this.on(eventName, hook)); - }); - */ - } - - /** - * Initialize connection - * @return {Promise} - */ - connect() {} - // ???? - - /** - * Close connection - * return {Promise} - */ - close() {} - // ???? - - /** - * Lock user - * @param username - * @param reason - * @param whom - * @param remoteip - * @returns {Redis} - */ - lockUser({ username, reason, whom, remoteip }) { - return this.adapter.lockUser({ username, reason, whom, remoteip }); - } - - /** - * Unlock user - * @param username - * @returns {Redis} - */ - unlockUser(username) { - return this.adapter.unlockUser(username); - } - - /** - * Check existance of user - * @param username - * @returns {Redis} - */ - isExists(username) { - return this.adapter.isExists(username); - } - - isAliasExists(alias, thunk) { - return this.adapter.isAliasExists(alias, thunk); - } - - /** - * User is public - * @param username - * @param audience - * @returns {function()} - */ - isPublic(username, audience) { - return this.adapter.isPublic(username, audience); - } - - /** - * Check that user is active - * @param data - * @returns {boolean} - */ - isActive(data) { - return this.adapter.isActive(data); - } - - /** - * Check that user is banned - * @param data - * @returns {Promise} - */ - isBanned(data) { - return this.adapter.isBanned(data); - } - - /** - * Activate user account - * @param user - * @returns {Redis} - */ - activateAccount(user) { - return this.adapter.activateAccount(user); - } - - /** - * Get user internal data - * @param username - * @returns {Object} - */ - getUser(username) { - return this.adapter.getUser(username); - } - - /** - * Get users metadata by username and audience - * @param username - * @param audience - * @returns {Object} - */ - - getMetadata(username, _audiences, fields = {}) { - return this.adapter.getMetadata(username, _audiences, fields); - } - - /** - * Return the list of users by specified params - * @param opts - * @returns {Array} - */ - getList(opts) { - return this.adapter.getList(opts); - } - - /** - * Check existence of alias - * @param data - * @returns {boolean} - */ - isAliasAssigned(data) { - return this.adapter.isAliasAssigned(data); - } - - /** - * Check that user is admin - * @param meta - * @returns {boolean} - */ - isAdmin(meta) { - return this.adapter.isAdmin(meta); - } - - /** - * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN - * @param username - * @param alias - * @returns {Redis} - */ - storeAlias(username, alias) { - return this.adapter.storeAlias(username, alias); - } - - /** - * Assign alias to the user record, marked by username - * @param username - * @param alias - * @returns {Redis} - */ - assignAlias(username, alias) { - return this.adapter.assignAlias(username, alias); - } - - get remoteipKey() { - return this.adapter.remoteipKey; - } - - set remoteipKey(val) { - this.adapter.remoteipKey = val; - } - - generateipKey(username, remoteip) { - return this.adapter.generateipKey(username, remoteip); - } - - get loginAttempts() { - return this.adapter.loginAttempts; - } - - set loginAttempts(val) { - this.adapter.loginAttempts = val; - } - - get options() { - return this.adapter.options; - } - - set options(opts) { - this.adapter.options = opts; - } - - dropAttempts() { - return this.adapter.dropAttempts(); - } - - checkLoginAttempts(data) { - return this.adapter.checkLoginAttempts(data); - } - - /** - * Set user password - * @param username - * @param hash - * @returns {Redis} - */ - setPassword(username, hash) { - return this.adapter.setPassword(username, hash); - } - - /** - * Reset the lock by IP - * @param username - * @param ip - * @returns {Redis} - */ - resetIPLock(username, ip) { - return this.adapter.resetIPLock(username, ip); - } - - /** - * - * @param username - * @param audience - * @param metadata - * @returns {Object} - */ - updateMetadata({ username, audience, metadata }) { - return this.adapter.updateMetadata({ username, audience, metadata }); - } - - /** - * Removing user by username (and data?) - * @param username - * @param data - * @returns {Redis} - */ - removeUser(username, data) { - return this.adapter.removeUser(username, data); - } - - /** - * Verify ip limits - * @param {redisCluster} redis - * @param {Object} registrationLimits - * @param {String} ipaddress - * @return {Function} - */ - checkLimits(registrationLimits, ipaddress) { - return this.adapter.checkLimits(registrationLimits, ipaddress); - } - - /** - * Creates user with a given hash - * @param redis - * @param username - * @param activate - * @param deleteInactiveAccounts - * @param userDataKey - * @returns {Function} - */ - createUser(username, activate, deleteInactiveAccounts) { - return this.adapter.createUser(username, activate, deleteInactiveAccounts); - } - - /** - * Performs captcha check, returns thukn - * @param {String} username - * @param {String} captcha - * @param {Object} captchaConfig - * @return {Function} - */ - checkCaptcha(username, captcha) { - return this.adapter.checkCaptcha(username, captcha); - } - - /** - * Stores username to the index set - * @param username - * @returns {Redis} - */ - storeUsername(username) { - return this.adapter.storeUsername(username); - } - - /** - * Running a custom script or query - * @param script - * @returns {*|Promise} - */ - - customScript(script) { - return this.adapter.customScript(script); - } - - /** - * The error wrapper for the front-level HTTP output - * @param e - */ - static mapErrors(e) { - const err = new Errors.HttpStatusError(e.status_code || 500, e.message); - if (err.status_code >= 500) { - err.message = Errors.HttpStatusError.message_map[500]; //hide the real error from the user - } - } - -} - -module.exports = function modelCreator() { - return new Users(RedisStorage); -}; - -/* - ВОПРОСЫ: - Не превращается ли адаптер в полноценную модель? - Что делать с промисами? Правильно ли частично их пихать в адаптер (по идее, соединение -- ресурс, так что да)? - Архитектура MServices, где берется redis? - Оставить Errors снаружи? - -+ ЭМИТТЕР НЕ НУЖЕН -+ МОЖНО СДЕЛАТЬ ХУКИ, но только если нужно -~ ЭРРОРЫ НАДО ВЫНЕСТИ НАРУЖУ с сообщениями, а внутри генерить женерик-эрроры с кодами, врапить их в экшне в HTTPошикби - sandbox/activate.js -> Если это модель, то оствлять ли всякие verifyToken, emailVerification и хуки снаружи? - СНАРУЖИ -+ sandbox/alias.js -> 18, 25 (запихнуть их в User?) не надо, все верно - sandbox/getMetadata -> волевым решением, логика Metadata вместе с промисами запихнута в метод getMetadata. С точки зрения абстракции всё соблюдено, но правильно ли это для текущей ситуации? - ДА, МОЖНО - но надо сделать разницу между трансформатором данных - и селектором - плюс вытянуть свежую репу - sandbox/list -> метод getList настолько широк, что поглатил в себя всю реализацию этого экшна. разве это хорошо? - ВСЕ ОК, дефолты можно вытащить наружу. подумать над общим форматом ответа и соотв-но вытащить кое что в методы сторожа - sandbox/login -> 25, передаем options в адаптер... что-то не в порядке в королевстве Датском! - МЕНЯЕМ логику работы промисов. чтобы не городить нерабочий огород в адаптере - - redisstorage -> 394 this.log. раньше this брался из экшна, к чему относится метод log? - redisstorage -> 148,157 оборачивать ли эти методы? эти статусы потенциально зависят от адаптера, но должны выводить значение в чистом виде. С другой стороны, в чистом виде значение не используется, а используется промис - ЛОГИЧНЕЕ будет сформить метод с промисом и кидаться ошибками на верхний уровень, собстна в sql логика будет та же - нафига нужны просто флаги -- не понятно - redisstorage -> 105 что на счет методов с thunk'ом? - ПОСМОТРЕТЬ что делает thunk и где он участвует, вожможно придется оставить - - bluebird: tap - - ПОСМОТРЕТЬ levelDB и схожие адаптеры для работы с логикой - - МОЖНО в адаптере сделать трансмиттер ошибок адаптера в ошибки HTTP - - */ - -//# sourceMappingURL=adapter-compiled.js.map \ No newline at end of file diff --git a/src/db/adapter-compiled.js.map b/src/db/adapter-compiled.js.map deleted file mode 100644 index a68ad9a28..000000000 --- a/src/db/adapter-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["adapter.js"],"names":[],"mappings":"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","file":"adapter-compiled.js","sourcesContent":["/**\n * Created by Stainwoortsel on 30.05.2016.\n */\nconst RedisStorage = require('./redisstorage');\nconst Errors = require('common-errors');\n\nclass Users{\n constructor(adapter){\n\n this.adapter = adapter;\n\n/*\n let opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];\n\n // init configuration\n const config = this._config = _extends({}, defaultOpts, opts);\n\n // setup hooks\n forOwn(config.hooks, (_hooks, eventName) => {\n const hooks = Array.isArray(_hooks) ? _hooks : [_hooks];\n each(hooks, hook => this.on(eventName, hook));\n });\n*/\n\n }\n\n /**\n * Initialize connection\n * @return {Promise}\n */\n connect(){\n // ????\n }\n\n /**\n * Close connection\n * return {Promise}\n */\n close(){\n // ????\n }\n\n\n /**\n * Lock user\n * @param username\n * @param reason\n * @param whom\n * @param remoteip\n * @returns {Redis}\n */\n lockUser({ username, reason, whom, remoteip }){\n return this.adapter.lockUser({ username, reason, whom, remoteip });\n }\n\n /**\n * Unlock user\n * @param username\n * @returns {Redis}\n */\n unlockUser(username){\n return this.adapter.unlockUser(username);\n }\n\n /**\n * Check existance of user\n * @param username\n * @returns {Redis}\n */\n isExists(username){\n return this.adapter.isExists(username);\n }\n\n isAliasExists(alias, thunk){\n return this.adapter.isAliasExists(alias, thunk);\n }\n\n /**\n * User is public\n * @param username\n * @param audience\n * @returns {function()}\n */\n isPublic(username, audience) {\n return this.adapter.isPublic(username, audience);\n }\n\n /**\n * Check that user is active\n * @param data\n * @returns {boolean}\n */\n isActive(data){\n return this.adapter.isActive(data);\n }\n\n /**\n * Check that user is banned\n * @param data\n * @returns {Promise}\n */\n isBanned(data){\n return this.adapter.isBanned(data);\n }\n\n /**\n * Activate user account\n * @param user\n * @returns {Redis}\n */\n activateAccount(user){\n return this.adapter.activateAccount(user);\n }\n\n /**\n * Get user internal data\n * @param username\n * @returns {Object}\n */\n getUser(username){\n return this.adapter.getUser(username);\n }\n\n /**\n * Get users metadata by username and audience\n * @param username\n * @param audience\n * @returns {Object}\n */\n\n getMetadata(username, _audiences, fields = {}) {\n return this.adapter.getMetadata(username, _audiences, fields);\n }\n\n\n /**\n * Return the list of users by specified params\n * @param opts\n * @returns {Array}\n */\n getList(opts){\n return this.adapter.getList(opts);\n }\n\n /**\n * Check existence of alias\n * @param data\n * @returns {boolean}\n */\n isAliasAssigned(data){\n return this.adapter.isAliasAssigned(data);\n }\n\n /**\n * Check that user is admin\n * @param meta\n * @returns {boolean}\n */\n isAdmin(meta){\n return this.adapter.isAdmin(meta);\n }\n\n /**\n * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN\n * @param username\n * @param alias\n * @returns {Redis}\n */\n storeAlias(username, alias){\n return this.adapter.storeAlias(username, alias);\n }\n\n /**\n * Assign alias to the user record, marked by username\n * @param username\n * @param alias\n * @returns {Redis}\n */\n assignAlias(username, alias){\n return this.adapter.assignAlias(username, alias);\n }\n\n get remoteipKey(){\n return this.adapter.remoteipKey;\n }\n\n set remoteipKey(val){\n this.adapter.remoteipKey = val;\n }\n\n generateipKey(username, remoteip){\n return this.adapter.generateipKey(username, remoteip);\n }\n\n get loginAttempts(){\n return this.adapter.loginAttempts;\n }\n\n set loginAttempts(val){\n this.adapter.loginAttempts = val;\n }\n\n get options(){\n return this.adapter.options;\n }\n\n set options(opts){\n this.adapter.options = opts;\n }\n\n dropAttempts(){\n return this.adapter.dropAttempts();\n }\n\n checkLoginAttempts(data) {\n return this.adapter.checkLoginAttempts(data);\n }\n\n /**\n * Set user password\n * @param username\n * @param hash\n * @returns {Redis}\n */\n setPassword(username, hash){\n return this.adapter.setPassword(username, hash);\n }\n\n /**\n * Reset the lock by IP\n * @param username\n * @param ip\n * @returns {Redis}\n */\n resetIPLock(username, ip){\n return this.adapter.resetIPLock(username, ip);\n }\n\n /**\n *\n * @param username\n * @param audience\n * @param metadata\n * @returns {Object}\n */\n updateMetadata({username, audience, metadata}) {\n return this.adapter.updateMetadata({username, audience, metadata});\n }\n\n /**\n * Removing user by username (and data?)\n * @param username\n * @param data\n * @returns {Redis}\n */\n removeUser(username, data){\n return this.adapter.removeUser(username, data);\n }\n\n /**\n * Verify ip limits\n * @param {redisCluster} redis\n * @param {Object} registrationLimits\n * @param {String} ipaddress\n * @return {Function}\n */\n checkLimits(registrationLimits, ipaddress) {\n return this.adapter.checkLimits(registrationLimits, ipaddress);\n }\n\n /**\n * Creates user with a given hash\n * @param redis\n * @param username\n * @param activate\n * @param deleteInactiveAccounts\n * @param userDataKey\n * @returns {Function}\n */\n createUser(username, activate, deleteInactiveAccounts) {\n return this.adapter.createUser(username, activate, deleteInactiveAccounts);\n }\n\n /**\n * Performs captcha check, returns thukn\n * @param {String} username\n * @param {String} captcha\n * @param {Object} captchaConfig\n * @return {Function}\n */\n checkCaptcha(username, captcha) {\n return this.adapter.checkCaptcha(username, captcha);\n }\n\n /**\n * Stores username to the index set\n * @param username\n * @returns {Redis}\n */\n storeUsername(username){\n return this.adapter.storeUsername(username);\n }\n\n /**\n * Running a custom script or query\n * @param script\n * @returns {*|Promise}\n */\n\n customScript(script){\n return this.adapter.customScript(script);\n }\n\n /**\n * The error wrapper for the front-level HTTP output\n * @param e\n */\n static mapErrors(e){\n const err = new Errors.HttpStatusError(e.status_code || 500 , e.message);\n if(err.status_code >= 500) {\n err.message = Errors.HttpStatusError.message_map[500]; //hide the real error from the user\n }\n }\n\n}\n\nmodule.exports = function modelCreator(){\n return new Users(RedisStorage);\n};\n\n\n/*\n ВОПРОСЫ:\n Не превращается ли адаптер в полноценную модель?\n Что делать с промисами? Правильно ли частично их пихать в адаптер (по идее, соединение -- ресурс, так что да)?\n Архитектура MServices, где берется redis?\n Оставить Errors снаружи?\n\n+ ЭМИТТЕР НЕ НУЖЕН\n+ МОЖНО СДЕЛАТЬ ХУКИ, но только если нужно\n~ ЭРРОРЫ НАДО ВЫНЕСТИ НАРУЖУ с сообщениями, а внутри генерить женерик-эрроры с кодами, врапить их в экшне в HTTPошикби\n sandbox/activate.js -> Если это модель, то оствлять ли всякие verifyToken, emailVerification и хуки снаружи?\n СНАРУЖИ\n+ sandbox/alias.js -> 18, 25 (запихнуть их в User?) не надо, все верно\n sandbox/getMetadata -> волевым решением, логика Metadata вместе с промисами запихнута в метод getMetadata. С точки зрения абстракции всё соблюдено, но правильно ли это для текущей ситуации?\n ДА, МОЖНО\n но надо сделать разницу между трансформатором данных\n и селектором\n плюс вытянуть свежую репу\n sandbox/list -> метод getList настолько широк, что поглатил в себя всю реализацию этого экшна. разве это хорошо?\n ВСЕ ОК, дефолты можно вытащить наружу. подумать над общим форматом ответа и соотв-но вытащить кое что в методы сторожа\n sandbox/login -> 25, передаем options в адаптер... что-то не в порядке в королевстве Датском!\n МЕНЯЕМ логику работы промисов. чтобы не городить нерабочий огород в адаптере\n\n redisstorage -> 394 this.log. раньше this брался из экшна, к чему относится метод log?\n redisstorage -> 148,157 оборачивать ли эти методы? эти статусы потенциально зависят от адаптера, но должны выводить значение в чистом виде. С другой стороны, в чистом виде значение не используется, а используется промис\n ЛОГИЧНЕЕ будет сформить метод с промисом и кидаться ошибками на верхний уровень, собстна в sql логика будет та же\n нафига нужны просто флаги -- не понятно\n redisstorage -> 105 что на счет методов с thunk'ом?\n ПОСМОТРЕТЬ что делает thunk и где он участвует, вожможно придется оставить\n\n bluebird: tap\n\n ПОСМОТРЕТЬ levelDB и схожие адаптеры для работы с логикой\n\n МОЖНО в адаптере сделать трансмиттер ошибок адаптера в ошибки HTTP\n\n */\n"]} \ No newline at end of file diff --git a/src/db/redisstorage-compiled.js b/src/db/redisstorage-compiled.js deleted file mode 100644 index caa67f248..000000000 --- a/src/db/redisstorage-compiled.js +++ /dev/null @@ -1,605 +0,0 @@ -'use strict'; - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -/** - * Created by Stainwoortsel on 30.05.2016. - */ -const Promise = require('bluebird'); -const Errors = require('common-errors'); -const mapValues = require('lodash/mapValues'); -const defaults = require('lodash/defaults'); -const get = require('lodash/get'); -const pick = require('lodash/pick'); -const request = require('request-promise'); -const uuid = require('node-uuid'); -const fsort = require('redis-filtered-sort'); -const fmt = require('util').format; -const is = require('is'); -const sha256 = require('./sha256.js'); -const moment = require('moment'); - -const stringify = JSON.stringify.bind(JSON); -const { - USERS_DATA, USERS_METADATA, USERS_ALIAS_TO_LOGIN, - USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA, - USERS_ACTIVE_FLAG, USERS_INDEX, USERS_PUBLIC_INDEX, - USERS_ALIAS_FIELD -} = require('../constants.js'); - -const { redis, captcha: captchaConfig, config } = this; -const { jwt: { lockAfterAttempts, defaultAudience } } = config; - -/** - * Generate hash key string - * @param args - * @returns {string} - */ -const generateKey = (...args) => { - const SEPARATOR = '!'; - return args.join(SEPARATOR); -}; - -module.exports = { - /** - * Lock user - * @param username - * @param reason - * @param whom - * @param remoteip - * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} - */ - lockUser({ username, reason, whom, remoteip }) { - const data = { - banned: true, - [USERS_BANNED_DATA]: { - reason, - whom, - remoteip - } - }; - - return redis.pipeline().hset(generateKey(username, USERS_DATA), USERS_BANNED_FLAG, 'true') - // set .banned on metadata for filtering & sorting users by that field - .hmset(generateKey(username, USERS_METADATA, defaultAudience), mapValues(data, stringify)).del(generateKey(username, USERS_TOKENS)).exec(); - }, - - /** - * Unlock user - * @param username - * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} - */ - unlockUser({ username }) { - return redis.pipeline().hdel(generateKey(username, USERS_DATA), USERS_BANNED_FLAG) - // remove .banned on metadata for filtering & sorting users by that field - .hdel(generateKey(username, USERS_METADATA, defaultAudience), 'banned', USERS_BANNED_DATA).exec(); - }, - - /** - * Check existance of user - * @param username - * @returns {Redis} - */ - isExists(username) { - return redis.pipeline().hget(USERS_ALIAS_TO_LOGIN, username).exists(generateKey(username, USERS_DATA)).exec().spread((alias, exists) => { - if (alias[1]) { - return alias[1]; - } - - if (!exists[1]) { - throw new Errors.HttpStatusError(404, `"${ username }" does not exists`); - } - - return username; - }); - }, - - isAliasExists(alias, thunk) { - function resolveAlias(alias) { - return redis.hget(USERS_ALIAS_TO_LOGIN, alias).then(username => { - if (username) { - throw new Errors.HttpStatusError(409, `"${ alias }" already exists`); - } - - return username; - }); - } - if (thunk) { - return function resolveAliasThunk() { - return resolveAlias(alias); - }; - } - - return resolveAlias(alias); - }, - - /** - * User is public - * @param username - * @param audience - * @returns {function()} - */ - isPublic(username, audience) { - return metadata => { - if (get(metadata, [audience, USERS_ALIAS_FIELD]) === username) { - return; - } - - throw new Errors.HttpStatusError(404, 'username was not found'); - }; - }, - - /** - * Check that user is active - * @param data - * @returns {Promise} - */ - isActive(data) { - if (String(data[USERS_ACTIVE_FLAG]) !== 'true') { - return Promise.reject(new Errors.HttpStatusError(412, 'Account hasn\'t been activated')); - } - - return Promise.resolve(data); - }, - - /** - * Check that user is banned - * @param data - * @returns {Promise} - */ - isBanned(data) { - if (String(data[USERS_BANNED_FLAG]) === 'true') { - return Promise.reject(new Errors.HttpStatusError(423, 'Account has been locked')); - } - - return Promise.resolve(data); - }, - - /** - * Activate user account - * @param user - * @returns {Redis} - */ - activateAccount(user) { - const userKey = generateKey(user, USERS_DATA); - - // WARNING: `persist` is very important, otherwise we will lose user's information in 30 days - // set to active & persist - return redis.pipeline().hget(userKey, USERS_ACTIVE_FLAG).hset(userKey, USERS_ACTIVE_FLAG, 'true').persist(userKey).sadd(USERS_INDEX, user).exec().spread(function pipeResponse(isActive) { - const status = isActive[1]; - if (status === 'true') { - throw new Errors.HttpStatusError(417, `Account ${ user } was already activated`); - } - }); - }, - - /** - * Get user internal data - * @param username - * @returns {Object} - */ - getUser(username) { - const userKey = generateKey(username, USERS_DATA); - - return redis.pipeline().hget(USERS_ALIAS_TO_LOGIN, username).exists(userKey).hgetallBuffer(userKey).exec().spread((aliasToUsername, exists, data) => { - if (aliasToUsername[1]) { - return this.getUser(aliasToUsername[1]); - } - - if (!exists[1]) { - throw new Errors.HttpStatusError(404, `"${ username }" does not exists`); - } - - return _extends({}, data[1], { username }); - }); - }, - - /** - * Get users metadata by username and audience - * @param username - * @param audience - * @returns {Object} - */ - - // getMetadata(username, audience){ - // return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience)); - // }, - - getMetadata(username, _audiences, fields = {}) { - const audiences = Array.isArray(_audiences) ? _audiences : [_audiences]; - - return Promise.map(audiences, audience => { - return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience)); - }).then(function remapAudienceData(data) { - const output = {}; - audiences.forEach(function transform(aud, idx) { - const datum = data[idx]; - - if (datum) { - const pickFields = fields[aud]; - output[aud] = mapValues(datum, JSONParse); - if (pickFields) { - output[aud] = pick(output[aud], pickFields); - } - } else { - output[aud] = {}; - } - }); - - return output; - }); - }, - - /** - * Return the list of users by specified params - * @param opts - * @returns {Array} - */ - getList(opts) { - const { criteria, audience, filter, index, strFilter, order, offset, limit } = opts; - const metaKey = generateKey('*', USERS_METADATA, audience); - - return redis.fsort(index, metaKey, criteria, order, strFilter, offset, limit).then(ids => { - const length = +ids.pop(); - if (length === 0 || ids.length === 0) { - return [ids || [], [], length]; - } - - const pipeline = redis.pipeline(); - ids.forEach(id => { - pipeline.hgetallBuffer(redisKey(id, USERS_METADATA, audience)); - }); - return Promise.all([ids, pipeline.exec(), length]); - }).spread((ids, props, length) => { - const users = ids.map(function remapData(id, idx) { - const data = props[idx][1]; - const account = { - id, - metadata: { - [audience]: data ? mapValues(data, JSONParse) : {} - } - }; - - return account; - }); - - return { - users, - cursor: offset + limit, - page: Math.floor(offset / limit + 1), - pages: Math.ceil(length / limit) - }; - }); - }, - - /** - * Check existence of alias - * @param data - * @returns {boolean} - */ - isAliasAssigned(data) { - return data[USERS_ALIAS_FIELD] !== undefined; // was just `data[USERS_ALIAS_FIELD]` - }, - - /** - * Check that user is admin - * @param meta - * @returns {boolean} - */ - isAdmin(meta) { - const audience = config.jwt.defaultAudience; - return (meta[audience].roles || []).indexOf(USERS_ADMIN_ROLE) >= 0; - }, - - /** - * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN - * @param username - * @param alias - * @returns {Redis} - */ - storeAlias(username, alias) { - return redis.hsetnx(USERS_ALIAS_TO_LOGIN, alias, username); - }, - - /** - * Assign alias to the user record, marked by username - * @param username - * @param alias - * @returns {Redis} - */ - assignAlias(username, alias) { - return redis.pipeline().sadd(USERS_PUBLIC_INDEX, username).hset(generateKey(username, USERS_DATA), USERS_ALIAS_FIELD, alias).hset(generateKey(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, stringify).exec(); - }, - - _remoteipKey: '', - get remoteipKey() { - return this._remoteipKey; - }, - - set remoteipKey(val) { - this._remoteipKey = val; - }, - - generateipKey(username, remoteip) { - return this._remoteipKey = generateKey(username, 'ip', remoteip); - }, - - _loginAttempts: 0, - get loginAttempts() { - return this._loginAttempts; - }, - set loginAttempts(val) { - this._loginAttempts = val; - }, - - _options: {}, - get options() { - return this._options; - }, - - set options(opts) { - this._options = opts; - }, - - dropAttempts() { - this._loginAttempts = 0; - return redis.del(this.key); - }, - checkLoginAttempts(data) { - const pipeline = redis.pipeline(); - const username = data.username; - const remoteipKey = this.generateipKey(username, this._options.remoteip); - - pipeline.incrby(remoteipKey, 1); - if (config.jwt.keepLoginAttempts > 0) { - pipeline.expire(remoteipKey, config.jwt.keepLoginAttempts); - } - - return pipeline.exec().spread(function incremented(incrementValue) { - const err = incrementValue[0]; - if (err) { - this.log.error('Redis error:', err); - return; - } - - this.loginAttempts = incrementValue[1]; - if (this.loginAttempts > lockAfterAttempts) { - const duration = moment().add(config.jwt.keepLoginAttempts, 'seconds').toNow(true); - const msg = `You are locked from making login attempts for the next ${ duration }`; - throw new Errors.HttpStatusError(429, msg); - } - }); - }, - - /** - * Set user password - * @param username - * @param hash - * @returns {Redis} - */ - setPassword(username, hash) { - return redis.hset(generateKey(username, USERS_DATA), 'password', hash).return(username); - }, - - /** - * Reset the lock by IP - * @param username - * @param ip - * @returns {Redis} - */ - resetIPLock(username, ip) { - return redis.del(generateKey(username, 'ip', ip)); - }, - - /** - * - * @param username - * @param audience - * @param metadata - * @returns {Object} - */ - updateMetadata({ username, audience, metadata, script }) { - const audiences = is.array(audience) ? audience : [audience]; - - // keys - const keys = audiences.map(aud => redisKey(username, USERS_METADATA, aud)); - - // if we have meta, then we can - if (metadata) { - const pipe = redis.pipeline(); - const metaOps = is.array(metadata) ? metadata : [metadata]; - const operations = metaOps.map((meta, idx) => handleAudience(pipe, keys[idx], meta)); - return pipe.exec().then(res => mapMetaResponse(operations, res)); - } - - //or... - return this.customScript(script); - }, - - /** - * Removing user by username (and data?) - * @param username - * @param data - * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} - */ - removeUser(username, data) { - const audience = config.jwt.defaultAudience; - const transaction = redis.multi(); - const alias = data[USERS_ALIAS_FIELD]; - if (alias) { - transaction.hdel(USERS_ALIAS_TO_LOGIN, alias); - } - - // clean indices - transaction.srem(USERS_PUBLIC_INDEX, username); - transaction.srem(USERS_INDEX, username); - - // remove metadata & internal data - transaction.del(generateKey(username, USERS_DATA)); - transaction.del(generateKey(username, USERS_METADATA, audience)); - - // remove auth tokens - transaction.del(generateKey(username, USERS_TOKENS)); - - // complete it - return transaction.exec(); - }, - - /** - * Verify ip limits - * @param {redisCluster} redis - * @param {Object} registrationLimits - * @param {String} ipaddress - * @return {Function} - */ - checkLimits(registrationLimits, ipaddress) { - const { ip: { time, times } } = registrationLimits; - const ipaddressLimitKey = generateKey('reg-limit', ipaddress); - const now = Date.now(); - const old = now - time; - - return function iplimits() { - return redis.pipeline().zadd(ipaddressLimitKey, now, uuid.v4()).pexpire(ipaddressLimitKey, time).zremrangebyscore(ipaddressLimitKey, '-inf', old).zcard(ipaddressLimitKey).exec().then(props => { - const cardinality = props[3][1]; - if (cardinality > times) { - const msg = 'You can\'t register more users from your ipaddress now'; - throw new Errors.HttpStatusError(429, msg); - } - }); - }; - }, - - /** - * Creates user with a given hash - * @param redis - * @param username - * @param activate - * @param deleteInactiveAccounts - * @param userDataKey - * @returns {Function} - */ - createUser(username, activate, deleteInactiveAccounts) { - /** - * Input from scrypt.hash - */ - const userDataKey = generateKey(username, USERS_DATA); - - return function create(hash) { - const pipeline = redis.pipeline(); - - pipeline.hsetnx(userDataKey, 'password', hash); - pipeline.hsetnx(userDataKey, USERS_ACTIVE_FLAG, activate); - - return pipeline.exec().spread(function insertedUserData(passwordSetResponse) { - if (passwordSetResponse[1] === 0) { - throw new Errors.HttpStatusError(412, `User "${ username }" already exists`); - } - - if (!activate && deleteInactiveAccounts >= 0) { - // WARNING: IF USER IS NOT VERIFIED WITHIN - // [by default 30] DAYS - IT WILL BE REMOVED FROM DATABASE - return redis.expire(userDataKey, deleteInactiveAccounts); - } - - return null; - }); - }; - }, - - /** - * Performs captcha check, returns thukn - * @param {String} username - * @param {String} captcha - * @param {Object} captchaConfig - * @return {Function} - */ - checkCaptcha(username, captcha) { - const { secret, ttl, uri } = captchaConfig; - return function checkCaptcha() { - const captchaCacheKey = captcha.response; - return redis.pipeline().set(captchaCacheKey, username, 'EX', ttl, 'NX').get(captchaCacheKey).exec().spread(function captchaCacheResponse(setResponse, getResponse) { - if (getResponse[1] !== username) { - const msg = 'Captcha challenge you\'ve solved can not be used, please complete it again'; - throw new Errors.HttpStatusError(412, msg); - } - }).then(function verifyGoogleCaptcha() { - return request.post({ uri, qs: defaults(captcha, { secret }), json: true }).then(function captchaSuccess(body) { - if (!body.success) { - return Promise.reject({ statusCode: 200, error: body }); - } - - return true; - }).catch(function captchaError(err) { - const errData = JSON.stringify(pick(err, ['statusCode', 'error'])); - throw new Errors.HttpStatusError(412, fmt('Captcha response: %s', errData)); - }); - }); - }; - }, - - /** - * Stores username to the index set - * @param username - * @returns {Redis} - */ - storeUsername(username) { - return redis.sadd(USERS_INDEX, username); - }, - - /** - * Execute custom script on LUA - * @param script - * @returns {Promise} - */ - customScript(script) { - // dynamic scripts - const $scriptKeys = Object.keys(script); - const scripts = $scriptKeys.map(scriptName => { - const { lua, argv = [] } = script[scriptName]; - const sha = sha256(lua); - const name = `ms_users_${ sha }`; - if (!is.fn(redis[name])) { - redis.defineCommand(name, { lua }); - } - return redis[name](keys.length, keys, argv); - }); - - return Promise.all(scripts).then(res => { - const output = {}; - $scriptKeys.forEach((fieldName, idx) => { - output[fieldName] = res[idx]; - }); - return output; - }); - }, - - handleAudience(key, metadata) { - const pipeline = redis.pipeline(); - const $remove = metadata.$remove; - const $removeOps = $remove && $remove.length || 0; - if ($removeOps > 0) { - pipeline.hdel(key, $remove); - } - - const $set = metadata.$set; - const $setKeys = $set && Object.keys($set); - const $setLength = $setKeys && $setKeys.length || 0; - if ($setLength > 0) { - pipeline.hmset(key, mapValues($set, stringify)); - } - - const $incr = metadata.$incr; - const $incrFields = $incr && Object.keys($incr); - const $incrLength = $incrFields && $incrFields.length || 0; - if ($incrLength > 0) { - $incrFields.forEach(fieldName => { - pipeline.hincrby(key, fieldName, $incr[fieldName]); - }); - } - - return { $removeOps, $setLength, $incrLength, $incrFields }; - } - -}; - -//# sourceMappingURL=redisstorage-compiled.js.map \ No newline at end of file diff --git a/src/db/redisstorage-compiled.js.map b/src/db/redisstorage-compiled.js.map deleted file mode 100644 index 45a5ac4b0..000000000 --- a/src/db/redisstorage-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["redisstorage.js"],"names":[],"mappings":"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","file":"redisstorage-compiled.js","sourcesContent":["/**\n * Created by Stainwoortsel on 30.05.2016.\n */\nconst Promise = require('bluebird');\nconst Errors = require('common-errors');\nconst mapValues = require('lodash/mapValues');\nconst defaults = require('lodash/defaults');\nconst get = require('lodash/get');\nconst pick = require('lodash/pick');\nconst request = require('request-promise');\nconst uuid = require('node-uuid');\nconst fsort = require('redis-filtered-sort');\nconst fmt = require('util').format;\nconst is = require('is');\nconst sha256 = require('./sha256.js');\nconst moment = require('moment');\n\nconst stringify = JSON.stringify.bind(JSON);\nconst {\n USERS_DATA, USERS_METADATA, USERS_ALIAS_TO_LOGIN,\n USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA,\n USERS_ACTIVE_FLAG, USERS_INDEX, USERS_PUBLIC_INDEX,\n USERS_ALIAS_FIELD\n} = require('../constants.js');\n\nconst { redis, captcha: captchaConfig, config } = this;\nconst { jwt: { lockAfterAttempts, defaultAudience } } = config;\n\n\n/**\n * Generate hash key string\n * @param args\n * @returns {string}\n */\nconst generateKey = (...args) => {\n const SEPARATOR = '!';\n return args.join(SEPARATOR);\n};\n\nmodule.exports = {\n /**\n * Lock user\n * @param username\n * @param reason\n * @param whom\n * @param remoteip\n * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}}\n */\n lockUser({ username, reason, whom, remoteip }){\n const data = {\n banned: true,\n [USERS_BANNED_DATA]: {\n reason,\n whom,\n remoteip\n }\n };\n\n return redis\n .pipeline()\n .hset(generateKey(username, USERS_DATA), USERS_BANNED_FLAG, 'true')\n // set .banned on metadata for filtering & sorting users by that field\n .hmset(generateKey(username, USERS_METADATA, defaultAudience), mapValues(data, stringify))\n .del(generateKey(username, USERS_TOKENS))\n .exec();\n },\n\n /**\n * Unlock user\n * @param username\n * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}}\n */\n unlockUser({username}){\n return redis\n .pipeline()\n .hdel(generateKey(username, USERS_DATA), USERS_BANNED_FLAG)\n // remove .banned on metadata for filtering & sorting users by that field\n .hdel(generateKey(username, USERS_METADATA, defaultAudience), 'banned', USERS_BANNED_DATA)\n .exec();\n\n },\n\n /**\n * Check existance of user\n * @param username\n * @returns {Redis}\n */\n isExists(username){\n return redis\n .pipeline()\n .hget(USERS_ALIAS_TO_LOGIN, username)\n .exists(generateKey(username, USERS_DATA))\n .exec()\n .spread((alias, exists) => {\n if (alias[1]) {\n return alias[1];\n }\n\n if (!exists[1]) {\n throw new Errors.HttpStatusError(404, `\"${username}\" does not exists`);\n }\n\n return username;\n });\n },\n\n isAliasExists(alias, thunk){\n function resolveAlias(alias) {\n return redis\n .hget(USERS_ALIAS_TO_LOGIN, alias)\n .then(username => {\n if (username) {\n throw new Errors.HttpStatusError(409, `\"${alias}\" already exists`);\n }\n\n return username;\n });\n }\n if (thunk) {\n return function resolveAliasThunk() {\n return resolveAlias(alias);\n };\n }\n\n return resolveAlias(alias);\n },\n\n /**\n * User is public\n * @param username\n * @param audience\n * @returns {function()}\n */\n isPublic(username, audience) {\n return metadata => {\n if (get(metadata, [audience, USERS_ALIAS_FIELD]) === username) {\n return;\n }\n\n throw new Errors.HttpStatusError(404, 'username was not found');\n };\n },\n\n\n /**\n * Check that user is active\n * @param data\n * @returns {Promise}\n */\n isActive(data){\n if (String(data[USERS_ACTIVE_FLAG]) !== 'true') {\n return Promise.reject(new Errors.HttpStatusError(412, 'Account hasn\\'t been activated'));\n }\n\n return Promise.resolve(data);\n },\n\n /**\n * Check that user is banned\n * @param data\n * @returns {Promise}\n */\n isBanned(data){\n if(String(data[USERS_BANNED_FLAG]) === 'true') {\n return Promise.reject(new Errors.HttpStatusError(423, 'Account has been locked'));\n }\n\n return Promise.resolve(data);\n },\n\n /**\n * Activate user account\n * @param user\n * @returns {Redis}\n */\n activateAccount(user){\n const userKey = generateKey(user, USERS_DATA);\n\n // WARNING: `persist` is very important, otherwise we will lose user's information in 30 days\n // set to active & persist\n return redis\n .pipeline()\n .hget(userKey, USERS_ACTIVE_FLAG)\n .hset(userKey, USERS_ACTIVE_FLAG, 'true')\n .persist(userKey)\n .sadd(USERS_INDEX, user)\n .exec()\n .spread(function pipeResponse(isActive) {\n const status = isActive[1];\n if (status === 'true') {\n throw new Errors.HttpStatusError(417, `Account ${user} was already activated`);\n }\n });\n },\n\n /**\n * Get user internal data\n * @param username\n * @returns {Object}\n */\n getUser(username){\n const userKey = generateKey(username, USERS_DATA);\n\n return redis\n .pipeline()\n .hget(USERS_ALIAS_TO_LOGIN, username)\n .exists(userKey)\n .hgetallBuffer(userKey)\n .exec()\n .spread((aliasToUsername, exists, data) => {\n if (aliasToUsername[1]) {\n return this.getUser(aliasToUsername[1]);\n }\n\n if (!exists[1]) {\n throw new Errors.HttpStatusError(404, `\"${username}\" does not exists`);\n }\n\n return { ...data[1], username };\n });\n },\n\n /**\n * Get users metadata by username and audience\n * @param username\n * @param audience\n * @returns {Object}\n */\n\n // getMetadata(username, audience){\n // return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience));\n // },\n\n getMetadata(username, _audiences, fields = {}) {\n const audiences = Array.isArray(_audiences) ? _audiences : [_audiences];\n\n return Promise.map(audiences, audience => {\n return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience));\n })\n .then(function remapAudienceData(data) {\n const output = {};\n audiences.forEach(function transform(aud, idx) {\n const datum = data[idx];\n\n if (datum) {\n const pickFields = fields[aud];\n output[aud] = mapValues(datum, JSONParse);\n if (pickFields) {\n output[aud] = pick(output[aud], pickFields);\n }\n } else {\n output[aud] = {};\n }\n });\n\n return output;\n });\n },\n\n\n /**\n * Return the list of users by specified params\n * @param opts\n * @returns {Array}\n */\n getList(opts){\n const { criteria, audience, filter, index, strFilter, order, offset, limit } = opts;\n const metaKey = generateKey('*', USERS_METADATA, audience);\n\n return redis\n .fsort(index, metaKey, criteria, order, strFilter, offset, limit)\n .then(ids => {\n const length = +ids.pop();\n if (length === 0 || ids.length === 0) {\n return [\n ids || [],\n [],\n length,\n ];\n }\n\n const pipeline = redis.pipeline();\n ids.forEach(id => {\n pipeline.hgetallBuffer(redisKey(id, USERS_METADATA, audience));\n });\n return Promise.all([\n ids,\n pipeline.exec(),\n length,\n ]);\n })\n .spread((ids, props, length) => {\n const users = ids.map(function remapData(id, idx) {\n const data = props[idx][1];\n const account = {\n id,\n metadata: {\n [audience]: data ? mapValues(data, JSONParse) : {},\n },\n };\n\n return account;\n });\n\n return {\n users,\n cursor: offset + limit,\n page: Math.floor(offset / limit + 1),\n pages: Math.ceil(length / limit),\n };\n });\n },\n\n /**\n * Check existence of alias\n * @param data\n * @returns {boolean}\n */\n isAliasAssigned(data){\n return data[USERS_ALIAS_FIELD] !== undefined; // was just `data[USERS_ALIAS_FIELD]`\n },\n\n /**\n * Check that user is admin\n * @param meta\n * @returns {boolean}\n */\n isAdmin(meta){\n const audience = config.jwt.defaultAudience;\n return(meta[audience].roles || []).indexOf(USERS_ADMIN_ROLE) >= 0;\n },\n\n /**\n * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN\n * @param username\n * @param alias\n * @returns {Redis}\n */\n storeAlias(username, alias){\n return redis.hsetnx(USERS_ALIAS_TO_LOGIN, alias, username);\n },\n\n /**\n * Assign alias to the user record, marked by username\n * @param username\n * @param alias\n * @returns {Redis}\n */\n assignAlias(username, alias){\n return redis\n .pipeline()\n .sadd(USERS_PUBLIC_INDEX, username)\n .hset(generateKey(username, USERS_DATA), USERS_ALIAS_FIELD, alias)\n .hset(generateKey(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, stringify)\n .exec();\n },\n\n _remoteipKey: '',\n get remoteipKey(){\n return this._remoteipKey;\n },\n\n set remoteipKey(val){\n this._remoteipKey = val;\n },\n\n generateipKey(username, remoteip){\n return this._remoteipKey = generateKey(username, 'ip', remoteip);\n },\n\n _loginAttempts: 0,\n get loginAttempts(){\n return this._loginAttempts;\n },\n set loginAttempts(val){\n this._loginAttempts = val;\n },\n\n _options: {},\n get options(){\n return this._options;\n },\n\n set options(opts){\n this._options = opts;\n },\n\n dropAttempts(){\n this._loginAttempts = 0;\n return redis.del(this.key);\n },\n checkLoginAttempts(data) {\n const pipeline = redis.pipeline();\n const username = data.username;\n const remoteipKey = this.generateipKey(username, this._options.remoteip);\n\n pipeline.incrby(remoteipKey, 1);\n if (config.jwt.keepLoginAttempts > 0) {\n pipeline.expire(remoteipKey, config.jwt.keepLoginAttempts);\n }\n\n return pipeline\n .exec()\n .spread(function incremented(incrementValue) {\n const err = incrementValue[0];\n if (err) {\n this.log.error('Redis error:', err);\n return;\n }\n\n this.loginAttempts = incrementValue[1];\n if (this.loginAttempts > lockAfterAttempts) {\n const duration = moment().add(config.jwt.keepLoginAttempts, 'seconds').toNow(true);\n const msg = `You are locked from making login attempts for the next ${duration}`;\n throw new Errors.HttpStatusError(429, msg);\n }\n });\n },\n\n /**\n * Set user password\n * @param username\n * @param hash\n * @returns {Redis}\n */\n setPassword(username, hash){\n return redis\n .hset(generateKey(username, USERS_DATA), 'password', hash)\n .return(username);\n },\n\n /**\n * Reset the lock by IP\n * @param username\n * @param ip\n * @returns {Redis}\n */\n resetIPLock(username, ip){\n return redis.del(generateKey(username, 'ip', ip));\n },\n\n /**\n *\n * @param username\n * @param audience\n * @param metadata\n * @returns {Object}\n */\n updateMetadata({ username, audience, metadata, script }) {\n const audiences = is.array(audience) ? audience : [audience];\n\n // keys\n const keys = audiences.map(aud => redisKey(username, USERS_METADATA, aud));\n\n // if we have meta, then we can\n if (metadata) {\n const pipe = redis.pipeline();\n const metaOps = is.array(metadata) ? metadata : [metadata];\n const operations = metaOps.map((meta, idx) => handleAudience(pipe, keys[idx], meta));\n return pipe.exec().then(res => mapMetaResponse(operations, res));\n }\n\n //or...\n return this.customScript(script)\n },\n\n /**\n * Removing user by username (and data?)\n * @param username\n * @param data\n * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}}\n */\n removeUser(username, data){\n const audience = config.jwt.defaultAudience;\n const transaction = redis.multi();\n const alias = data[USERS_ALIAS_FIELD];\n if (alias) {\n transaction.hdel(USERS_ALIAS_TO_LOGIN, alias);\n }\n\n // clean indices\n transaction.srem(USERS_PUBLIC_INDEX, username);\n transaction.srem(USERS_INDEX, username);\n\n // remove metadata & internal data\n transaction.del(generateKey(username, USERS_DATA));\n transaction.del(generateKey(username, USERS_METADATA, audience));\n\n // remove auth tokens\n transaction.del(generateKey(username, USERS_TOKENS));\n\n // complete it\n return transaction.exec();\n },\n\n /**\n * Verify ip limits\n * @param {redisCluster} redis\n * @param {Object} registrationLimits\n * @param {String} ipaddress\n * @return {Function}\n */\n checkLimits(registrationLimits, ipaddress) {\n const {ip: {time, times}} = registrationLimits;\n const ipaddressLimitKey = generateKey('reg-limit', ipaddress);\n const now = Date.now();\n const old = now - time;\n\n return function iplimits() {\n return redis\n .pipeline()\n .zadd(ipaddressLimitKey, now, uuid.v4())\n .pexpire(ipaddressLimitKey, time)\n .zremrangebyscore(ipaddressLimitKey, '-inf', old)\n .zcard(ipaddressLimitKey)\n .exec()\n .then(props => {\n const cardinality = props[3][1];\n if (cardinality > times) {\n const msg = 'You can\\'t register more users from your ipaddress now';\n throw new Errors.HttpStatusError(429, msg);\n }\n });\n }\n },\n\n /**\n * Creates user with a given hash\n * @param redis\n * @param username\n * @param activate\n * @param deleteInactiveAccounts\n * @param userDataKey\n * @returns {Function}\n */\n createUser(username, activate, deleteInactiveAccounts) {\n /**\n * Input from scrypt.hash\n */\n const userDataKey = generateKey(username, USERS_DATA);\n\n return function create(hash) {\n const pipeline = redis.pipeline();\n\n pipeline.hsetnx(userDataKey, 'password', hash);\n pipeline.hsetnx(userDataKey, USERS_ACTIVE_FLAG, activate);\n\n return pipeline\n .exec()\n .spread(function insertedUserData(passwordSetResponse) {\n if (passwordSetResponse[1] === 0) {\n throw new Errors.HttpStatusError(412, `User \"${username}\" already exists`);\n }\n\n if (!activate && deleteInactiveAccounts >= 0) {\n // WARNING: IF USER IS NOT VERIFIED WITHIN \n // [by default 30] DAYS - IT WILL BE REMOVED FROM DATABASE\n return redis.expire(userDataKey, deleteInactiveAccounts);\n }\n\n return null;\n });\n };\n },\n\n /**\n * Performs captcha check, returns thukn\n * @param {String} username\n * @param {String} captcha\n * @param {Object} captchaConfig\n * @return {Function}\n */\n checkCaptcha(username, captcha) {\n const {secret, ttl, uri} = captchaConfig;\n return function checkCaptcha() {\n const captchaCacheKey = captcha.response;\n return redis\n .pipeline()\n .set(captchaCacheKey, username, 'EX', ttl, 'NX')\n .get(captchaCacheKey)\n .exec()\n .spread(function captchaCacheResponse(setResponse, getResponse) {\n if (getResponse[1] !== username) {\n const msg = 'Captcha challenge you\\'ve solved can not be used, please complete it again';\n throw new Errors.HttpStatusError(412, msg);\n }\n })\n .then(function verifyGoogleCaptcha() {\n return request\n .post({uri, qs: defaults(captcha, {secret}), json: true})\n .then(function captchaSuccess(body) {\n if (!body.success) {\n return Promise.reject({statusCode: 200, error: body});\n }\n\n return true;\n })\n .catch(function captchaError(err) {\n const errData = JSON.stringify(pick(err, ['statusCode', 'error']));\n throw new Errors.HttpStatusError(412, fmt('Captcha response: %s', errData));\n });\n });\n };\n },\n\n /**\n * Stores username to the index set\n * @param username\n * @returns {Redis}\n */\n storeUsername(username){\n return redis.sadd(USERS_INDEX, username);\n },\n\n /**\n * Execute custom script on LUA\n * @param script\n * @returns {Promise}\n */\n customScript(script){\n // dynamic scripts\n const $scriptKeys = Object.keys(script);\n const scripts = $scriptKeys.map(scriptName => {\n const { lua, argv = [] } = script[scriptName];\n const sha = sha256(lua);\n const name = `ms_users_${sha}`;\n if (!is.fn(redis[name])) {\n redis.defineCommand(name, { lua });\n }\n return redis[name](keys.length, keys, argv);\n });\n\n return Promise.all(scripts).then(res => {\n const output = {};\n $scriptKeys.forEach((fieldName, idx) => {\n output[fieldName] = res[idx];\n });\n return output;\n });\n },\n\n handleAudience(key, metadata) {\n const pipeline = redis.pipeline();\n const $remove = metadata.$remove;\n const $removeOps = $remove && $remove.length || 0;\n if ($removeOps > 0) {\n pipeline.hdel(key, $remove);\n }\n\n const $set = metadata.$set;\n const $setKeys = $set && Object.keys($set);\n const $setLength = $setKeys && $setKeys.length || 0;\n if ($setLength > 0) {\n pipeline.hmset(key, mapValues($set, stringify));\n }\n\n const $incr = metadata.$incr;\n const $incrFields = $incr && Object.keys($incr);\n const $incrLength = $incrFields && $incrFields.length || 0;\n if ($incrLength > 0) {\n $incrFields.forEach(fieldName => {\n pipeline.hincrby(key, fieldName, $incr[fieldName]);\n });\n }\n\n return { $removeOps, $setLength, $incrLength, $incrFields };\n }\n\n\n};\n"]} \ No newline at end of file diff --git a/src/utils/sha256-compiled-compiled-compiled-compiled.js b/src/utils/sha256-compiled-compiled-compiled-compiled.js deleted file mode 100644 index a00f10752..000000000 --- a/src/utils/sha256-compiled-compiled-compiled-compiled.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -const crypto = require('crypto'); - -/** - * Shorthand for sha256 - * @param {String} data - */ -module.exports = function digest(data) { - return crypto.createHash('sha256').update(data, 'utf8').digest(); -}; - -//# sourceMappingURL=sha256-compiled.js.map - -//# sourceMappingURL=sha256-compiled-compiled.js.map - -//# sourceMappingURL=sha256-compiled-compiled-compiled.js.map - -//# sourceMappingURL=sha256-compiled-compiled-compiled-compiled.js.map \ No newline at end of file diff --git a/src/utils/sha256-compiled-compiled-compiled-compiled.js.map b/src/utils/sha256-compiled-compiled-compiled-compiled.js.map deleted file mode 100644 index bcd578859..000000000 --- a/src/utils/sha256-compiled-compiled-compiled-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["sha256-compiled-compiled-compiled.js"],"names":[],"mappings":"AAAA;;AAEA,MAAM,SAAS,QAAQ,QAAR,CAAT;;;;;;AAMN,OAAO,OAAP,GAAiB,SAAS,MAAT,CAAgB,IAAhB,EAAsB;AACrC,SAAO,OAAO,UAAP,CAAkB,QAAlB,EAA4B,MAA5B,CAAmC,IAAnC,EAAyC,MAAzC,EAAiD,MAAjD,EAAP,CADqC;CAAtB","file":"sha256-compiled-compiled-compiled-compiled.js","sourcesContent":["'use strict';\n\nconst crypto = require('crypto');\n\n/**\r\n * Shorthand for sha256\r\n * @param {String} data\r\n */\nmodule.exports = function digest(data) {\n return crypto.createHash('sha256').update(data, 'utf8').digest();\n};\n\n//# sourceMappingURL=sha256-compiled.js.map\n\n//# sourceMappingURL=sha256-compiled-compiled.js.map\n\n//# sourceMappingURL=sha256-compiled-compiled-compiled.js.map"]} \ No newline at end of file diff --git a/src/utils/sha256-compiled-compiled-compiled.js b/src/utils/sha256-compiled-compiled-compiled.js deleted file mode 100644 index c7236bf9b..000000000 --- a/src/utils/sha256-compiled-compiled-compiled.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -const crypto = require('crypto'); - -/** - * Shorthand for sha256 - * @param {String} data - */ -module.exports = function digest(data) { - return crypto.createHash('sha256').update(data, 'utf8').digest(); -}; - -//# sourceMappingURL=sha256-compiled.js.map - -//# sourceMappingURL=sha256-compiled-compiled.js.map - -//# sourceMappingURL=sha256-compiled-compiled-compiled.js.map \ No newline at end of file diff --git a/src/utils/sha256-compiled-compiled-compiled.js.map b/src/utils/sha256-compiled-compiled-compiled.js.map deleted file mode 100644 index fcace4c7e..000000000 --- a/src/utils/sha256-compiled-compiled-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["sha256-compiled-compiled.js"],"names":[],"mappings":"AAAA;;AAEA,MAAM,SAAS,QAAQ,QAAR,CAAT;;;;;;AAMN,OAAO,OAAP,GAAiB,SAAS,MAAT,CAAgB,IAAhB,EAAsB;AACrC,SAAO,OAAO,UAAP,CAAkB,QAAlB,EAA4B,MAA5B,CAAmC,IAAnC,EAAyC,MAAzC,EAAiD,MAAjD,EAAP,CADqC;CAAtB","file":"sha256-compiled-compiled-compiled.js","sourcesContent":["'use strict';\n\nconst crypto = require('crypto');\n\n/**\r\n * Shorthand for sha256\r\n * @param {String} data\r\n */\nmodule.exports = function digest(data) {\n return crypto.createHash('sha256').update(data, 'utf8').digest();\n};\n\n//# sourceMappingURL=sha256-compiled.js.map\n\n//# sourceMappingURL=sha256-compiled-compiled.js.map"]} \ No newline at end of file diff --git a/src/utils/sha256-compiled-compiled.js b/src/utils/sha256-compiled-compiled.js deleted file mode 100644 index 13dbcc19c..000000000 --- a/src/utils/sha256-compiled-compiled.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const crypto = require('crypto'); - -/** - * Shorthand for sha256 - * @param {String} data - */ -module.exports = function digest(data) { - return crypto.createHash('sha256').update(data, 'utf8').digest(); -}; - -//# sourceMappingURL=sha256-compiled.js.map - -//# sourceMappingURL=sha256-compiled-compiled.js.map \ No newline at end of file diff --git a/src/utils/sha256-compiled-compiled.js.map b/src/utils/sha256-compiled-compiled.js.map deleted file mode 100644 index d8efc2253..000000000 --- a/src/utils/sha256-compiled-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["sha256-compiled.js"],"names":[],"mappings":"AAAA;;AAEA,MAAM,SAAS,QAAQ,QAAR,CAAT;;;;;;AAMN,OAAO,OAAP,GAAiB,SAAS,MAAT,CAAgB,IAAhB,EAAsB;AACrC,SAAO,OAAO,UAAP,CAAkB,QAAlB,EAA4B,MAA5B,CAAmC,IAAnC,EAAyC,MAAzC,EAAiD,MAAjD,EAAP,CADqC;CAAtB","file":"sha256-compiled-compiled.js","sourcesContent":["'use strict';\n\nconst crypto = require('crypto');\n\n/**\r\n * Shorthand for sha256\r\n * @param {String} data\r\n */\nmodule.exports = function digest(data) {\n return crypto.createHash('sha256').update(data, 'utf8').digest();\n};\n\n//# sourceMappingURL=sha256-compiled.js.map"]} \ No newline at end of file diff --git a/src/utils/sha256-compiled.js b/src/utils/sha256-compiled.js deleted file mode 100644 index 79a013c87..000000000 --- a/src/utils/sha256-compiled.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -const crypto = require('crypto'); - -/** - * Shorthand for sha256 - * @param {String} data - */ -module.exports = function digest(data) { - return crypto.createHash('sha256').update(data, 'utf8').digest(); -}; - -//# sourceMappingURL=sha256-compiled.js.map \ No newline at end of file diff --git a/src/utils/sha256-compiled.js.map b/src/utils/sha256-compiled.js.map deleted file mode 100644 index da263c24d..000000000 --- a/src/utils/sha256-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["sha256.js"],"names":[],"mappings":";;AAAA,MAAM,SAAS,QAAQ,QAAR,CAAT;;;;;;AAMN,OAAO,OAAP,GAAiB,SAAS,MAAT,CAAgB,IAAhB,EAAsB;AACrC,SAAO,OAAO,UAAP,CAAkB,QAAlB,EAA4B,MAA5B,CAAmC,IAAnC,EAAyC,MAAzC,EAAiD,MAAjD,EAAP,CADqC;CAAtB","file":"sha256-compiled.js","sourcesContent":["const crypto = require('crypto');\r\n\r\n/**\r\n * Shorthand for sha256\r\n * @param {String} data\r\n */\r\nmodule.exports = function digest(data) {\r\n return crypto.createHash('sha256').update(data, 'utf8').digest();\r\n};\r\n"]} \ No newline at end of file diff --git a/test/suites/updateMetadata-compiled.js b/test/suites/updateMetadata-compiled.js deleted file mode 100644 index 2764b1c35..000000000 --- a/test/suites/updateMetadata-compiled.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict'; - -/* global inspectPromise */ -const { expect } = require('chai'); - -describe('#updateMetadata', function getMetadataSuite() { - const headers = { routingKey: 'users.updateMetadata' }; - const username = 'v@makeomatic.ru'; - const audience = '*.localhost'; - const extra = 'extra.localhost'; - - beforeEach(global.startService); - afterEach(global.clearRedis); - - beforeEach(function pretest() { - return this.users.router({ username, password: '123', audience }, { routingKey: 'users.register' }); - }); - - it('must reject updating metadata on a non-existing user', function test() { - return this.users.router({ username: 'ok google', audience, metadata: { $remove: ['test'] } }, headers).reflect().then(inspectPromise(false)).then(getMetadata => { - expect(getMetadata.name).to.be.eq('HttpStatusError'); - expect(getMetadata.statusCode).to.be.eq(404); - }); - }); - - it('must be able to add metadata for a single audience of an existing user', function test() { - return this.users.router({ username, audience, metadata: { $set: { x: 10 } } }, headers).reflect().then(inspectPromise()); - }); - - it('must be able to remove metadata for a single audience of an existing user', function test() { - return this.users.router({ username, audience, metadata: { $remove: ['x'] } }, headers).reflect().then(inspectPromise()).then(data => { - expect(data.$remove).to.be.eq(0); - }); - }); - - it('rejects on mismatch of audience & metadata arrays', function test() { - return this.users.router({ - username, audience: [audience], - metadata: [{ $set: { x: 10 } }, { $remove: ['x'] }] - }, headers).reflect().then(inspectPromise(false)); - }); - - it('must be able to perform batch operations for multiple audiences of an existing user', function test() { - return this.users.router({ - username, - audience: [audience, extra], - metadata: [{ - $set: { - x: 10 - }, - $incr: { - b: 2 - } - }, { - $incr: { - b: 3 - } - }] - }, headers).reflect().then(inspectPromise()).then(data => { - const [mainData, extraData] = data; - - expect(mainData.$set).to.be.eq('OK'); - expect(mainData.$incr.b).to.be.eq(2); - expect(extraData.$incr.b).to.be.eq(3); - }); - }); - - it('must be able to run dynamic scripts', function test() { - return this.users.router({ username, audience: [audience, extra], script: { - balance: { - lua: 'return {KEYS[1],KEYS[2],ARGV[1]}', - argv: ['nom-nom'] - } - } }, headers).reflect().then(inspectPromise()).then(data => { - expect(data.balance).to.be.deep.eq([`{ms-users}v@makeomatic.ru!metadata!${ audience }`, `{ms-users}v@makeomatic.ru!metadata!${ extra }`, 'nom-nom']); - }); - }); -}); - -//# sourceMappingURL=updateMetadata-compiled.js.map \ No newline at end of file diff --git a/test/suites/updateMetadata-compiled.js.map b/test/suites/updateMetadata-compiled.js.map deleted file mode 100644 index 23817a900..000000000 --- a/test/suites/updateMetadata-compiled.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["updateMetadata.js"],"names":[],"mappings":"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","file":"updateMetadata-compiled.js","sourcesContent":["/* global inspectPromise */\r\nconst { expect } = require('chai');\r\n\r\ndescribe('#updateMetadata', function getMetadataSuite() {\r\n const headers = { routingKey: 'users.updateMetadata' };\r\n const username = 'v@makeomatic.ru';\r\n const audience = '*.localhost';\r\n const extra = 'extra.localhost';\r\n\r\n beforeEach(global.startService);\r\n afterEach(global.clearRedis);\r\n\r\n beforeEach(function pretest() {\r\n return this.users.router({ username, password: '123', audience }, { routingKey: 'users.register' });\r\n });\r\n\r\n it('must reject updating metadata on a non-existing user', function test() {\r\n return this.users.router({ username: 'ok google', audience, metadata: { $remove: ['test'] } }, headers)\r\n .reflect()\r\n .then(inspectPromise(false))\r\n .then(getMetadata => {\r\n expect(getMetadata.name).to.be.eq('HttpStatusError');\r\n expect(getMetadata.statusCode).to.be.eq(404);\r\n });\r\n });\r\n\r\n it('must be able to add metadata for a single audience of an existing user', function test() {\r\n return this.users.router({ username, audience, metadata: { $set: { x: 10 } } }, headers)\r\n .reflect()\r\n .then(inspectPromise());\r\n });\r\n\r\n it('must be able to remove metadata for a single audience of an existing user', function test() {\r\n return this.users\r\n .router({ username, audience, metadata: { $remove: ['x'] } }, headers)\r\n .reflect()\r\n .then(inspectPromise())\r\n .then(data => {\r\n expect(data.$remove).to.be.eq(0);\r\n });\r\n });\r\n\r\n it('rejects on mismatch of audience & metadata arrays', function test() {\r\n return this.users\r\n .router({\r\n username, audience: [audience],\r\n metadata: [{ $set: { x: 10 } }, { $remove: ['x'] }],\r\n }, headers)\r\n .reflect()\r\n .then(inspectPromise(false));\r\n });\r\n\r\n it('must be able to perform batch operations for multiple audiences of an existing user', function test() {\r\n return this.users\r\n .router({\r\n username,\r\n audience: [\r\n audience,\r\n extra,\r\n ],\r\n metadata: [\r\n {\r\n $set: {\r\n x: 10,\r\n },\r\n $incr: {\r\n b: 2,\r\n },\r\n },\r\n {\r\n $incr: {\r\n b: 3,\r\n },\r\n },\r\n ],\r\n }, headers)\r\n .reflect()\r\n .then(inspectPromise())\r\n .then(data => {\r\n const [mainData, extraData] = data;\r\n\r\n expect(mainData.$set).to.be.eq('OK');\r\n expect(mainData.$incr.b).to.be.eq(2);\r\n expect(extraData.$incr.b).to.be.eq(3);\r\n });\r\n });\r\n\r\n it('must be able to run dynamic scripts', function test() {\r\n return this.users.router({ username, audience: [audience, extra], script: {\r\n balance: {\r\n lua: 'return {KEYS[1],KEYS[2],ARGV[1]}',\r\n argv: ['nom-nom'],\r\n },\r\n } }, headers)\r\n .reflect()\r\n .then(inspectPromise())\r\n .then(data => {\r\n expect(data.balance).to.be.deep.eq([\r\n `{ms-users}v@makeomatic.ru!metadata!${audience}`,\r\n `{ms-users}v@makeomatic.ru!metadata!${extra}`,\r\n 'nom-nom',\r\n ]);\r\n });\r\n });\r\n});\r\n"]} \ No newline at end of file From a33438420fa610d4a5d607d30978e1c073ba0325 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Sun, 5 Jun 2016 02:00:09 +0300 Subject: [PATCH 06/38] feat: actions: login, alias, list --- src/actions/alias.js | 3 +- src/actions/list.js | 1 - src/actions/login.js | 60 +++++----------------------- src/db/adapter.js | 52 ++++++++---------------- src/db/redisstorage.js | 89 ++++++++++++++++++------------------------ 5 files changed, 65 insertions(+), 140 deletions(-) diff --git a/src/actions/alias.js b/src/actions/alias.js index be7a7b932..bcec6d756 100644 --- a/src/actions/alias.js +++ b/src/actions/alias.js @@ -1,5 +1,6 @@ const Promise = require('bluebird'); const Errors = require('common-errors'); +const { USERS_ALIAS_FIELD } = require('../constants.js'); const Users = require('../db/adapter'); @@ -13,7 +14,7 @@ module.exports = function assignAlias(opts) { .tap(Users.isActive) .tap(Users.isBanned) .then(data => { - if (Users.isAliasAssigned(data)) { + if (data[USERS_ALIAS_FIELD]) { throw new Errors.HttpStatusError(417, 'alias is already assigned'); } diff --git a/src/actions/list.js b/src/actions/list.js index d23b5a4a5..21adceee9 100644 --- a/src/actions/list.js +++ b/src/actions/list.js @@ -8,7 +8,6 @@ module.exports = function iterateOverActiveUsers(opts) { return Users.getList({ criteria, audience, - filter, index: opts.public ? USERS_PUBLIC_INDEX : USERS_INDEX, strFilter: typeof filter === 'string' ? filter : fsort.filter(filter || {}), order: opts.order || 'ASC', diff --git a/src/actions/login.js b/src/actions/login.js index c3a175dec..6fbedd2a8 100644 --- a/src/actions/login.js +++ b/src/actions/login.js @@ -1,72 +1,31 @@ const Promise = require('bluebird'); -const Errors = require('common-errors'); const scrypt = require('../utils/scrypt.js'); -const redisKey = require('../utils/key.js'); const jwt = require('../utils/jwt.js'); -const moment = require('moment'); -const isActive = require('../utils/isActive.js'); -const isBanned = require('../utils/isBanned.js'); -const getInternalData = require('../utils/getInternalData.js'); const noop = require('lodash/noop'); +const Users = require('../db/adapter'); + + module.exports = function login(opts) { const config = this.config.jwt; - const { redis } = this; const { password } = opts; const { lockAfterAttempts, defaultAudience } = config; const audience = opts.audience || defaultAudience; const remoteip = opts.remoteip || false; const verifyIp = remoteip && lockAfterAttempts > 0; - // references for data from login attempts - let remoteipKey; - let loginAttempts; - - function checkLoginAttempts(data) { - const pipeline = redis.pipeline(); - const username = data.username; - remoteipKey = redisKey(username, 'ip', remoteip); - - pipeline.incrby(remoteipKey, 1); - if (config.keepLoginAttempts > 0) { - pipeline.expire(remoteipKey, config.keepLoginAttempts); - } - - return pipeline - .exec() - .spread(function incremented(incrementValue) { - const err = incrementValue[0]; - if (err) { - this.log.error('Redis error:', err); - return; - } - - loginAttempts = incrementValue[1]; - if (loginAttempts > lockAfterAttempts) { - const duration = moment().add(config.keepLoginAttempts, 'seconds').toNow(true); - const msg = `You are locked from making login attempts for the next ${duration}`; - throw new Errors.HttpStatusError(429, msg); - } - }); - } - function verifyHash(data) { const { password: hash } = data; return scrypt.verify(hash, password); } - function dropLoginCounter() { - loginAttempts = 0; - return redis.del(remoteipKey); - } - function getUserInfo({ username }) { return jwt.login.call(this, username, audience); } function enrichError(err) { if (remoteip) { - err.loginAttempts = loginAttempts; + err.loginAttempts = Users.getAttempts(); } throw err; @@ -74,12 +33,13 @@ module.exports = function login(opts) { return Promise .bind(this, opts.username) - .then(getInternalData) - .tap(verifyIp ? checkLoginAttempts : noop) + .then(Users.getUser) + .then(data => [data, remoteip]) + .tap(verifyIp ? Users.checkLoginAttempts : noop) .tap(verifyHash) - .tap(verifyIp ? dropLoginCounter : noop) - .tap(isActive) - .tap(isBanned) + .tap(verifyIp ? Users.dropAttempts : noop) + .tap(Users.isActive) + .tap(Users.isBanned) .then(getUserInfo) .catch(verifyIp ? enrichError : e => { throw e; }); }; diff --git a/src/db/adapter.js b/src/db/adapter.js index 5a46cf4c5..f7b2937d5 100644 --- a/src/db/adapter.js +++ b/src/db/adapter.js @@ -141,16 +141,7 @@ class Users{ getList(opts){ return this.adapter.getList(opts); } - - /** - * Check existence of alias - * @param data - * @returns {boolean} - */ - isAliasAssigned(data){ - return this.adapter.isAliasAssigned(data); - } - + /** * Check that user is admin * @param meta @@ -180,38 +171,27 @@ class Users{ return this.adapter.assignAlias(username, alias); } - get remoteipKey(){ - return this.adapter.remoteipKey; - } - - set remoteipKey(val){ - this.adapter.remoteipKey = val; - } - - generateipKey(username, remoteip){ - return this.adapter.generateipKey(username, remoteip); - } - - get loginAttempts(){ - return this.adapter.loginAttempts; - } - - set loginAttempts(val){ - this.adapter.loginAttempts = val; - } - - get options(){ - return this.adapter.options; - } - - set options(opts){ - this.adapter.options = opts; + /** + * Return current login attempts count + * @returns {int} + */ + getAttempts(){ + return this.adapter.getAttempts(); } + /** + * Drop login attempts counter + * @returns {Redis} + */ dropAttempts(){ return this.adapter.dropAttempts(); } + /** + * Check login attempts + * @param data + * @returns {Redis} + */ checkLoginAttempts(data) { return this.adapter.checkLoginAttempts(data); } diff --git a/src/db/redisstorage.js b/src/db/redisstorage.js index 3147bb3fb..369f8125e 100644 --- a/src/db/redisstorage.js +++ b/src/db/redisstorage.js @@ -25,6 +25,8 @@ const { const { redis, captcha: captchaConfig, config } = this; const { jwt: { lockAfterAttempts, defaultAudience } } = config; +let remoteipKey; +let loginAttempts; /** @@ -264,7 +266,7 @@ module.exports = { * @returns {Array} */ getList(opts){ - const { criteria, audience, filter, index, strFilter, order, offset, limit } = opts; + const { criteria, audience, index, strFilter, order, offset, limit } = opts; const metaKey = generateKey('*', USERS_METADATA, audience); return redis @@ -275,7 +277,7 @@ module.exports = { return [ ids || [], [], - length, + length ]; } @@ -286,7 +288,7 @@ module.exports = { return Promise.all([ ids, pipeline.exec(), - length, + length ]); }) .spread((ids, props, length) => { @@ -295,8 +297,8 @@ module.exports = { const account = { id, metadata: { - [audience]: data ? mapValues(data, JSONParse) : {}, - }, + [audience]: data ? mapValues(data, JSONParse) : {} + } }; return account; @@ -306,20 +308,11 @@ module.exports = { users, cursor: offset + limit, page: Math.floor(offset / limit + 1), - pages: Math.ceil(length / limit), + pages: Math.ceil(length / limit) }; }); }, - /** - * Check existence of alias - * @param data - * @returns {boolean} - */ - isAliasAssigned(data){ - return data[USERS_ALIAS_FIELD] !== undefined; // was just `data[USERS_ALIAS_FIELD]` - }, - /** * Check that user is admin * @param meta @@ -355,44 +348,37 @@ module.exports = { .exec(); }, - _remoteipKey: '', - get remoteipKey(){ - return this._remoteipKey; - }, - - set remoteipKey(val){ - this._remoteipKey = val; - }, - - generateipKey(username, remoteip){ - return this._remoteipKey = generateKey(username, 'ip', remoteip); - }, - - _loginAttempts: 0, - get loginAttempts(){ - return this._loginAttempts; - }, - set loginAttempts(val){ - this._loginAttempts = val; - }, - - _options: {}, - get options(){ - return this._options; - }, - - set options(opts){ - this._options = opts; + /** + * Return current login attempts count + * @returns {int} + */ + getAttempts(){ + return loginAttempts; }, + /** + * Drop login attempts counter + * @returns {Redis} + */ dropAttempts(){ - this._loginAttempts = 0; - return redis.del(this.key); + loginAttempts = 0; + if (remoteipKey) { + return redis.del(remoteipKey); + } else { + throw new Errors.Error('Empty remote ip key'); + } }, - checkLoginAttempts(data) { + + /** + * Check login attempts + * @param data + * @param _remoteip + * @returns {Redis} + */ + checkLoginAttempts(data, _remoteip) { const pipeline = redis.pipeline(); - const username = data.username; - const remoteipKey = this.generateipKey(username, this._options.remoteip); + const { username } = data; + remoteipKey = generateKey(username, 'ip', _remoteip); pipeline.incrby(remoteipKey, 1); if (config.jwt.keepLoginAttempts > 0) { @@ -404,12 +390,11 @@ module.exports = { .spread(function incremented(incrementValue) { const err = incrementValue[0]; if (err) { - this.log.error('Redis error:', err); - return; + throw new Errors.data.RedisError(err); } - this.loginAttempts = incrementValue[1]; - if (this.loginAttempts > lockAfterAttempts) { + loginAttempts = incrementValue[1]; + if (loginAttempts > lockAfterAttempts) { const duration = moment().add(config.jwt.keepLoginAttempts, 'seconds').toNow(true); const msg = `You are locked from making login attempts for the next ${duration}`; throw new Errors.HttpStatusError(429, msg); From 98cf306ab4832f733e915143f92844d94a8fafd4 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Sun, 5 Jun 2016 17:30:17 +0300 Subject: [PATCH 07/38] feat: register action --- src/utils/mapMetaResponse.js | 3 +++ src/utils/verifyGoogleCaptcha.js | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 src/utils/mapMetaResponse.js create mode 100644 src/utils/verifyGoogleCaptcha.js diff --git a/src/utils/mapMetaResponse.js b/src/utils/mapMetaResponse.js new file mode 100644 index 000000000..f29708265 --- /dev/null +++ b/src/utils/mapMetaResponse.js @@ -0,0 +1,3 @@ +/** + * Created by Stainwoortsel on 05.06.2016. + */ diff --git a/src/utils/verifyGoogleCaptcha.js b/src/utils/verifyGoogleCaptcha.js new file mode 100644 index 000000000..f29708265 --- /dev/null +++ b/src/utils/verifyGoogleCaptcha.js @@ -0,0 +1,3 @@ +/** + * Created by Stainwoortsel on 05.06.2016. + */ From 17ba3bf28023b7290b396dd05c2ed9192aedd0a6 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Sun, 5 Jun 2016 17:32:00 +0300 Subject: [PATCH 08/38] feat: add new utils: verifyGoogleCaptcha and mapMetaResponse --- src/actions/register.js | 96 +++---------------- src/db/adapter.js | 28 +++--- src/db/redisstorage.js | 157 +++++++++++++++++-------------- src/utils/mapMetaResponse.js | 36 +++++++ src/utils/verifyGoogleCaptcha.js | 21 +++++ 5 files changed, 177 insertions(+), 161 deletions(-) diff --git a/src/actions/register.js b/src/actions/register.js index 0c53700fe..6f43df831 100644 --- a/src/actions/register.js +++ b/src/actions/register.js @@ -1,81 +1,18 @@ const Promise = require('bluebird'); const Errors = require('common-errors'); -const setMetadata = require('../utils/updateMetadata.js'); const scrypt = require('../utils/scrypt.js'); -const redisKey = require('../utils/key.js'); const emailValidation = require('../utils/send-email.js'); const jwt = require('../utils/jwt.js'); const uuid = require('node-uuid'); -const { USERS_INDEX, USERS_DATA, USERS_ACTIVE_FLAG, MAIL_REGISTER } = require('../constants.js'); +const { MAIL_REGISTER } = require('../constants.js'); + const isDisposable = require('../utils/isDisposable.js'); const mxExists = require('../utils/mxExists.js'); -const makeCaptchaCheck = require('../utils/checkCaptcha.js'); -const userExists = require('../utils/userExists.js'); const aliasExists = require('../utils/aliasExists.js'); const noop = require('lodash/noop'); const assignAlias = require('./alias.js'); -/** - * Verify ip limits - * @param {redisCluster} redis - * @param {Object} registrationLimits - * @param {String} ipaddress - * @return {Function} - */ -function checkLimits(redis, registrationLimits, ipaddress) { - const { ip: { time, times } } = registrationLimits; - const ipaddressLimitKey = redisKey('reg-limit', ipaddress); - const now = Date.now(); - const old = now - time; - - return function iplimits() { - return redis - .pipeline() - .zadd(ipaddressLimitKey, now, uuid.v4()) - .pexpire(ipaddressLimitKey, time) - .zremrangebyscore(ipaddressLimitKey, '-inf', old) - .zcard(ipaddressLimitKey) - .exec() - .then(props => { - const cardinality = props[3][1]; - if (cardinality > times) { - const msg = 'You can\'t register more users from your ipaddress now'; - throw new Errors.HttpStatusError(429, msg); - } - }); - }; -} - -/** - * Creates user with a given hash - */ -function createUser(redis, username, activate, deleteInactiveAccounts, userDataKey) { - /** - * Input from scrypt.hash - */ - return function create(hash) { - const pipeline = redis.pipeline(); - - pipeline.hsetnx(userDataKey, 'password', hash); - pipeline.hsetnx(userDataKey, USERS_ACTIVE_FLAG, activate); - - return pipeline - .exec() - .spread(function insertedUserData(passwordSetResponse) { - if (passwordSetResponse[1] === 0) { - throw new Errors.HttpStatusError(412, `User "${username}" already exists`); - } - - if (!activate && deleteInactiveAccounts >= 0) { - // WARNING: IF USER IS NOT VERIFIED WITHIN - // [by default 30] DAYS - IT WILL BE REMOVED FROM DATABASE - return redis.expire(userDataKey, deleteInactiveAccounts); - } - - return null; - }); - }; -} +const Users = require('../adapter'); /** * Registration handler @@ -83,8 +20,8 @@ function createUser(redis, username, activate, deleteInactiveAccounts, userDataK * @return {Promise} */ module.exports = function registerUser(message) { - const { redis, config } = this; - const { deleteInactiveAccounts, captcha: captchaConfig, registrationLimits } = config; + const { config } = this; + const { registrationLimits } = config; // message const { username, alias, password, audience, ipaddress, skipChallenge, activate } = message; @@ -104,7 +41,7 @@ module.exports = function registerUser(message) { // optional captcha verification if (captcha) { logger.debug('verifying captcha'); - promise = promise.tap(makeCaptchaCheck(redis, username, captcha, captchaConfig)); + promise = promise.tap(Users.checkCaptcha(username, captcha)); } if (registrationLimits) { @@ -117,20 +54,17 @@ module.exports = function registerUser(message) { } if (registrationLimits.ip && ipaddress) { - promise = promise.tap(checkLimits(redis, registrationLimits, ipaddress)); + promise = promise.tap(Users.checkLimits(ipaddress)); } } - // shared user key - const userDataKey = redisKey(username, USERS_DATA); - // step 2, verify that user _still_ does not exist promise = promise // verify user does not exist at this point - .tap(userExists) + .tap(Users.isExists) .throw(new Errors.HttpStatusError(409, `"${username}" already exists`)) .catchReturn({ statusCode: 404 }, username) - .tap(alias ? aliasExists(alias, true) : noop) + .tap(alias ? () => Users.isAliasExists(alias) : noop) // step 3 - encrypt password .then(() => { if (password) { @@ -146,7 +80,7 @@ module.exports = function registerUser(message) { }) .then(scrypt.hash) // step 4 - create user if it wasn't created by some1 else trying to use race-conditions - .then(createUser(redis, username, activate, deleteInactiveAccounts, userDataKey)) + .then(Users.createUser(username, activate)) // step 5 - save metadata if present .return({ username, @@ -154,11 +88,11 @@ module.exports = function registerUser(message) { metadata: { $set: { username, - ...metadata || {}, - }, - }, + ...metadata || {} + } + } }) - .then(setMetadata) + .then(Users.updateMetadata) .return(username); // no instant activation -> send email or skip it based on the settings @@ -171,7 +105,7 @@ module.exports = function registerUser(message) { // perform instant activation return promise // add to redis index - .then(() => redis.sadd(USERS_INDEX, username)) + .then(() => Users.storeUsername(username)) // call hook .return(['users:activate', username, audience]) .spread(this.hook) diff --git a/src/db/adapter.js b/src/db/adapter.js index f7b2937d5..195a215ba 100644 --- a/src/db/adapter.js +++ b/src/db/adapter.js @@ -141,7 +141,7 @@ class Users{ getList(opts){ return this.adapter.getList(opts); } - + /** * Check that user is admin * @param meta @@ -319,18 +319,18 @@ module.exports = function modelCreator(){ + ЭМИТТЕР НЕ НУЖЕН + МОЖНО СДЕЛАТЬ ХУКИ, но только если нужно ~ ЭРРОРЫ НАДО ВЫНЕСТИ НАРУЖУ с сообщениями, а внутри генерить женерик-эрроры с кодами, врапить их в экшне в HTTPошикби - sandbox/activate.js -> Если это модель, то оствлять ли всякие verifyToken, emailVerification и хуки снаружи? - СНАРУЖИ ++ sandbox/activate.js -> Если это модель, то оствлять ли всякие verifyToken, emailVerification и хуки снаружи? ++ СНАРУЖИ + sandbox/alias.js -> 18, 25 (запихнуть их в User?) не надо, все верно - sandbox/getMetadata -> волевым решением, логика Metadata вместе с промисами запихнута в метод getMetadata. С точки зрения абстракции всё соблюдено, но правильно ли это для текущей ситуации? - ДА, МОЖНО - но надо сделать разницу между трансформатором данных - и селектором - плюс вытянуть свежую репу - sandbox/list -> метод getList настолько широк, что поглатил в себя всю реализацию этого экшна. разве это хорошо? ++ sandbox/getMetadata -> волевым решением, логика Metadata вместе с промисами запихнута в метод getMetadata. С точки зрения абстракции всё соблюдено, но правильно ли это для текущей ситуации? ++ ДА, МОЖНО ++ но надо сделать разницу между трансформатором данных ++ и селектором ++ плюс вытянуть свежую репу ++ sandbox/list -> метод getList настолько широк, что поглатил в себя всю реализацию этого экшна. разве это хорошо? ВСЕ ОК, дефолты можно вытащить наружу. подумать над общим форматом ответа и соотв-но вытащить кое что в методы сторожа - sandbox/login -> 25, передаем options в адаптер... что-то не в порядке в королевстве Датском! - МЕНЯЕМ логику работы промисов. чтобы не городить нерабочий огород в адаптере ++ sandbox/login -> 25, передаем options в адаптер... что-то не в порядке в королевстве Датском! ++ МЕНЯЕМ логику работы промисов. чтобы не городить нерабочий огород в адаптере redisstorage -> 394 this.log. раньше this брался из экшна, к чему относится метод log? redisstorage -> 148,157 оборачивать ли эти методы? эти статусы потенциально зависят от адаптера, но должны выводить значение в чистом виде. С другой стороны, в чистом виде значение не используется, а используется промис @@ -344,5 +344,11 @@ module.exports = function modelCreator(){ ПОСМОТРЕТЬ levelDB и схожие адаптеры для работы с логикой МОЖНО в адаптере сделать трансмиттер ошибок адаптера в ошибки HTTP +------------------ + redisstorage:checkCaptcha -> имеет ли смысл verifyGoogleCaptcha вынести из сторожа в экшн? + redisstorage:updateMetadata -> будет ли где-то еще применим метод _handleAudience и mapMetaResponse + mapMetaResponse -- теоретиццки не зависит от реализации в той или иной СУБД, а просто мэппит поля + _handleAudience -- использует pipe и активно работает в базе, извеняя поля в зависимости от переданных Meta + поэтому первое реализовано как утилита, второе как внутренний метод. В теории, _handleAudience может быть совсем другим для той же РСУБД (если вообще будет) */ diff --git a/src/db/redisstorage.js b/src/db/redisstorage.js index 369f8125e..73bfdd8c0 100644 --- a/src/db/redisstorage.js +++ b/src/db/redisstorage.js @@ -14,8 +14,12 @@ const fmt = require('util').format; const is = require('is'); const sha256 = require('./sha256.js'); const moment = require('moment'); +const verifyGoogleCaptcha = require('../utils/verifyGoogleCaptcha'); +const mapMetaResponse = require('../utils/mapMetaResponse'); const stringify = JSON.stringify.bind(JSON); +const JSONParse = JSON.parse.bind(JSON); + const { USERS_DATA, USERS_METADATA, USERS_ALIAS_TO_LOGIN, USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA, @@ -24,7 +28,7 @@ const { } = require('../constants.js'); const { redis, captcha: captchaConfig, config } = this; -const { jwt: { lockAfterAttempts, defaultAudience } } = config; +const { deleteInactiveAccounts, jwt: { lockAfterAttempts, defaultAudience } } = config; let remoteipKey; let loginAttempts; @@ -106,25 +110,16 @@ module.exports = { }); }, - isAliasExists(alias, thunk){ - function resolveAlias(alias) { - return redis - .hget(USERS_ALIAS_TO_LOGIN, alias) - .then(username => { - if (username) { - throw new Errors.HttpStatusError(409, `"${alias}" already exists`); - } - - return username; - }); - } - if (thunk) { - return function resolveAliasThunk() { - return resolveAlias(alias); - }; - } + isAliasExists(alias){ + return redis + .hget(USERS_ALIAS_TO_LOGIN, alias) + .then(username => { + if (username) { + throw new Errors.HttpStatusError(409, `"${alias}" already exists`); + } - return resolveAlias(alias); + return username; + }); }, /** @@ -222,40 +217,45 @@ module.exports = { }); }, + _getMeta(username, audience){ + return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience)) + }, + _remapMeta(data, audiences, fields){ + const output = {}; + audiences.forEach(function transform(aud, idx) { + const datum = data[idx]; + + if (datum) { + const pickFields = fields[aud]; + output[aud] = mapValues(datum, JSONParse); + if (pickFields) { + output[aud] = pick(output[aud], pickFields); + } + } else { + output[aud] = {}; + } + }); + + return output; + }, + + /** * Get users metadata by username and audience * @param username * @param audience + * @param fields * @returns {Object} - */ - - // getMetadata(username, audience){ - // return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience)); - // }, - + */ getMetadata(username, _audiences, fields = {}) { const audiences = Array.isArray(_audiences) ? _audiences : [_audiences]; - return Promise.map(audiences, audience => { - return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience)); - }) - .then(function remapAudienceData(data) { - const output = {}; - audiences.forEach(function transform(aud, idx) { - const datum = data[idx]; - - if (datum) { - const pickFields = fields[aud]; - output[aud] = mapValues(datum, JSONParse); - if (pickFields) { - output[aud] = pick(output[aud], pickFields); - } - } else { - output[aud] = {}; - } - }); - - return output; + return Promise + .map(audiences, audience => { + return this._getMeta(username, audience); + }) + .then(data => { + return this._remapMeta(data, audiences, fields); }); }, @@ -424,6 +424,40 @@ module.exports = { return redis.del(generateKey(username, 'ip', ip)); }, + + /** + * Process metadata update operation for a passed audience / inner method + * @param {Object} pipeline + * @param {String} audience + * @param {Object} metadata + */ + _handleAudience(pipeline, key, metadata) { + const $remove = metadata.$remove; + const $removeOps = $remove && $remove.length || 0; + if ($removeOps > 0) { + pipeline.hdel(key, $remove); + } + + const $set = metadata.$set; + const $setKeys = $set && Object.keys($set); + const $setLength = $setKeys && $setKeys.length || 0; + if ($setLength > 0) { + pipeline.hmset(key, mapValues($set, stringify)); + } + + const $incr = metadata.$incr; + const $incrFields = $incr && Object.keys($incr); + const $incrLength = $incrFields && $incrFields.length || 0; + if ($incrLength > 0) { + $incrFields.forEach(fieldName => { + pipeline.hincrby(key, fieldName, $incr[fieldName]); + }); + } + + return { $removeOps, $setLength, $incrLength, $incrFields }; + }, + + /** * * @param username @@ -431,17 +465,17 @@ module.exports = { * @param metadata * @returns {Object} */ - updateMetadata({ username, audience, metadata, script }) { + updateMetadata({ username, audience, metadata, script }) { const audiences = is.array(audience) ? audience : [audience]; // keys - const keys = audiences.map(aud => redisKey(username, USERS_METADATA, aud)); + const keys = audiences.map(aud => generateKey(username, USERS_METADATA, aud)); // if we have meta, then we can if (metadata) { const pipe = redis.pipeline(); const metaOps = is.array(metadata) ? metadata : [metadata]; - const operations = metaOps.map((meta, idx) => handleAudience(pipe, keys[idx], meta)); + const operations = metaOps.map((meta, idx) => this._handleAudience(pipe, keys[idx], meta)); return pipe.exec().then(res => mapMetaResponse(operations, res)); } @@ -485,7 +519,8 @@ module.exports = { * @param {String} ipaddress * @return {Function} */ - checkLimits(registrationLimits, ipaddress) { + checkLimits(ipaddress) { + const { registrationLimits } = config; const {ip: {time, times}} = registrationLimits; const ipaddressLimitKey = generateKey('reg-limit', ipaddress); const now = Date.now(); @@ -514,11 +549,10 @@ module.exports = { * @param redis * @param username * @param activate - * @param deleteInactiveAccounts * @param userDataKey * @returns {Function} */ - createUser(username, activate, deleteInactiveAccounts) { + createUser(username, activate) { /** * Input from scrypt.hash */ @@ -552,12 +586,11 @@ module.exports = { * Performs captcha check, returns thukn * @param {String} username * @param {String} captcha - * @param {Object} captchaConfig * @return {Function} */ checkCaptcha(username, captcha) { - const {secret, ttl, uri} = captchaConfig; - return function checkCaptcha() { + const {ttl} = captchaConfig; + return function checkTheCaptcha() { const captchaCacheKey = captcha.response; return redis .pipeline() @@ -570,21 +603,7 @@ module.exports = { throw new Errors.HttpStatusError(412, msg); } }) - .then(function verifyGoogleCaptcha() { - return request - .post({uri, qs: defaults(captcha, {secret}), json: true}) - .then(function captchaSuccess(body) { - if (!body.success) { - return Promise.reject({statusCode: 200, error: body}); - } - - return true; - }) - .catch(function captchaError(err) { - const errData = JSON.stringify(pick(err, ['statusCode', 'error'])); - throw new Errors.HttpStatusError(412, fmt('Captcha response: %s', errData)); - }); - }); + .then(() => verifyGoogleCaptcha(captcha)); }; }, diff --git a/src/utils/mapMetaResponse.js b/src/utils/mapMetaResponse.js index f29708265..186ea5998 100644 --- a/src/utils/mapMetaResponse.js +++ b/src/utils/mapMetaResponse.js @@ -1,3 +1,39 @@ /** * Created by Stainwoortsel on 05.06.2016. */ +/** + * Is a common method for mapping updateMetadata ops + * @param operations + * @param responses + * @returns {Promise} + */ +module.exports = function mapMetaResponse(operations, responses) { + let cursor = 0; + return Promise.map(operations, props => { + const { $removeOps, $setLength, $incrLength, $incrFields } = props; + const output = {}; + + if ($removeOps > 0) { + output.$remove = responses[cursor][1]; + cursor++; + } + + if ($setLength > 0) { + output.$set = responses[cursor][1]; + cursor++; + } + + if ($incrLength > 0) { + const $incrResponse = output.$incr = {}; + $incrFields.forEach(fieldName => { + $incrResponse[fieldName] = responses[cursor][1]; + cursor++; + }); + } + + return output; + }) + .then(ops => { + return ops.length > 1 ? ops : ops[0]; + }); +}; diff --git a/src/utils/verifyGoogleCaptcha.js b/src/utils/verifyGoogleCaptcha.js index f29708265..e85270b8b 100644 --- a/src/utils/verifyGoogleCaptcha.js +++ b/src/utils/verifyGoogleCaptcha.js @@ -1,3 +1,24 @@ /** * Created by Stainwoortsel on 05.06.2016. */ +const defaults = require('lodash/defaults'); +const Errors = require('common-errors'); + +const { captcha: captchaConfig } = this; //????? is THIS available here? + +module.exports = function verifyGoogleCaptcha(captcha) { //captchaConfig + const {secret, uri} = captchaConfig; + return request + .post({uri, qs: defaults(captcha, {secret}), json: true}) + .then(function captchaSuccess(body) { + if (!body.success) { + return Promise.reject({statusCode: 200, error: body}); + } + + return true; + }) + .catch(function captchaError(err) { + const errData = JSON.stringify(pick(err, ['statusCode', 'error'])); + throw new Errors.HttpStatusError(412, fmt('Captcha response: %s', errData)); + }); +}; From e04d2c718b6d3d2ee0f39525a78edd2d04c73151 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Sun, 5 Jun 2016 17:41:31 +0300 Subject: [PATCH 09/38] feat: remove action --- src/actions/remove.js | 39 ++++++--------------------------------- src/db/adapter.js | 2 +- 2 files changed, 7 insertions(+), 34 deletions(-) diff --git a/src/actions/remove.js b/src/actions/remove.js index 004696159..767da6de4 100644 --- a/src/actions/remove.js +++ b/src/actions/remove.js @@ -1,25 +1,16 @@ const Promise = require('bluebird'); const Errors = require('common-errors'); const key = require('../utils/key'); -const getInternalData = require('../utils/getInternalData'); -const getMetadata = require('../utils/getMetadata'); -const { - USERS_INDEX, - USERS_PUBLIC_INDEX, - USERS_ALIAS_TO_LOGIN, - USERS_DATA, - USERS_METADATA, - USERS_TOKENS, - USERS_ALIAS_FIELD, - USERS_ADMIN_ROLE, -} = require('../constants'); +const { USERS_ADMIN_ROLE } = require('../constants'); + +const Users = require('../db/adapter'); module.exports = function removeUser({ username }) { const audience = this.config.jwt.defaultAudience; return Promise.props({ - internal: getInternalData.call(this, username), - meta: getMetadata.call(this, username, audience), + internal: Users.getUser(username), + meta: Users.getMetadata(username, audience) }) .then(({ internal, meta }) => { const isAdmin = (meta[audience].roles || []).indexOf(USERS_ADMIN_ROLE) >= 0; @@ -27,24 +18,6 @@ module.exports = function removeUser({ username }) { throw new Errors.HttpStatusError(400, 'can\'t remove admin user from the system'); } - const transaction = this.redis.multi(); - const alias = internal[USERS_ALIAS_FIELD]; - if (alias) { - transaction.hdel(USERS_ALIAS_TO_LOGIN, alias); - } - - // clean indices - transaction.srem(USERS_PUBLIC_INDEX, username); - transaction.srem(USERS_INDEX, username); - - // remove metadata & internal data - transaction.del(key(username, USERS_DATA)); - transaction.del(key(username, USERS_METADATA, audience)); - - // remove auth tokens - transaction.del(key(username, USERS_TOKENS)); - - // complete it - return transaction.exec(); + return Users.removeUser(username, internal); }); }; diff --git a/src/db/adapter.js b/src/db/adapter.js index 195a215ba..8b72fd5f0 100644 --- a/src/db/adapter.js +++ b/src/db/adapter.js @@ -350,5 +350,5 @@ module.exports = function modelCreator(){ mapMetaResponse -- теоретиццки не зависит от реализации в той или иной СУБД, а просто мэппит поля _handleAudience -- использует pipe и активно работает в базе, извеняя поля в зависимости от переданных Meta поэтому первое реализовано как утилита, второе как внутренний метод. В теории, _handleAudience может быть совсем другим для той же РСУБД (если вообще будет) - + redisstorage:isAdmin -> стоит ли его помещать в сторож? по идее, конечно, это может быть метод, зависимый от выборки. Но, скорее всего, он просто будет чекать поля. В actions/remove логика оставлена в самом экшне */ From e11373381dae8e86570cc3577bd1dd9b3c891d32 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Sun, 5 Jun 2016 17:47:54 +0300 Subject: [PATCH 10/38] feat: actions: requestPassword, updateMetadata --- src/actions/remove.js | 2 -- src/actions/requestPassword.js | 10 ++++------ src/actions/updateMetadata.js | 7 +++---- src/db/redisstorage.js | 6 +++--- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/actions/remove.js b/src/actions/remove.js index 767da6de4..6ecc51543 100644 --- a/src/actions/remove.js +++ b/src/actions/remove.js @@ -1,8 +1,6 @@ const Promise = require('bluebird'); const Errors = require('common-errors'); -const key = require('../utils/key'); const { USERS_ADMIN_ROLE } = require('../constants'); - const Users = require('../db/adapter'); module.exports = function removeUser({ username }) { diff --git a/src/actions/requestPassword.js b/src/actions/requestPassword.js index 0a5603ead..c47418bf6 100644 --- a/src/actions/requestPassword.js +++ b/src/actions/requestPassword.js @@ -1,8 +1,6 @@ const Promise = require('bluebird'); const emailValidation = require('../utils/send-email.js'); -const getInternalData = require('../utils/getInternalData.js'); -const isActive = require('../utils/isActive.js'); -const isBanned = require('../utils/isBanned.js'); +const Users = require('../db/adapter'); module.exports = function requestPassword(opts) { const { username, generateNewPassword } = opts; @@ -13,9 +11,9 @@ module.exports = function requestPassword(opts) { return Promise .bind(this, username) - .then(getInternalData) - .tap(isActive) - .tap(isBanned) + .then(Users.getUser) + .tap(Users.isActive) + .tap(Users.isBanned) .then(() => emailValidation.send.call(this, username, action)) .return({ success: true }); }; diff --git a/src/actions/updateMetadata.js b/src/actions/updateMetadata.js index 60ce3756f..1e791a6ed 100644 --- a/src/actions/updateMetadata.js +++ b/src/actions/updateMetadata.js @@ -1,11 +1,10 @@ const Promise = require('bluebird'); -const updateMetadata = require('../utils/updateMetadata.js'); -const userExists = require('../utils/userExists.js'); +const Users = require('../db/adapter'); module.exports = function updateMetadataAction(message) { return Promise .bind(this, message.username) - .then(userExists) + .then(Users.isExists) .then(username => ({ ...message, username })) - .then(updateMetadata); + .then(Users.updateMetadata); }; diff --git a/src/db/redisstorage.js b/src/db/redisstorage.js index 73bfdd8c0..a4041e194 100644 --- a/src/db/redisstorage.js +++ b/src/db/redisstorage.js @@ -437,7 +437,7 @@ module.exports = { if ($removeOps > 0) { pipeline.hdel(key, $remove); } - + const $set = metadata.$set; const $setKeys = $set && Object.keys($set); const $setLength = $setKeys && $setKeys.length || 0; @@ -464,8 +464,8 @@ module.exports = { * @param audience * @param metadata * @returns {Object} - */ - updateMetadata({ username, audience, metadata, script }) { + */ + updateMetadata({ username, audience, metadata, script }) { const audiences = is.array(audience) ? audience : [audience]; // keys From 0b51598a19e95d7d02b5daed729b429e202f1336 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Tue, 7 Jun 2016 01:05:11 +0300 Subject: [PATCH 11/38] feat: actions: updatePassword, verify --- src/actions/activate.js | 2 ++ src/actions/register.js | 2 +- src/actions/updatePassword.js | 26 +++++++------------------- src/actions/verify.js | 4 ++-- src/db/adapter.js | 4 ++-- src/db/redisstorage.js | 30 ++++++++++++++++++++---------- 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/actions/activate.js b/src/actions/activate.js index 136d9f066..09d5594e2 100644 --- a/src/actions/activate.js +++ b/src/actions/activate.js @@ -3,6 +3,8 @@ const emailVerification = require('../utils/send-email.js'); const jwt = require('../utils/jwt.js'); const Users = require('../db/adapter'); +Users.bind(this); + module.exports = function verifyChallenge(opts) { // TODO: add security logs // var remoteip = opts.remoteip; diff --git a/src/actions/register.js b/src/actions/register.js index 6f43df831..b1f8184b4 100644 --- a/src/actions/register.js +++ b/src/actions/register.js @@ -64,7 +64,7 @@ module.exports = function registerUser(message) { .tap(Users.isExists) .throw(new Errors.HttpStatusError(409, `"${username}" already exists`)) .catchReturn({ statusCode: 404 }, username) - .tap(alias ? () => Users.isAliasExists(alias) : noop) + .tap(alias ? () => Users.aliasAlreadyExists(alias) : noop) // step 3 - encrypt password .then(() => { if (password) { diff --git a/src/actions/updatePassword.js b/src/actions/updatePassword.js index 96e8a985e..b5aa8a9c2 100644 --- a/src/actions/updatePassword.js +++ b/src/actions/updatePassword.js @@ -1,13 +1,8 @@ const Promise = require('bluebird'); const scrypt = require('../utils/scrypt.js'); -const redisKey = require('../utils/key.js'); const jwt = require('../utils/jwt.js'); const emailChallenge = require('../utils/send-email.js'); -const getInternalData = require('../utils/getInternalData.js'); -const isActive = require('../utils/isActive.js'); -const isBanned = require('../utils/isBanned.js'); -const userExists = require('../utils/userExists.js'); -const { USERS_DATA } = require('../constants.js'); +const Users = require('../adapter'); /** * Verifies token and deletes it if it matches @@ -25,9 +20,9 @@ function tokenReset(token) { function usernamePasswordReset(username, password) { return Promise .bind(this, username) - .then(getInternalData) - .tap(isActive) - .tap(isBanned) + .then(Users.getUser) + .tap(Users.isActive) + .tap(Users.isBanned) .tap(data => scrypt.verify(data.password, password)) .return(username); } @@ -38,24 +33,17 @@ function usernamePasswordReset(username, password) { * @param {String} password */ function setPassword(_username, password) { - const { redis } = this; - return Promise .bind(this, _username) - .then(userExists) + .then(Users.isExists) .then(username => Promise.props({ username, hash: scrypt.hash(password), })) - .then(({ username, hash }) => - redis - .hset(redisKey(username, USERS_DATA), 'password', hash) - .return(username) - ); + .then(Users.setPassword); } module.exports = exports = function updatePassword(opts) { - const { redis } = this; const { newPassword: password, remoteip } = opts; const invalidateTokens = !!opts.invalidateTokens; @@ -77,7 +65,7 @@ module.exports = exports = function updatePassword(opts) { if (remoteip) { promise = promise.tap(function resetLock(username) { - return redis.del(redisKey(username, 'ip', remoteip)); + return Users.resetIPLock(username, remoteip); }); } diff --git a/src/actions/verify.js b/src/actions/verify.js index 60835aadd..87720c75e 100644 --- a/src/actions/verify.js +++ b/src/actions/verify.js @@ -1,6 +1,6 @@ const Promise = require('bluebird'); const jwt = require('../utils/jwt.js'); -const getMetadata = require('../utils/getMetadata.js'); +const Users = require('../db/adapter'); /** * Verifies that passed token is signed correctly, returns associated metadata with it @@ -25,7 +25,7 @@ module.exports = function verify(opts) { const username = decoded.username; return Promise.props({ username, - metadata: getMetadata.call(this, username, audience), + metadata: Users.getMetadata(username, audience) }); }); }; diff --git a/src/db/adapter.js b/src/db/adapter.js index 8b72fd5f0..111672563 100644 --- a/src/db/adapter.js +++ b/src/db/adapter.js @@ -71,8 +71,8 @@ class Users{ return this.adapter.isExists(username); } - isAliasExists(alias, thunk){ - return this.adapter.isAliasExists(alias, thunk); + aliasAlreadyExists(alias, thunk){ + return this.adapter.aliasAlreadyExists(alias, thunk); } /** diff --git a/src/db/redisstorage.js b/src/db/redisstorage.js index a4041e194..6fd7ea236 100644 --- a/src/db/redisstorage.js +++ b/src/db/redisstorage.js @@ -17,9 +17,11 @@ const moment = require('moment'); const verifyGoogleCaptcha = require('../utils/verifyGoogleCaptcha'); const mapMetaResponse = require('../utils/mapMetaResponse'); -const stringify = JSON.stringify.bind(JSON); +//JSON +const JSONStringify = JSON.stringify.bind(JSON); const JSONParse = JSON.parse.bind(JSON); +//constants const { USERS_DATA, USERS_METADATA, USERS_ALIAS_TO_LOGIN, USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA, @@ -27,8 +29,11 @@ const { USERS_ALIAS_FIELD } = require('../constants.js'); +//config's and base objects const { redis, captcha: captchaConfig, config } = this; const { deleteInactiveAccounts, jwt: { lockAfterAttempts, defaultAudience } } = config; + +//local vatiables inside the module let remoteipKey; let loginAttempts; @@ -50,7 +55,7 @@ module.exports = { * @param reason * @param whom * @param remoteip - * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} + * @returns {Redis|{index: number, input: string}} */ lockUser({ username, reason, whom, remoteip }){ const data = { @@ -66,7 +71,7 @@ module.exports = { .pipeline() .hset(generateKey(username, USERS_DATA), USERS_BANNED_FLAG, 'true') // set .banned on metadata for filtering & sorting users by that field - .hmset(generateKey(username, USERS_METADATA, defaultAudience), mapValues(data, stringify)) + .hmset(generateKey(username, USERS_METADATA, defaultAudience), mapValues(data, JSONStringify)) .del(generateKey(username, USERS_TOKENS)) .exec(); }, @@ -74,7 +79,7 @@ module.exports = { /** * Unlock user * @param username - * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} + * @returns {Redis|{index: number, input: string}} */ unlockUser({username}){ return redis @@ -89,7 +94,7 @@ module.exports = { /** * Check existance of user * @param username - * @returns {Redis} + * @returns {Redis|username} */ isExists(username){ return redis @@ -110,7 +115,12 @@ module.exports = { }); }, - isAliasExists(alias){ + /** + * Check the existance of alias + * @param alias + * @returns {username|''} + */ + aliasAlreadyExists(alias){ return redis .hget(USERS_ALIAS_TO_LOGIN, alias) .then(username => { @@ -344,7 +354,7 @@ module.exports = { .pipeline() .sadd(USERS_PUBLIC_INDEX, username) .hset(generateKey(username, USERS_DATA), USERS_ALIAS_FIELD, alias) - .hset(generateKey(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, stringify) + .hset(generateKey(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, JSONStringify) .exec(); }, @@ -408,7 +418,7 @@ module.exports = { * @param hash * @returns {Redis} */ - setPassword(username, hash){ + setPassword({username, hash}){ return redis .hset(generateKey(username, USERS_DATA), 'password', hash) .return(username); @@ -442,7 +452,7 @@ module.exports = { const $setKeys = $set && Object.keys($set); const $setLength = $setKeys && $setKeys.length || 0; if ($setLength > 0) { - pipeline.hmset(key, mapValues($set, stringify)); + pipeline.hmset(key, mapValues($set, JSONStringify)); } const $incr = metadata.$incr; @@ -655,7 +665,7 @@ module.exports = { const $setKeys = $set && Object.keys($set); const $setLength = $setKeys && $setKeys.length || 0; if ($setLength > 0) { - pipeline.hmset(key, mapValues($set, stringify)); + pipeline.hmset(key, mapValues($set, JSONStringify)); } const $incr = metadata.$incr; From 55c47afed9047440b0e17f3bbd410ff82a500c2f Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Wed, 8 Jun 2016 04:36:28 +0300 Subject: [PATCH 12/38] style: actions, adapter, redisstorage, utils, docker.sh code was linted, docker script was adapted to MS Windows --- package.json | 2 +- src/actions/ban.js | 6 +- src/actions/list.js | 5 +- src/actions/register.js | 13 ++- src/actions/remove.js | 2 +- src/actions/updatePassword.js | 2 +- src/actions/verify.js | 2 +- src/db/adapter.js | 133 +++++++------------------------ src/db/redisstorage.js | 113 ++++++++++++-------------- src/users.js | 3 +- src/utils/getInternalData.js | 1 - src/utils/verifyGoogleCaptcha.js | 14 ++-- test/docker.sh | 12 +-- 13 files changed, 110 insertions(+), 198 deletions(-) diff --git a/package.json b/package.json index e9a87b14a..c6a945e57 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "compile": "rm -rf ./lib; babel -d ./lib ./src", "prepublish": "npm run lint && npm run compile", - "test": "npm run lint && ./test/docker.sh", + "test": "npm run lint && bash ./test/docker.sh", "start": "node ./bin/mservice.js | bunyan -o short", "semantic-release": "semantic-release pre && npm publish && semantic-release post", "docker-release": "./docker-release.sh", diff --git a/src/actions/ban.js b/src/actions/ban.js index 6d4a41783..d73b7d232 100644 --- a/src/actions/ban.js +++ b/src/actions/ban.js @@ -6,12 +6,12 @@ function lockUser({ username, reason, whom, remoteip }) { username, reason: reason || '', whom: whom || '', - remoteip: remoteip || '' - }) + remoteip: remoteip || '', + }); } function unlockUser({ username }) { - return Users.unlockUser({username}); + return Users.unlockUser({ username }); } /** diff --git a/src/actions/list.js b/src/actions/list.js index 21adceee9..4bc3b61fb 100644 --- a/src/actions/list.js +++ b/src/actions/list.js @@ -1,4 +1,4 @@ -const Users = require('../adapter'); +const Users = require('../db/adapter'); const fsort = require('redis-filtered-sort'); const { USERS_INDEX, USERS_PUBLIC_INDEX } = require('../constants.js'); @@ -12,7 +12,6 @@ module.exports = function iterateOverActiveUsers(opts) { strFilter: typeof filter === 'string' ? filter : fsort.filter(filter || {}), order: opts.order || 'ASC', offset: opts.offset || 0, - limit: opts.limit || 10 + limit: opts.limit || 10, }); - }; diff --git a/src/actions/register.js b/src/actions/register.js index b1f8184b4..92cba4b12 100644 --- a/src/actions/register.js +++ b/src/actions/register.js @@ -3,16 +3,13 @@ const Errors = require('common-errors'); const scrypt = require('../utils/scrypt.js'); const emailValidation = require('../utils/send-email.js'); const jwt = require('../utils/jwt.js'); -const uuid = require('node-uuid'); const { MAIL_REGISTER } = require('../constants.js'); - const isDisposable = require('../utils/isDisposable.js'); const mxExists = require('../utils/mxExists.js'); -const aliasExists = require('../utils/aliasExists.js'); const noop = require('lodash/noop'); const assignAlias = require('./alias.js'); -const Users = require('../adapter'); +const Users = require('../db/adapter'); /** * Registration handler @@ -20,7 +17,7 @@ const Users = require('../adapter'); * @return {Promise} */ module.exports = function registerUser(message) { - const { config } = this; + const { config } = this; const { registrationLimits } = config; // message @@ -88,9 +85,9 @@ module.exports = function registerUser(message) { metadata: { $set: { username, - ...metadata || {} - } - } + ...metadata || {}, + }, + }, }) .then(Users.updateMetadata) .return(username); diff --git a/src/actions/remove.js b/src/actions/remove.js index 6ecc51543..32585e1d8 100644 --- a/src/actions/remove.js +++ b/src/actions/remove.js @@ -8,7 +8,7 @@ module.exports = function removeUser({ username }) { return Promise.props({ internal: Users.getUser(username), - meta: Users.getMetadata(username, audience) + meta: Users.getMetadata(username, audience), }) .then(({ internal, meta }) => { const isAdmin = (meta[audience].roles || []).indexOf(USERS_ADMIN_ROLE) >= 0; diff --git a/src/actions/updatePassword.js b/src/actions/updatePassword.js index b5aa8a9c2..c6deb7ee5 100644 --- a/src/actions/updatePassword.js +++ b/src/actions/updatePassword.js @@ -2,7 +2,7 @@ const Promise = require('bluebird'); const scrypt = require('../utils/scrypt.js'); const jwt = require('../utils/jwt.js'); const emailChallenge = require('../utils/send-email.js'); -const Users = require('../adapter'); +const Users = require('../db/adapter'); /** * Verifies token and deletes it if it matches diff --git a/src/actions/verify.js b/src/actions/verify.js index 87720c75e..0487edc59 100644 --- a/src/actions/verify.js +++ b/src/actions/verify.js @@ -25,7 +25,7 @@ module.exports = function verify(opts) { const username = decoded.username; return Promise.props({ username, - metadata: Users.getMetadata(username, audience) + metadata: Users.getMetadata(username, audience), }); }); }; diff --git a/src/db/adapter.js b/src/db/adapter.js index 111672563..431e2251c 100644 --- a/src/db/adapter.js +++ b/src/db/adapter.js @@ -4,43 +4,11 @@ const RedisStorage = require('./redisstorage'); const Errors = require('common-errors'); -class Users{ - constructor(adapter){ - +class Users { + constructor(adapter) { this.adapter = adapter; - -/* - let opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - // init configuration - const config = this._config = _extends({}, defaultOpts, opts); - - // setup hooks - forOwn(config.hooks, (_hooks, eventName) => { - const hooks = Array.isArray(_hooks) ? _hooks : [_hooks]; - each(hooks, hook => this.on(eventName, hook)); - }); -*/ - - } - - /** - * Initialize connection - * @return {Promise} - */ - connect(){ - // ???? } - /** - * Close connection - * return {Promise} - */ - close(){ - // ???? - } - - /** * Lock user * @param username @@ -49,7 +17,7 @@ class Users{ * @param remoteip * @returns {Redis} */ - lockUser({ username, reason, whom, remoteip }){ + lockUser({ username, reason, whom, remoteip }) { return this.adapter.lockUser({ username, reason, whom, remoteip }); } @@ -58,7 +26,7 @@ class Users{ * @param username * @returns {Redis} */ - unlockUser(username){ + unlockUser(username) { return this.adapter.unlockUser(username); } @@ -67,11 +35,11 @@ class Users{ * @param username * @returns {Redis} */ - isExists(username){ + isExists(username) { return this.adapter.isExists(username); } - aliasAlreadyExists(alias, thunk){ + aliasAlreadyExists(alias, thunk) { return this.adapter.aliasAlreadyExists(alias, thunk); } @@ -90,7 +58,7 @@ class Users{ * @param data * @returns {boolean} */ - isActive(data){ + isActive(data) { return this.adapter.isActive(data); } @@ -99,7 +67,7 @@ class Users{ * @param data * @returns {Promise} */ - isBanned(data){ + isBanned(data) { return this.adapter.isBanned(data); } @@ -108,7 +76,7 @@ class Users{ * @param user * @returns {Redis} */ - activateAccount(user){ + activateAccount(user) { return this.adapter.activateAccount(user); } @@ -117,7 +85,7 @@ class Users{ * @param username * @returns {Object} */ - getUser(username){ + getUser(username) { return this.adapter.getUser(username); } @@ -138,7 +106,7 @@ class Users{ * @param opts * @returns {Array} */ - getList(opts){ + getList(opts) { return this.adapter.getList(opts); } @@ -147,7 +115,7 @@ class Users{ * @param meta * @returns {boolean} */ - isAdmin(meta){ + isAdmin(meta) { return this.adapter.isAdmin(meta); } @@ -157,7 +125,7 @@ class Users{ * @param alias * @returns {Redis} */ - storeAlias(username, alias){ + storeAlias(username, alias) { return this.adapter.storeAlias(username, alias); } @@ -167,7 +135,7 @@ class Users{ * @param alias * @returns {Redis} */ - assignAlias(username, alias){ + assignAlias(username, alias) { return this.adapter.assignAlias(username, alias); } @@ -175,7 +143,7 @@ class Users{ * Return current login attempts count * @returns {int} */ - getAttempts(){ + getAttempts() { return this.adapter.getAttempts(); } @@ -183,7 +151,7 @@ class Users{ * Drop login attempts counter * @returns {Redis} */ - dropAttempts(){ + dropAttempts() { return this.adapter.dropAttempts(); } @@ -202,7 +170,7 @@ class Users{ * @param hash * @returns {Redis} */ - setPassword(username, hash){ + setPassword(username, hash) { return this.adapter.setPassword(username, hash); } @@ -212,7 +180,7 @@ class Users{ * @param ip * @returns {Redis} */ - resetIPLock(username, ip){ + resetIPLock(username, ip) { return this.adapter.resetIPLock(username, ip); } @@ -223,8 +191,8 @@ class Users{ * @param metadata * @returns {Object} */ - updateMetadata({username, audience, metadata}) { - return this.adapter.updateMetadata({username, audience, metadata}); + updateMetadata({ username, audience, metadata }) { + return this.adapter.updateMetadata({ username, audience, metadata }); } /** @@ -233,7 +201,7 @@ class Users{ * @param data * @returns {Redis} */ - removeUser(username, data){ + removeUser(username, data) { return this.adapter.removeUser(username, data); } @@ -277,7 +245,7 @@ class Users{ * @param username * @returns {Redis} */ - storeUsername(username){ + storeUsername(username) { return this.adapter.storeUsername(username); } @@ -287,7 +255,7 @@ class Users{ * @returns {*|Promise} */ - customScript(script){ + customScript(script) { return this.adapter.customScript(script); } @@ -295,60 +263,15 @@ class Users{ * The error wrapper for the front-level HTTP output * @param e */ - static mapErrors(e){ - const err = new Errors.HttpStatusError(e.status_code || 500 , e.message); - if(err.status_code >= 500) { - err.message = Errors.HttpStatusError.message_map[500]; //hide the real error from the user + static mapErrors(e) { + const err = new Errors.HttpStatusError(e.status_code || 500, e.message); + if (err.status_code >= 500) { + err.message = Errors.HttpStatusError.message_map[500]; // hide the real error from the user } } } -module.exports = function modelCreator(){ +module.exports = function modelCreator() { return new Users(RedisStorage); }; - - -/* - ВОПРОСЫ: - Не превращается ли адаптер в полноценную модель? - Что делать с промисами? Правильно ли частично их пихать в адаптер (по идее, соединение -- ресурс, так что да)? - Архитектура MServices, где берется redis? - Оставить Errors снаружи? - -+ ЭМИТТЕР НЕ НУЖЕН -+ МОЖНО СДЕЛАТЬ ХУКИ, но только если нужно -~ ЭРРОРЫ НАДО ВЫНЕСТИ НАРУЖУ с сообщениями, а внутри генерить женерик-эрроры с кодами, врапить их в экшне в HTTPошикби -+ sandbox/activate.js -> Если это модель, то оствлять ли всякие verifyToken, emailVerification и хуки снаружи? -+ СНАРУЖИ -+ sandbox/alias.js -> 18, 25 (запихнуть их в User?) не надо, все верно -+ sandbox/getMetadata -> волевым решением, логика Metadata вместе с промисами запихнута в метод getMetadata. С точки зрения абстракции всё соблюдено, но правильно ли это для текущей ситуации? -+ ДА, МОЖНО -+ но надо сделать разницу между трансформатором данных -+ и селектором -+ плюс вытянуть свежую репу -+ sandbox/list -> метод getList настолько широк, что поглатил в себя всю реализацию этого экшна. разве это хорошо? - ВСЕ ОК, дефолты можно вытащить наружу. подумать над общим форматом ответа и соотв-но вытащить кое что в методы сторожа -+ sandbox/login -> 25, передаем options в адаптер... что-то не в порядке в королевстве Датском! -+ МЕНЯЕМ логику работы промисов. чтобы не городить нерабочий огород в адаптере - - redisstorage -> 394 this.log. раньше this брался из экшна, к чему относится метод log? - redisstorage -> 148,157 оборачивать ли эти методы? эти статусы потенциально зависят от адаптера, но должны выводить значение в чистом виде. С другой стороны, в чистом виде значение не используется, а используется промис - ЛОГИЧНЕЕ будет сформить метод с промисом и кидаться ошибками на верхний уровень, собстна в sql логика будет та же - нафига нужны просто флаги -- не понятно - redisstorage -> 105 что на счет методов с thunk'ом? - ПОСМОТРЕТЬ что делает thunk и где он участвует, вожможно придется оставить - - bluebird: tap - - ПОСМОТРЕТЬ levelDB и схожие адаптеры для работы с логикой - - МОЖНО в адаптере сделать трансмиттер ошибок адаптера в ошибки HTTP ------------------- - redisstorage:checkCaptcha -> имеет ли смысл verifyGoogleCaptcha вынести из сторожа в экшн? - redisstorage:updateMetadata -> будет ли где-то еще применим метод _handleAudience и mapMetaResponse - mapMetaResponse -- теоретиццки не зависит от реализации в той или иной СУБД, а просто мэппит поля - _handleAudience -- использует pipe и активно работает в базе, извеняя поля в зависимости от переданных Meta - поэтому первое реализовано как утилита, второе как внутренний метод. В теории, _handleAudience может быть совсем другим для той же РСУБД (если вообще будет) - redisstorage:isAdmin -> стоит ли его помещать в сторож? по идее, конечно, это может быть метод, зависимый от выборки. Но, скорее всего, он просто будет чекать поля. В actions/remove логика оставлена в самом экшне - */ diff --git a/src/db/redisstorage.js b/src/db/redisstorage.js index 6fd7ea236..2feaec34d 100644 --- a/src/db/redisstorage.js +++ b/src/db/redisstorage.js @@ -4,36 +4,32 @@ const Promise = require('bluebird'); const Errors = require('common-errors'); const mapValues = require('lodash/mapValues'); -const defaults = require('lodash/defaults'); const get = require('lodash/get'); const pick = require('lodash/pick'); -const request = require('request-promise'); const uuid = require('node-uuid'); -const fsort = require('redis-filtered-sort'); -const fmt = require('util').format; const is = require('is'); -const sha256 = require('./sha256.js'); +const sha256 = require('../utils/sha256.js'); const moment = require('moment'); const verifyGoogleCaptcha = require('../utils/verifyGoogleCaptcha'); const mapMetaResponse = require('../utils/mapMetaResponse'); -//JSON +// JSON const JSONStringify = JSON.stringify.bind(JSON); const JSONParse = JSON.parse.bind(JSON); -//constants +// constants const { USERS_DATA, USERS_METADATA, USERS_ALIAS_TO_LOGIN, USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA, USERS_ACTIVE_FLAG, USERS_INDEX, USERS_PUBLIC_INDEX, - USERS_ALIAS_FIELD + USERS_ALIAS_FIELD, USERS_ADMIN_ROLE, } = require('../constants.js'); -//config's and base objects +// config's and base objects const { redis, captcha: captchaConfig, config } = this; const { deleteInactiveAccounts, jwt: { lockAfterAttempts, defaultAudience } } = config; -//local vatiables inside the module +// local vatiables inside the module let remoteipKey; let loginAttempts; @@ -57,14 +53,14 @@ module.exports = { * @param remoteip * @returns {Redis|{index: number, input: string}} */ - lockUser({ username, reason, whom, remoteip }){ + lockUser({ username, reason, whom, remoteip }) { const data = { banned: true, [USERS_BANNED_DATA]: { reason, whom, - remoteip - } + remoteip, + }, }; return redis @@ -81,14 +77,13 @@ module.exports = { * @param username * @returns {Redis|{index: number, input: string}} */ - unlockUser({username}){ + unlockUser({ username }) { return redis .pipeline() .hdel(generateKey(username, USERS_DATA), USERS_BANNED_FLAG) // remove .banned on metadata for filtering & sorting users by that field .hdel(generateKey(username, USERS_METADATA, defaultAudience), 'banned', USERS_BANNED_DATA) .exec(); - }, /** @@ -96,7 +91,7 @@ module.exports = { * @param username * @returns {Redis|username} */ - isExists(username){ + isExists(username) { return redis .pipeline() .hget(USERS_ALIAS_TO_LOGIN, username) @@ -120,7 +115,7 @@ module.exports = { * @param alias * @returns {username|''} */ - aliasAlreadyExists(alias){ + aliasAlreadyExists(alias) { return redis .hget(USERS_ALIAS_TO_LOGIN, alias) .then(username => { @@ -154,7 +149,7 @@ module.exports = { * @param data * @returns {Promise} */ - isActive(data){ + isActive(data) { if (String(data[USERS_ACTIVE_FLAG]) !== 'true') { return Promise.reject(new Errors.HttpStatusError(412, 'Account hasn\'t been activated')); } @@ -167,8 +162,8 @@ module.exports = { * @param data * @returns {Promise} */ - isBanned(data){ - if(String(data[USERS_BANNED_FLAG]) === 'true') { + isBanned(data) { + if (String(data[USERS_BANNED_FLAG]) === 'true') { return Promise.reject(new Errors.HttpStatusError(423, 'Account has been locked')); } @@ -180,7 +175,7 @@ module.exports = { * @param user * @returns {Redis} */ - activateAccount(user){ + activateAccount(user) { const userKey = generateKey(user, USERS_DATA); // WARNING: `persist` is very important, otherwise we will lose user's information in 30 days @@ -205,7 +200,7 @@ module.exports = { * @param username * @returns {Object} */ - getUser(username){ + getUser(username) { const userKey = generateKey(username, USERS_DATA); return redis @@ -216,7 +211,7 @@ module.exports = { .exec() .spread((aliasToUsername, exists, data) => { if (aliasToUsername[1]) { - return this.getUser(aliasToUsername[1]); + return this.getUser(aliasToUsername[1]); } if (!exists[1]) { @@ -227,10 +222,10 @@ module.exports = { }); }, - _getMeta(username, audience){ - return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience)) + _getMeta(username, audience) { + return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience)); }, - _remapMeta(data, audiences, fields){ + _remapMeta(data, audiences, fields) { const output = {}; audiences.forEach(function transform(aud, idx) { const datum = data[idx]; @@ -275,7 +270,7 @@ module.exports = { * @param opts * @returns {Array} */ - getList(opts){ + getList(opts) { const { criteria, audience, index, strFilter, order, offset, limit } = opts; const metaKey = generateKey('*', USERS_METADATA, audience); @@ -287,18 +282,18 @@ module.exports = { return [ ids || [], [], - length + length, ]; } const pipeline = redis.pipeline(); ids.forEach(id => { - pipeline.hgetallBuffer(redisKey(id, USERS_METADATA, audience)); + pipeline.hgetallBuffer(generateKey(id, USERS_METADATA, audience)); }); return Promise.all([ ids, pipeline.exec(), - length + length, ]); }) .spread((ids, props, length) => { @@ -307,8 +302,8 @@ module.exports = { const account = { id, metadata: { - [audience]: data ? mapValues(data, JSONParse) : {} - } + [audience]: data ? mapValues(data, JSONParse) : {}, + }, }; return account; @@ -318,7 +313,7 @@ module.exports = { users, cursor: offset + limit, page: Math.floor(offset / limit + 1), - pages: Math.ceil(length / limit) + pages: Math.ceil(length / limit), }; }); }, @@ -328,9 +323,9 @@ module.exports = { * @param meta * @returns {boolean} */ - isAdmin(meta){ + isAdmin(meta) { const audience = config.jwt.defaultAudience; - return(meta[audience].roles || []).indexOf(USERS_ADMIN_ROLE) >= 0; + return (meta[audience].roles || []).indexOf(USERS_ADMIN_ROLE) >= 0; }, /** @@ -339,7 +334,7 @@ module.exports = { * @param alias * @returns {Redis} */ - storeAlias(username, alias){ + storeAlias(username, alias) { return redis.hsetnx(USERS_ALIAS_TO_LOGIN, alias, username); }, @@ -349,7 +344,7 @@ module.exports = { * @param alias * @returns {Redis} */ - assignAlias(username, alias){ + assignAlias(username, alias) { return redis .pipeline() .sadd(USERS_PUBLIC_INDEX, username) @@ -362,7 +357,7 @@ module.exports = { * Return current login attempts count * @returns {int} */ - getAttempts(){ + getAttempts() { return loginAttempts; }, @@ -370,13 +365,13 @@ module.exports = { * Drop login attempts counter * @returns {Redis} */ - dropAttempts(){ + dropAttempts() { loginAttempts = 0; if (remoteipKey) { return redis.del(remoteipKey); - } else { - throw new Errors.Error('Empty remote ip key'); } + + throw new Errors.Error('Empty remote ip key'); }, /** @@ -418,7 +413,7 @@ module.exports = { * @param hash * @returns {Redis} */ - setPassword({username, hash}){ + setPassword({ username, hash }) { return redis .hset(generateKey(username, USERS_DATA), 'password', hash) .return(username); @@ -430,7 +425,7 @@ module.exports = { * @param ip * @returns {Redis} */ - resetIPLock(username, ip){ + resetIPLock(username, ip) { return redis.del(generateKey(username, 'ip', ip)); }, @@ -489,8 +484,8 @@ module.exports = { return pipe.exec().then(res => mapMetaResponse(operations, res)); } - //or... - return this.customScript(script) + // or... + return this.customScript(script, keys); }, /** @@ -498,8 +493,8 @@ module.exports = { * @param username * @param data * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} - */ - removeUser(username, data){ + */ + removeUser(username, data) { const audience = config.jwt.defaultAudience; const transaction = redis.multi(); const alias = data[USERS_ALIAS_FIELD]; @@ -531,7 +526,7 @@ module.exports = { */ checkLimits(ipaddress) { const { registrationLimits } = config; - const {ip: {time, times}} = registrationLimits; + const { ip: { time, times } } = registrationLimits; const ipaddressLimitKey = generateKey('reg-limit', ipaddress); const now = Date.now(); const old = now - time; @@ -551,7 +546,7 @@ module.exports = { throw new Errors.HttpStatusError(429, msg); } }); - } + }; }, /** @@ -599,7 +594,7 @@ module.exports = { * @return {Function} */ checkCaptcha(username, captcha) { - const {ttl} = captchaConfig; + const { ttl } = captchaConfig; return function checkTheCaptcha() { const captchaCacheKey = captcha.response; return redis @@ -622,7 +617,7 @@ module.exports = { * @param username * @returns {Redis} */ - storeUsername(username){ + storeUsername(username) { return redis.sadd(USERS_INDEX, username); }, @@ -631,7 +626,7 @@ module.exports = { * @param script * @returns {Promise} */ - customScript(script){ + customScript(script, keys) { // dynamic scripts const $scriptKeys = Object.keys(script); const scripts = $scriptKeys.map(scriptName => { @@ -645,11 +640,11 @@ module.exports = { }); return Promise.all(scripts).then(res => { - const output = {}; - $scriptKeys.forEach((fieldName, idx) => { - output[fieldName] = res[idx]; - }); - return output; + const output = {}; + $scriptKeys.forEach((fieldName, idx) => { + output[fieldName] = res[idx]; + }); + return output; }); }, @@ -678,7 +673,5 @@ module.exports = { } return { $removeOps, $setLength, $incrLength, $incrFields }; - } - - + }, }; diff --git a/src/users.js b/src/users.js index aaf23366f..665e7071f 100644 --- a/src/users.js +++ b/src/users.js @@ -31,7 +31,7 @@ module.exports = class Users extends Mservice { this.log.fatal('Invalid configuration:', error.toJSON()); throw error; } -//повесить логику выбора адаптера можно повесить сюда, экшн долежн выполниться позже, можно лочить принятие сообщений внутри роута +// повесить логику выбора адаптера можно повесить сюда, экшн долежн выполниться позже, можно лочить принятие сообщений внутри роута this.on('plugin:connect:amqp', (amqp) => { this._mailer = new Mailer(amqp, config.mailer); }); @@ -60,7 +60,6 @@ module.exports = class Users extends Mservice { */ get config() { return this._config; - } /** diff --git a/src/utils/getInternalData.js b/src/utils/getInternalData.js index 4a50c3d6a..9e82c93c2 100644 --- a/src/utils/getInternalData.js +++ b/src/utils/getInternalData.js @@ -5,7 +5,6 @@ const { USERS_DATA, USERS_ALIAS_TO_LOGIN } = require('../constants.js'); module.exports = function getInternalData(username) { const { redis } = this; const userKey = redisKey(username, USERS_DATA); - return redis .pipeline() .hget(USERS_ALIAS_TO_LOGIN, username) diff --git a/src/utils/verifyGoogleCaptcha.js b/src/utils/verifyGoogleCaptcha.js index e85270b8b..b287b6f45 100644 --- a/src/utils/verifyGoogleCaptcha.js +++ b/src/utils/verifyGoogleCaptcha.js @@ -3,16 +3,18 @@ */ const defaults = require('lodash/defaults'); const Errors = require('common-errors'); +const request = require('request-promise'); +const pick = require('lodash/pick'); +const fmt = require('util').format; +const { captcha: captchaConfig } = this; // ????? is THIS available here? -const { captcha: captchaConfig } = this; //????? is THIS available here? - -module.exports = function verifyGoogleCaptcha(captcha) { //captchaConfig - const {secret, uri} = captchaConfig; +module.exports = function verifyGoogleCaptcha(captcha) { // captchaConfig + const { secret, uri } = captchaConfig; return request - .post({uri, qs: defaults(captcha, {secret}), json: true}) + .post({ uri, qs: defaults(captcha, { secret }), json: true }) .then(function captchaSuccess(body) { if (!body.success) { - return Promise.reject({statusCode: 200, error: body}); + return Promise.reject({ statusCode: 200, error: body }); } return true; diff --git a/test/docker.sh b/test/docker.sh index 891a3a7d6..7eb317d69 100755 --- a/test/docker.sh +++ b/test/docker.sh @@ -23,17 +23,17 @@ if ! [ -x "$COMPOSE" ]; then fi function finish { - $COMPOSE -f $DC stop - $COMPOSE -f $DC rm -f + "$COMPOSE" -f $DC stop + "$COMPOSE" -f $DC rm -f } trap finish EXIT export IMAGE=makeomatic/alpine-node:$NODE_VER -$COMPOSE -f $DC up -d +"$COMPOSE" -f $DC up -d if [[ "$SKIP_REBUILD" != "1" ]]; then echo "rebuilding native dependencies..." - $COMPOSE -f $DC run --rm tester npm rebuild + "$COMPOSE" -f $DC run --rm tester npm rebuild fi echo "cleaning old coverage" @@ -41,11 +41,11 @@ rm -rf ./coverage echo "running tests" for fn in $TESTS; do - $COMPOSE -f $DC run --rm tester /bin/sh -c "$NODE $COVER --dir ./coverage/${fn##*/} $MOCHA -- $fn" || exit 1 + "$COMPOSE" -f $DC run --rm tester /bin/sh -c "$NODE $COVER --dir ./coverage/${fn##*/} $MOCHA -- $fn" || exit 1 done echo "started generating combined coverage" -$COMPOSE -f $DC run --rm tester node ./test/aggregate-report.js +"$COMPOSE" -f $DC run --rm tester node ./test/aggregate-report.js echo "uploading coverage report from ./coverage/lcov.info" if [[ "$CI" == "true" ]]; then From b18ac68bc28757057e34e46ffd373dd77f171e27 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Sat, 18 Jun 2016 05:59:59 +0300 Subject: [PATCH 13/38] feat: new UserModer class abstraction, redisstorage, new modelError module, new remapMeta util, upda --- src/actions/activate.js | 12 +- src/actions/alias.js | 30 +- src/actions/ban.js | 23 +- src/actions/challenge.js | 14 +- src/actions/getInternalData.js | 9 +- src/actions/getMetadata.js | 12 +- src/actions/list.js | 22 +- src/db/adapter.js | 30 ++ src/db/mysqlstorage.js | 442 +++++++++++++++++++++++++++++ src/db/redisstorage.js | 48 +++- src/model/modelError.js | 132 +++++++++ src/model/storages/redisstorage.js | 337 ++++++++++++++++++++++ src/model/usermodel.js | 151 ++++++++++ src/utils/isActive.js | 4 +- src/utils/isBanned.js | 5 +- src/utils/jwt.js | 32 +-- src/utils/remapMeta.js | 25 ++ test/.bin/docker-compose | 0 18 files changed, 1227 insertions(+), 101 deletions(-) create mode 100644 src/db/mysqlstorage.js create mode 100644 src/model/modelError.js create mode 100644 src/model/storages/redisstorage.js create mode 100644 src/model/usermodel.js create mode 100644 src/utils/remapMeta.js create mode 100644 test/.bin/docker-compose diff --git a/src/actions/activate.js b/src/actions/activate.js index 09d5594e2..a5523c0a5 100644 --- a/src/actions/activate.js +++ b/src/actions/activate.js @@ -1,9 +1,8 @@ const Promise = require('bluebird'); const emailVerification = require('../utils/send-email.js'); const jwt = require('../utils/jwt.js'); -const Users = require('../db/adapter'); - -Users.bind(this); +const { User } = require('../model/usermodel'); +const { ModelError } = require('../model/modelError'); module.exports = function verifyChallenge(opts) { // TODO: add security logs @@ -22,9 +21,10 @@ module.exports = function verifyChallenge(opts) { return Promise .bind(this, username) - .then(username ? Users.isExists : verifyToken) - .tap(Users.activateAccount) + .then(username ? User.getUsername : verifyToken) + .tap(User.activate) .tap(hook) .then(user => [user, audience]) - .spread(jwt.login); + .spread(jwt.login) + .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); }; diff --git a/src/actions/alias.js b/src/actions/alias.js index bcec6d756..25a2bc160 100644 --- a/src/actions/alias.js +++ b/src/actions/alias.js @@ -1,8 +1,9 @@ const Promise = require('bluebird'); -const Errors = require('common-errors'); -const { USERS_ALIAS_FIELD } = require('../constants.js'); +const isActive = require('../utils/isActive'); +const isBanned = require('../utils/isBanned'); -const Users = require('../db/adapter'); +const { User } = require('../model/usermodel'); +const { ModelError } = require('../model/modelError'); module.exports = function assignAlias(opts) { @@ -10,21 +11,10 @@ module.exports = function assignAlias(opts) { return Promise .bind(this, username) - .then(Users.getUser) - .tap(Users.isActive) - .tap(Users.isBanned) - .then(data => { - if (data[USERS_ALIAS_FIELD]) { - throw new Errors.HttpStatusError(417, 'alias is already assigned'); - } - - return Users.storeAlias(username, alias); - }) - .then(assigned => { - if (assigned === 0) { - throw new Errors.HttpStatusError(409, 'alias was already taken'); - } - - return Users.assignAlias(username, alias); - }); + .then(User.getOne) + .tap(isActive) + .tap(isBanned) + .then(data => ({ username, alias, data })) + .then(User.User.setAlias) + .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); }; diff --git a/src/actions/ban.js b/src/actions/ban.js index d73b7d232..8a62b8046 100644 --- a/src/actions/ban.js +++ b/src/actions/ban.js @@ -1,18 +1,6 @@ const Promise = require('bluebird'); -const Users = require('../db/adapter'); - -function lockUser({ username, reason, whom, remoteip }) { - return Users.lockUser({ - username, - reason: reason || '', - whom: whom || '', - remoteip: remoteip || '', - }); -} - -function unlockUser({ username }) { - return Users.unlockUser({ username }); -} +const { User } = require('../model/usermodel'); +const { ModelError } = require('../model/modelError'); /** * Bans/unbans existing user @@ -22,7 +10,8 @@ function unlockUser({ username }) { module.exports = function banUser(opts) { return Promise .bind(this, opts.username) - .then(Users.isExists) - .then(username => ({ ...opts, username })) - .then(opts.ban ? lockUser : unlockUser); + .then(User.getUsername) + .then(username => ({ username, opts })) + .then(opts.ban ? User.lock : User.unlock) + .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); }; diff --git a/src/actions/challenge.js b/src/actions/challenge.js index 8198f1546..9352c581d 100644 --- a/src/actions/challenge.js +++ b/src/actions/challenge.js @@ -1,7 +1,8 @@ const Promise = require('bluebird'); -const Errors = require('common-errors'); const emailChallenge = require('../utils/send-email.js'); -const Users = require('../db/adapter'); +const isActive = require('../utils/isActive'); +const { User } = require('../model/usermodel'); +const { ModelError, ERR_USERNAME_ALREADY_ACTIVE } = require('../model/modelError'); module.exports = function sendChallenge(message) { const { username } = message; @@ -11,9 +12,10 @@ module.exports = function sendChallenge(message) { return Promise .bind(this, username) - .then(Users.getUser) - .tap(Users.isActive) - .throw(new Errors.HttpStatusError(417, `${username} is already active`)) + .then(User.getOne) + .tap(isActive) + .throw(new ModelError(ERR_USERNAME_ALREADY_ACTIVE, username)) .catchReturn({ statusCode: 412 }, username) - .then(emailChallenge.send); + .then(emailChallenge.send) + .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); }; diff --git a/src/actions/getInternalData.js b/src/actions/getInternalData.js index aca32ada3..2f59ac7d3 100644 --- a/src/actions/getInternalData.js +++ b/src/actions/getInternalData.js @@ -1,14 +1,17 @@ const Promise = require('bluebird'); const pick = require('lodash/pick'); -const Users = require('../db/adapter'); + +const { User } = require('../model/usermodel'); +const { ModelError } = require('../model/modelError'); module.exports = function internalData(message) { const { fields } = message; return Promise .bind(this, message.username) - .then(Users.getUser) + .then(User.getOne) .then(data => { return fields ? pick(data, fields) : data; - }); + }) + .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); }; diff --git a/src/actions/getMetadata.js b/src/actions/getMetadata.js index bd5a1c26a..07ab3e065 100644 --- a/src/actions/getMetadata.js +++ b/src/actions/getMetadata.js @@ -1,14 +1,14 @@ const Promise = require('bluebird'); -const noop = require('lodash/noop'); -const Users = require('../db/adapter'); +const { User } = require('../model/usermodel'); +const { ModelError } = require('../model/modelError'); module.exports = function getMetadataAction(message) { const { audience, username, fields } = message; return Promise .bind(this, username) - .then(Users.isExists) - .then(realUsername => [realUsername, audience, fields]) - .spread(Users.getMetadata) - .tap(message.public ? Users.isPublic(username, audience) : noop); + .then(User.getUsername) + .then(realUsername => [realUsername, audience, fields, message.public]) + .spread(User.getMeta) + .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); }; diff --git a/src/actions/list.js b/src/actions/list.js index 4bc3b61fb..1ae9a25cf 100644 --- a/src/actions/list.js +++ b/src/actions/list.js @@ -1,17 +1,11 @@ -const Users = require('../db/adapter'); -const fsort = require('redis-filtered-sort'); -const { USERS_INDEX, USERS_PUBLIC_INDEX } = require('../constants.js'); +const Promise = require('bluebird'); +const { User } = require('../model/usermodel'); +const { ModelError } = require('../model/modelError'); -module.exports = function iterateOverActiveUsers(opts) { - const { criteria, audience, filter } = opts; - return Users.getList({ - criteria, - audience, - index: opts.public ? USERS_PUBLIC_INDEX : USERS_INDEX, - strFilter: typeof filter === 'string' ? filter : fsort.filter(filter || {}), - order: opts.order || 'ASC', - offset: opts.offset || 0, - limit: opts.limit || 10, - }); +module.exports = function iterateOverActiveUsers(opts) { + return Promise + .bind(this, opts) + .then(User.getList) + .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); }; diff --git a/src/db/adapter.js b/src/db/adapter.js index 431e2251c..291aab779 100644 --- a/src/db/adapter.js +++ b/src/db/adapter.js @@ -259,6 +259,36 @@ class Users { return this.adapter.customScript(script); } + /** + * Store user token + * @param username + * @param token + * @returns {Redis} + */ + addToken(username, token) { + return this.adapter.addToken(username, token); + } + + /** + * Drop user token + * @param username + * @param token + * @returns {Redis} + */ + dropToken(username, token) { + return this.adapter.dropToken(username, token); + } + + /** + * Check the last access + * @param username + * @param token + * @returns {Redis} + */ + lastAccess(username, token) { + return this.adapter.lastAccess(username, token); + } + /** * The error wrapper for the front-level HTTP output * @param e diff --git a/src/db/mysqlstorage.js b/src/db/mysqlstorage.js new file mode 100644 index 000000000..36318b7d6 --- /dev/null +++ b/src/db/mysqlstorage.js @@ -0,0 +1,442 @@ +/** + * Created by Stainwoortsel on 14.06.2016. + */ +let loginAttempts; + +/** + * Generate hash key string + * @param args + * @returns {string} + */ +const generateKey = (...args) => { + const SEPARATOR = '!'; + return args.join(SEPARATOR); +}; + +module.exports = { + /** + * Lock user + * @param username + * @param reason + * @param whom + * @param remoteip + * @returns {Redis|{index: number, input: string}} + */ + lockUser({ username, reason, whom, remoteip }) { + /* + SET @aud = '*.localhost'; + SELECT @id=id FROM users WHERE username=:username; + UPDATE users SET isbanned=1 WHERE id=@id; + INSERT INTO user_meta (user_id, audience, k, v) + SELECT @id, @aud, 'banned', 'true' UNION + SELECT @id, @aud, 'banned:reason', :reason UNION + SELECT @id, @aud, 'banned:whom', :whom UNION + SELECT @id, @aud, 'banned:remoteip', :remoteip; + DELETE FROM user_tokens WHERE user_id=@id; + */ + }, + + /** + * Unlock user + * @param username + * @returns {Redis|{index: number, input: string}} + */ + unlockUser({ username }) { + /* + SET @aud = '*.localhost'; + SELECT @id=id FROM users WHERE username=:username; + DELETE FROM user_meta WHERE user_id=@id and k like 'banned%'; + // селект для наглядности, а вообще городят DELETE c 2 таблицами + */ + }, + + /** + * Check existance of user + * @param username + * @returns {Redis|username} + */ + isExists(username) { //ПЕРЕИМЕНОВАТЬ скорее fetchUsername resolve + /* + SELECT count(id) FROM users WHERE username=:username; + + return username | throw new Errors.HttpStatusError(404, `"${username}" does not exists`); + */ + }, + + /** + * Check the existance of alias + * @param alias + * @returns {username|''} + */ + aliasAlreadyExists(alias) { // УДАЛИТЬ в логику регистрации или setAlias который и юзать + /* + SELECT (SELECT count(id) FROM users WHERE alias=:alias) + + (SELECT count(user_id) FROM user_alias WHERE alias=:alias); + + return '' | throw new Errors.HttpStatusError(409, `"${alias}" already exists`); ??? почему пустая строка? + */ + }, + + /** + * User is public + * @param username + * @param audience + * @returns {function()} + */ + isPublic(username, audience) { // УДАЛИТЬ на самом деле, этот метод проверяет по какому полю запрашиваются + // метадата. если это емайл то мы ему кукишь, ибо низзя по имэйлу спрашивать, то есть это + // элемент метода гет-метадата, а значит пихаем в логику getMetadata + /* + SELECT ispublic FROM users WHERE username=:username ????? + || + SELECT um.v + FROM user_meta um + JOIN users u ON u.id=um.user_id AND u.username=:username + WHERE + um.audience=:audience AND + um.k = 'alias' + + return v === username | throw new Errors.HttpStatusError(404, 'username was not found') + */ + }, + + + /** + * Check that user is active + * @param data + * @returns {Promise} + */ + isActive(data) { + /* + логика из даты, лучше такие методы запихнуть в утилиты адаптера которые просто будут кидать ошибки + */ + }, + + /** + * Check that user is banned + * @param data + * @returns {Promise} + */ + isBanned(data) { + /* + см выше + */ + }, + + /** + * Activate user account + * @param user + * @returns {Redis} + */ + activateAccount(username) { + /* + UPDATE users SET isactive=1 WHERE username=:username; + достаточно просто апдейта, не надо страдать фигней + можно ошибку кидать по кол-ву обработанных строчек + если 0 то уже активировали + + + || + SELECT @id=id, @active=isactive FROM users WHERE username=:username; + // проверяем есть ли пользователь и активирован ли он? + UPDATE users SET isactive=1 WHERE id=@id; + // throw new Errors.HttpStatusError(417, `Account ${user} was already activated`); нужна ли проверка?.. + */ + }, + + /** + * Get user internal data + * @param username + * @returns {Object} + */ + getUser(username) { + /* + SELECT @id=id, ..... FROM users WHERE username=:username OR alias=:username; + // возможно нужна проверка по таблице user_aliases + // SELECT audience, k, v FROM user_meta WHERE user_id=@id; + */ + }, + + _getMeta(username, audience) { + /* + SELECT audience, k, v + FROM user_meta + JOIN + WHERE user_id=@id; + */ + }, + _remapMeta(data, audiences, fields) { + // ... + // сериализацию десериализацию можно оставить во вне, она скорее всего будет одинаковой + }, + + + /** + * Get users metadata by username and audience + * @param username + * @param audience + * @param fields + * @returns {Object} + */ + getMetadata(username, _audiences, fields = {}) { + /* + _getMeta + _remapData + */ + }, + + + /** + * Return the list of users by specified params + * @param opts + * @returns {Array} + */ + getList(opts) { + /* + + fsort -- это и селект и сортировка и всё вместе + + + + SELECT + FROM users, user_meta + + WHERE :criteria + + ORDER BY :order + LIMIT :offset, :limit + + // здесь возможен селект с join-ами на user_meta, + // если критерии выборки будут в полях таблицы user_meta + */ + }, + + /** + * Check that user is admin + * @param meta + * @returns {boolean} + */ + isAdmin(meta) { + /* + это выносим в утилиты, работает по метаданным + */ + }, + + /** + * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN + * @param username + * @param alias + * @returns {Redis} + */ + storeAlias(username, alias) { + /* + сделать метод setAlias + + INSERT INTO user_aliases(user_id, alias) + SELECT id, :alias + FROM users + WHERE username=:username; + */ + }, + + /** + * Assign alias to the user record, marked by username + * @param username + * @param alias + * @returns {Redis} + */ + assignAlias(username, alias) { + /* + сделать метод setAlias + + + SElECT @id = id FROM users WHERE username=:username; + UPDATE users SET alias=:alias, ispublic=1 WHERE id=@id; + UPDATE user_meta SET v = :alias WHERE user_id=@id AND audience='*.localhost' AND k='alias' + */ + }, + + /** + * Return current login attempts count + * @returns {int} + */ + getAttempts() { + return loginAttempts; + }, + /** + * Drop login attempts counter + * @returns {Redis} + */ + dropAttempts() { + loginAttempts = 0; + /* + DELETE FROM login_attempts WHERE ip=:ip + */ + }, + + /** + * Check login attempts + * @param data + * @param _remoteip + * @returns {Redis} + */ + checkLoginAttempts(data, _remoteip) { + /* + SELECT attempts, lastattempt FROM login_attempts WHERE ip=:ip AND username=:username; + + // здесь смотрим на кол-во попыток и на expire, само собой expire удаляем ВРУЧНУЮ! + + UPDATE login_attempts + SET attempts=attempts + 1, lastattempt=NOW() + WHERE ip=:ip AND username=:username; + + */ + }, + + /** + * Set user password + * @param username + * @param hash + * @returns {Redis} + */ + setPassword({ username, hash }) { + /* + UPDATE users SET pass=:hash WHERE username=:username + */ + }, + + /** + * Reset the lock by IP + * @param username + * @param ip + * @returns {Redis} + */ + resetIPLock(username, ip) { + /* + DELETE FROM login_attempts WHERE ip=:id AND username=:username + */ + }, + + + /** + * Process metadata update operation for a passed audience / inner method + * @param {Object} pipeline + * @param {String} audience + * @param {Object} metadata + */ + _handleAudience(pipeline, key, metadata) { + // ... updating user_meta, да это чисто внутренний метод, в sql он будет другой + }, + + + /** + * + * @param username + * @param audience + * @param metadata + * @returns {Object} + */ + updateMetadata({ username, audience, metadata, script }) { + // ... updating user_meta + // скрипт вынести за логику апдейта, толкьо для эдванст-юзыч + }, + + /** + * Removing user by username (and data?) + * @param username + * @param data + * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} + */ + removeUser(username, data) { + /* + SELECT @id = id FROM users WHERE username=:username; + DELETE FROM users WHERE id=@id; + DELETE FROM user_aliases WHERE user_id=@id; + DELETE FROM user_meta WHERE user_id=@id; + DELETE FROM user_tokens WHERE user_id=@id; + DELETE FROM login_attempts WHERE username=:username; ?? + */ + }, + + /** + * Verify ip limits + * @param {redisCluster} redis + * @param {Object} registrationLimits + * @param {String} ipaddress + * @return {Function} + */ + checkLimits(ipaddress) { + /* + INSERT INTO register2ip (ip, uuid, registered) + SELECT :ip, :uuid, NOW(); + + SELECT * FROM register2ip WHERE ip=:ipaddress + + DELETE FROM register2ip WHERE ip=:ipaddress AND registered < :old + + SELECT count(*) FROM register2ip WHERE ip=:ipaddress + + // проверка на ограничения + */ + }, + + /** + * Creates user with a given hash + * @param redis + * @param username + * @param activate + * @param userDataKey + * @returns {Function} + */ + createUser(username, activate) { + /* + можно сразу с алиасом + т.е. обеспечить атомарность + + проверить на наличие пользователя + + INSERT INTO users (username, isactive, registered) + SELECT :username, :activate, NOW(); + + // проверка на существование + */ + }, + + /** + * Performs captcha check, returns thukn + * @param {String} username + * @param {String} captcha + * @return {Function} + */ + checkCaptcha(username, captcha) { + // ... + }, + + /** + * Stores username to the index set + * @param username + * @returns {Redis} + */ + storeUsername(username) { + // по сути метод не нужен + // это все из логики регистрации + // а вообще это логика активации пользователя, + // просто при регистрации не нужен персист, ибо сразу, а для активации персист нуежн + // то есть если я его лишний раз вызову, ничего страшного + // это намек на отдельный большой метод activateUser для регистрации и активации + // ... у users в РСУБД есть поле ID, этот метод будет пустым + }, + + /** + * Execute custom script on LUA + * @param script + * @returns {Promise} + */ + customScript(script, keys) { + // ... + }, + + handleAudience(key, metadata) { + // updating user_meta + } +}; diff --git a/src/db/redisstorage.js b/src/db/redisstorage.js index 2feaec34d..00e8b192f 100644 --- a/src/db/redisstorage.js +++ b/src/db/redisstorage.js @@ -27,7 +27,8 @@ const { // config's and base objects const { redis, captcha: captchaConfig, config } = this; -const { deleteInactiveAccounts, jwt: { lockAfterAttempts, defaultAudience } } = config; +const { deleteInactiveAccounts, jwt: { lockAfterAttempts, defaultAudience, hashingFunction: { ttl } } } = config; + // local vatiables inside the module let remoteipKey; @@ -284,7 +285,7 @@ module.exports = { [], length, ]; - } + }o const pipeline = redis.pipeline(); ids.forEach(id => { @@ -648,6 +649,49 @@ module.exports = { }); }, + /** + * Store user token + * @param username + * @param token + * @returns {Redis} + */ + addToken(username, token) { + return redis.zadd(generateKey(username, USERS_TOKENS), Date.now(), token); + }, + + /** + * Drop user token + * @param username + * @param token + * @returns {Redis} + */ + dropToken(username, token) { + return token ? + redis.zrem(generateKey(username, USERS_TOKENS), token) : + redis.del(generateKey(username, USERS_TOKENS)); + }, + + /** + * Check the last access + * @param username + * @param token + * @returns {Redis} + */ + lastAccess(username, token) { + const tokensHolder = generateKey(username, USERS_TOKENS); + return redis.zscoreBuffer(tokensHolder, token).then(function getLastAccess(_score) { + // parseResponse + const score = parseInt(_score, 10); + + // throw if token not found or expired + if (isNaN(score) || Date.now() > score + ttl) { + throw new Errors.HttpStatusError(403, 'token has expired or was forged'); + } + + return score; + }); + }, + handleAudience(key, metadata) { const pipeline = redis.pipeline(); const $remove = metadata.$remove; diff --git a/src/model/modelError.js b/src/model/modelError.js new file mode 100644 index 000000000..8f7d30488 --- /dev/null +++ b/src/model/modelError.js @@ -0,0 +1,132 @@ +/** + * Created by Stainwoortsel on 17.06.2016. + */ +const findKey = require('lodash/findKey'); +const mapValues = require('lodash/mapValues'); +const isFunction = require('lodash/isFunction'); +const fmt = require('util').format; +const Errors = require('common-errors'); + +/** + * Generate error structure + * @param code + * @param http + * @param msg + */ +const genErr = (code, http, msg) => ({ code, http, msg }); + +/** + * Maping error code helper function + * @param e + */ +const mapErr = (e) => e.code; + +/** + * Error types structure + * @type {Object} + */ +const ErrorTypes = { + ERR_ALIAS_ALREADY_ASSIGNED: + genErr(100, 417, 'alias is already assigned'), + ERR_ALIAS_ALREADY_TAKEN: + genErr(101, 409, 'alias was already taken'), + ERR_ALIAS_ALREADY_EXISTS: + genErr(102, 409, (alias) => (`"${alias}" already exists`)), + ERR_USERNAME_ALREADY_ACTIVE: + genErr(110, 417, (username) => (`${username} is already active`)), + ERR_USERNAME_ALREADY_EXISTS: + genErr(111, 409, (username) => (`${username} is already exists`)), + ERR_USERNAME_NOT_EXISTS: + genErr(112, 404, (username) => (`${username} does not exists`)), + ERR_USERNAME_NOT_FOUND: + genErr(113, 404, 'username was not found'), + ERR_ACCOUNT_MUST_BE_ACTIVATED: + genErr(120, 400, 'Account must be activated when setting alias during registration'), + ERR_ACCOUNT_NOT_ACTIVATED: + genErr(121, 412, 'Account hasn\'t been activated'), + ERR_ACCOUNT_ALREADY_ACTIVATED: + genErr(122, 417, (user) => (`Account ${user} was already activated`)), + ERR_ACCOUNT_IS_LOCKED: + genErr(123, 423, 'Account has been locked'), + ERR_ACCOUNT_IS_ALREADY_EXISTS: + genErr(124, 412, (username) => (`User "${username}" already exists`)), + ERR_ADMIN_IS_UNTOUCHABLE: + genErr(130, 400, 'can\'t remove admin user from the system'), + ERR_CAPTCHA_WRONG_USERNAME: + genErr(140, 412, 'Captcha challenge you\'ve solved can not be used, please complete it again'), // eslint-disable-line + ERR_CAPTCHA_ERROR_RESPONSE: + genErr(141, 412, (errData) => fmt('Captcha response: %s', errData)), + ERR_EMAIL_DISPOSABLE: + genErr(150, 400, 'you must use non-disposable email to register'), + ERR_EMAIL_NO_MX: + genErr(151, 400, (hostname) => (`no MX record was found for hostname ${hostname}`)), + ERR_EMAIL_ALREADY_SENT: + genErr(152, 429, 'We\'ve already sent you an email, if it doesn\'t come - please try again in a little while or send us an email'), // eslint-disable-line + + ERR_TOKEN_INVALID: + genErr(160, 403, 'Invalid Token'), + ERR_TOKEN_AUDIENCE_MISMATCH: + genErr(161, 403, 'audience mismatch'), + ERR_TOKEN_MISS_EMAIL: + genErr(162, 403, 'Decoded token misses references to email and/or secret'), + ERR_TOKEN_EXPIRED: + genErr(163, 404, 'token expired or is invalid'), + ERR_TOKEN_FORGED: + genErr(164, 403, 'token has expired or was forged'), + ERR_TOKEN_BAD_EMAIL: + genErr(165, 412, 'associated email doesn\'t match token'), + ERR_TOKEN_CANT_DECODE: + genErr(166, 403, 'could not decode token'), + ERR_PASSWORD_INVALID: + genErr(170, 500, 'invalid password passed'), + ERR_PASSWORD_INVALID_HASH: + genErr(171, 500, 'invalid password hash retrieved from storage'), + ERR_PASSWORD_INCORRECT: + genErr(172, 403, 'incorrect password'), + ERR_PASSWORD_SCRYPT_ERROR: + genErr(173, 403, (err) => (err.scrypt_err_message || err.message)), + ERR_ATTEMPTS_LOCKED: + genErr(180, 429, (duration) => (`You are locked from making login attempts for the next ${duration}`)), + ERR_ATTEMPTS_TO_MUCH_REGISTERED: + genErr(181, 429, 'You can\'t register more users from your ipaddress now'), +}; + +/** + * Error codes map + * @type {Object} + */ +const ErrorCodes = mapValues(ErrorTypes, mapErr); + +/** + * Fabric for error classes with mapToHttp method, to map error into HttpStatusError + * @param name + * @param opts + * @returns {Class} HTTP-mapped error class + */ +const generateHttpMapedClass = function generateHttpMapedClass(name, opts) { + const Class = Errors.helpers.generateClass(name, opts); + + Class.prototype.mapToHttp = function mapToHttp() { + const key = findKey(ErrorTypes, { code: this.code }); + const err = ErrorTypes[key]; + return new Errors.HttpStatusError(err.http, this.generateMessage()); + }; + + return Class; +}; + +/** + * Model error class + * @type {Class} + */ +const ModelError = generateHttpMapedClass('ModelError', { + extends: Errors.Error, + args: ['code', 'data'], + generateMessage: function generateMessage() { + const key = findKey(ErrorTypes, { code: this.code }); + const err = ErrorTypes[key]; + return isFunction(err.msg) ? err.msg.call(this, this.data) : err.msg; + }, +}); + +module.exports = { ...ErrorCodes, ModelError }; diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js new file mode 100644 index 000000000..afbae743a --- /dev/null +++ b/src/model/storages/redisstorage.js @@ -0,0 +1,337 @@ +/** + * Created by Stainwoortsel on 17.06.2016. + */ +const Errors = require('common-errors'); +const remapMeta = require('../../utils/remapMeta'); +const mapValues = require('lodash/mapValues'); +const fsort = require('redis-filtered-sort'); +const noop = require('lodash/noop'); +const get = require('lodash/get'); +const { + ModelError, + ERR_ALIAS_ALREADY_ASSIGNED, ERR_ALIAS_ALREADY_TAKEN, ERR_USERNAME_NOT_EXISTS, ERR_USERNAME_NOT_FOUND, +} = require('../modelError'); +/* + ERR_ALIAS_ALREADY_EXISTS, ERR_USERNAME_ALREADY_ACTIVE, + ERR_USERNAME_ALREADY_EXISTS, ERR_ACCOUNT_MUST_BE_ACTIVATED, + ERR_ACCOUNT_NOT_ACTIVATED, ERR_ACCOUNT_ALREADY_ACTIVATED, ERR_ACCOUNT_IS_LOCKED, ERR_ACCOUNT_IS_ALREADY_EXISTS, + ERR_ADMIN_IS_UNTOUCHABLE, ERR_CAPTCHA_WRONG_USERNAME, ERR_CAPTCHA_ERROR_RESPONSE, ERR_EMAIL_DISPOSABLE, + ERR_EMAIL_NO_MX, ERR_EMAIL_ALREADY_SENT, ERR_TOKEN_INVALID, ERR_TOKEN_AUDIENCE_MISMATCH, ERR_TOKEN_MISS_EMAIL, + ERR_TOKEN_EXPIRED, ERR_TOKEN_FORGED, ERR_TOKEN_BAD_EMAIL, ERR_TOKEN_CANT_DECODE, ERR_PASSWORD_INVALID, + ERR_PASSWORD_INVALID_HASH, ERR_PASSWORD_INCORRECT, ERR_PASSWORD_SCRYPT_ERROR, ERR_ATTEMPTS_LOCKED, + ERR_ATTEMPTS_TO_MUCH_REGISTERED +*/ + +// JSON +const JSONStringify = JSON.stringify.bind(JSON); +const JSONParse = JSON.parse.bind(JSON); + +// constants +const { + USERS_DATA, USERS_METADATA, USERS_ALIAS_TO_LOGIN, + USERS_ACTIVE_FLAG, USERS_INDEX, USERS_PUBLIC_INDEX, + USERS_ALIAS_FIELD, +} = require('../../constants'); +/* + USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA, + USERS_ADMIN_ROLE, + */ + +// config's and base objects +const { redis, captcha: captchaConfig, config } = this; +const { deleteInactiveAccounts, jwt: { lockAfterAttempts, defaultAudience, hashingFunction: { ttl } } } = config; + +/** + * Generate hash key string + * @param args + * @returns {string} + */ +const generateKey = (...args) => { + const SEPARATOR = '!'; + return args.join(SEPARATOR); +}; + + +module.exports = {}; + +module.exports.User = { + + /** + * Get user by username + * @param username + * @returns {Object} + */ + getOne(username) { + const userKey = generateKey(username, USERS_DATA); + + return redis + .pipeline() + .hget(USERS_ALIAS_TO_LOGIN, username) + .exists(userKey) + .hgetallBuffer(userKey) + .exec() + .spread((aliasToUsername, exists, data) => { + if (aliasToUsername[1]) { + return this.getUser(aliasToUsername[1]); + } + + if (!exists[1]) { + throw new ModelError(ERR_USERNAME_NOT_EXISTS); + } + + return { ...data[1], username }; + }); + }, + + /** + * Get list of users by params + * @param opts + * @returns {Array} + */ + getList(opts) { + const { criteria, audience, filter } = opts; + const strFilter = typeof filter === 'string' ? filter : fsort.filter(filter || {}); + const order = opts.order || 'ASC'; + const offset = opts.offset || 0; + const limit = opts.limit || 10; + const index = opts.public ? USERS_PUBLIC_INDEX : USERS_INDEX; + + const metaKey = generateKey('*', USERS_METADATA, audience); + + return redis + .fsort(index, metaKey, criteria, order, strFilter, offset, limit) + .then(ids => { + const length = +ids.pop(); + if (length === 0 || ids.length === 0) { + return [ + ids || [], + [], + length, + ]; + } + + const pipeline = redis.pipeline(); + ids.forEach(id => { + pipeline.hgetallBuffer(generateKey(id, USERS_METADATA, audience)); + }); + return Promise.all([ + ids, + pipeline.exec(), + length, + ]); + }) + .spread((ids, props, length) => { + const users = ids.map(function remapData(id, idx) { + const data = props[idx][1]; + const account = { + id, + metadata: { + [audience]: data ? mapValues(data, JSONParse) : {}, + }, + }; + + return account; + }); + + return { + users, + cursor: offset + limit, + page: Math.floor(offset / limit + 1), + pages: Math.ceil(length / limit), + }; + }); + }, + + /** + * Get metadata of user + * @param username + * @param _audiences + * @param fields + * @param _public + * @returns {Object} + */ + getMeta(username, _audiences, fields = {}, _public = null) { + const audiences = Array.isArray(_audiences) ? _audiences : [_audiences]; + const audience = Array.isArray(_audiences) ? _audiences[0] : _audiences; + + return Promise + .map(audiences, _audience => { + return redis.hgetallBuffer(generateKey(username, USERS_METADATA, _audience)); + }) + .then(data => { + return remapMeta(data, audiences, fields); + }) + .tap(_public ? (metadata) => { + if (get(metadata, [audience, USERS_ALIAS_FIELD]) === username) { + return; + } + + throw new ModelError(ERR_USERNAME_NOT_FOUND); + } : noop); + }, + + /** + * Get ~real~ username by username or alias + * @param username + * @returns {String} username + */ + getUsername(username) { + return redis + .pipeline() + .hget(USERS_ALIAS_TO_LOGIN, username) + .exists(generateKey(username, USERS_DATA)) + .exec() + .spread((alias, exists) => { + if (alias[1]) { + return alias[1]; + } + + if (!exists[1]) { + throw new ModelError(ERR_USERNAME_NOT_EXISTS, username); + } + + return username; + }); + }, + + setAlias(username, alias, data) { + if (data[USERS_ALIAS_FIELD]) { + throw new ModelError(ERR_ALIAS_ALREADY_ASSIGNED); + } + + const assigned = redis.hsetnx(USERS_ALIAS_TO_LOGIN, alias, username); + if (assigned === 0) { + throw new ModelError(ERR_ALIAS_ALREADY_TAKEN); + } + + return redis + .pipeline() + .sadd(USERS_PUBLIC_INDEX, username) + .hset(generateKey(username, USERS_DATA), USERS_ALIAS_FIELD, alias) + .hset(generateKey(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, JSONStringify) + .exec(); + }, + + /** + * Set user password + * @param username + * @param hash + * @returns {String} username + */ + setPassword(username, hash) { + return this.adapter.setPassword(username, hash); + }, + + /** + * Updates metadata of user by username and audience + * @param username + * @param audience + * @param metadata + * @returns {Object} + */ + setMeta(username, audience, metadata) { + return this.adapter.setMeta(username, audience, metadata); + }, + + /** + * Create user account with alias and password + * @param username + * @param alias + * @param hash + * @param activate + * @returns {*} + */ + create(username, alias, hash, activate) { + return this.adapter.create(username, alias, hash, activate); + }, + + /** + * Remove user + * @param username + * @param data + * @returns {*} + */ + remove(username, data) { + return this.adapter.remove(username, data); + }, + + /** + * Activate user + * @param username + * @returns {*} + */ + activate(username) { + const userKey = generateKey(username, USERS_DATA); + + // WARNING: `persist` is very important, otherwise we will lose user's information in 30 days + // set to active & persist + return redis + .pipeline() + .hget(userKey, USERS_ACTIVE_FLAG) + .hset(userKey, USERS_ACTIVE_FLAG, 'true') + .persist(userKey) + .sadd(USERS_INDEX, username) + .exec() + .spread(function pipeResponse(isActive) { + const status = isActive[1]; + if (status === 'true') { + throw new Errors.HttpStatusError(417, `Account ${username} was already activated`); + } + }); + }, + + /** + * Ban user + * @param username + * @param opts + * @returns {*} + */ + lock(username, opts) { + + }, + + /** + * Unlock banned user + * @param username + * @returns {*} + */ + unlock(username) { + + }, +}; + +let loginAttempts; +module.exports.Attempts = { + + check: function check(username, ip) { + const ipKey = generateKey(username, 'ip', ip); + // ... + }, + + /** + * Drop login attempts + * @param username + * @param ip + * @returns {*} + */ + drop: function drop(username, ip) { + const ipKey = generateKey(username, 'ip', ip); + return redis.del(ipKey); + }, + + /** + * Get attempts count + * @returns {*} + */ + count: function count() { + return loginAttempts; + }, +}; + +module.exports.Tokens = { + +}; + +module.exports.Utils = { + +}; diff --git a/src/model/usermodel.js b/src/model/usermodel.js new file mode 100644 index 000000000..3caf4d61b --- /dev/null +++ b/src/model/usermodel.js @@ -0,0 +1,151 @@ +/** + * Created by Stainwoortsel on 17.06.2016. + */ +const storage = require('./storages/redisstorage'); + + +class UserModel { + /** + * Create user model + * @param adapter + */ + constructor(adapter) { + this.adapter = adapter; + } + + /** + * Get user by username + * @param username + * @returns {Object} + */ + getOne(username) { + return this.adapter.getOne(username); + } + + /** + * Get list of users by params + * @param opts + * @returns {Array} + */ + getList(opts) { + return this.adapter.getList(opts); + } + + /** + * Get metadata of user + * @param username + * @param audiences + * @param fields + * @returns {Object} + */ + getMeta(username, audiences, fields = {}) { + return this.adapter.getMeta(username, audiences, fields); + } + + /** + * Get ~real~ username by username or alias + * @param username + * @returns {String} username + */ + getUsername(username) { + return this.adapter.getUsername(username); + } + + setAlias(username, alias) { + return this.adapter.setAlias(username, alias); + } + + /** + * Set user password + * @param username + * @param hash + * @returns {String} username + */ + setPassword(username, hash) { + return this.adapter.setPassword(username, hash); + } + + /** + * Updates metadata of user by username and audience + * @param username + * @param audience + * @param metadata + * @returns {Object} + */ + setMeta(username, audience, metadata) { + return this.adapter.setMeta(username, audience, metadata); + } + + /** + * Create user account with alias and password + * @param username + * @param alias + * @param hash + * @param activate + * @returns {*} + */ + create(username, alias, hash, activate) { + return this.adapter.create(username, alias, hash, activate); + } + + /** + * Remove user + * @param username + * @param data + * @returns {*} + */ + remove(username, data) { + return this.adapter.remove(username, data); + } + + /** + * Activate user + * @param username + * @returns {*} + */ + activate(username) { + return this.adapter.activate(username); + } + + /** + * Ban user + * @param username + * @param opts + * @returns {*} + */ + lock(username, opts) { + return this.adapter.lock(username, opts); + } + + /** + * Unlock banned user + * @param username + * @returns {*} + */ + unlock(username) { + return this.adapter.unlock(username); + } +} + +class AttemptsHelper { + + constructor(adapter) { + this.adapter = adapter; + } + + check(username, ip) { + return this.adapter.check(username, ip); + } + + drop(username, ip) { + return this.adapter.drop(username, ip); + } + + count() { + return this.adapter.count(); + } + +} + +module.exports.User = new UserModel(storage.User); +module.exports.Attempts = new AttemptsHelper(storage.Attempts); diff --git a/src/utils/isActive.js b/src/utils/isActive.js index 69c8fafc6..3e2da704a 100644 --- a/src/utils/isActive.js +++ b/src/utils/isActive.js @@ -1,10 +1,10 @@ const Promise = require('bluebird'); -const Errors = require('common-errors'); +const { ModelError, ERR_ACCOUNT_NOT_ACTIVATED } = require('../model/modelError'); const { USERS_ACTIVE_FLAG } = require('../constants.js'); module.exports = function isBanned(data) { if (String(data[USERS_ACTIVE_FLAG]) !== 'true') { - return Promise.reject(new Errors.HttpStatusError(412, 'Account hasn\'t been activated')); + return Promise.reject(new ModelError(ERR_ACCOUNT_NOT_ACTIVATED)); } return Promise.resolve(data); diff --git a/src/utils/isBanned.js b/src/utils/isBanned.js index 197a38915..c82c8977c 100644 --- a/src/utils/isBanned.js +++ b/src/utils/isBanned.js @@ -1,11 +1,10 @@ const Promise = require('bluebird'); -const Errors = require('common-errors'); +const { ModelError, ERR_ACCOUNT_IS_LOCKED } = require('../model/modelError'); const { USERS_BANNED_FLAG } = require('../constants.js'); module.exports = function isBanned(data) { if (String(data[USERS_BANNED_FLAG]) === 'true') { - return Promise.reject(new Errors.HttpStatusError(423, 'Account has been locked')); + return Promise.reject(new ModelError(ERR_ACCOUNT_IS_LOCKED)); } - return Promise.resolve(data); }; diff --git a/src/utils/jwt.js b/src/utils/jwt.js index 2bdd04622..c5dfa3274 100644 --- a/src/utils/jwt.js +++ b/src/utils/jwt.js @@ -1,11 +1,10 @@ const Errors = require('common-errors'); const Promise = require('bluebird'); const jwt = Promise.promisifyAll(require('jsonwebtoken')); -const redisKey = require('./key.js'); const getMetadata = require('../utils/getMetadata.js'); const FlakeId = require('flake-idgen'); const flakeIdGen = new FlakeId(); -const { USERS_TOKENS } = require('../constants.js'); +const Users = require('../db/adapter'); /** * Logs user in and returns JWT and User Object @@ -14,7 +13,7 @@ const { USERS_TOKENS } = require('../constants.js'); * @return {Promise} */ exports.login = function login(username, _audience) { - const { redis, config } = this; + const { config } = this; const { jwt: jwtConfig } = config; const { hashingFunction: algorithm, defaultAudience, secret } = jwtConfig; let audience = _audience || defaultAudience; @@ -35,7 +34,7 @@ exports.login = function login(username, _audience) { } return Promise.props({ - lastAccessUpdated: redis.zadd(redisKey(username, USERS_TOKENS), Date.now(), token), + lastAccessUpdated: Users.addToken(username, token), jwt: token, username, metadata: getMetadata.call(this, username, audience), @@ -58,7 +57,7 @@ exports.login = function login(username, _audience) { * @return {Promise} */ exports.logout = function logout(token, audience) { - const { redis, config } = this; + const { config } = this; const { jwt: jwtConfig } = config; const { hashingFunction: algorithm, secret, issuer } = jwtConfig; @@ -69,7 +68,7 @@ exports.logout = function logout(token, audience) { throw new Errors.HttpStatusError(403, 'Invalid Token'); }) .then(function decodedToken(decoded) { - return redis.zrem(redisKey(decoded.username, USERS_TOKENS), token); + return Users.dropToken(decoded.username, token); }) .return({ success: true }); }; @@ -79,7 +78,7 @@ exports.logout = function logout(token, audience) { * @param {String} username */ exports.reset = function reset(username) { - return this.redis.del(redisKey(username, USERS_TOKENS)); + return Users.dropToken(username); }; /** @@ -90,9 +89,9 @@ exports.reset = function reset(username) { * @return {Promise} */ exports.verify = function verifyToken(token, audience, peek) { - const { redis, config } = this; + const { config } = this; const { jwt: jwtConfig } = config; - const { hashingFunction: algorithm, secret, ttl, issuer } = jwtConfig; + const { hashingFunction: algorithm, secret, issuer } = jwtConfig; return jwt .verifyAsync(token, secret, { issuer, algorithms: [algorithm] }) @@ -106,22 +105,11 @@ exports.verify = function verifyToken(token, audience, peek) { } const { username } = decoded; - const tokensHolder = redisKey(username, USERS_TOKENS); - let lastAccess = redis.zscoreBuffer(tokensHolder, token).then(function getLastAccess(_score) { - // parseResponse - const score = parseInt(_score, 10); - - // throw if token not found or expired - if (isNaN(score) || Date.now() > score + ttl) { - throw new Errors.HttpStatusError(403, 'token has expired or was forged'); - } - - return score; - }); + let lastAccess = Users.lastAccess(username, token); if (!peek) { lastAccess = lastAccess.then(function refreshLastAccess() { - return redis.zadd(tokensHolder, Date.now(), token); + return Users.addToken(username, token); }); } diff --git a/src/utils/remapMeta.js b/src/utils/remapMeta.js new file mode 100644 index 000000000..adb0b005b --- /dev/null +++ b/src/utils/remapMeta.js @@ -0,0 +1,25 @@ +/** + * Created by Stainwoortsel on 18.06.2016. + */ +const mapValues = require('lodash/mapValues'); +const pick = require('lodash/pick'); +const JSONParse = JSON.parse.bind(JSON); + +module.exports = function remapMeta(data, audiences, fields) { + const output = {}; + audiences.forEach(function transform(aud, idx) { + const datum = data[idx]; + + if (datum) { + const pickFields = fields[aud]; + output[aud] = mapValues(datum, JSONParse); + if (pickFields) { + output[aud] = pick(output[aud], pickFields); + } + } else { + output[aud] = {}; + } + }); + + return output; +}; diff --git a/test/.bin/docker-compose b/test/.bin/docker-compose new file mode 100644 index 000000000..e69de29bb From 821deb686650cafb3146b9bf7605308fb74a72b8 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Mon, 20 Jun 2016 03:20:09 +0300 Subject: [PATCH 14/38] feat: renewed modelError methods, usermodal, redisstodage; utils: jwt; actions: remove, requestPassw --- src/actions/activate.js | 4 +- src/actions/alias.js | 4 +- src/actions/ban.js | 4 +- src/actions/challenge.js | 4 +- src/actions/getInternalData.js | 4 +- src/actions/getMetadata.js | 4 +- src/actions/list.js | 4 +- src/actions/login.js | 23 +- src/actions/remove.js | 16 +- src/actions/requestPassword.js | 15 +- src/actions/updateMetadata.js | 8 +- src/actions/updatePassword.js | 24 +- src/actions/verify.js | 8 +- src/db/adapter.js | 307 ------------ src/db/mysqlstorage.js | 442 ------------------ src/db/redisstorage.js | 721 ----------------------------- src/model/modelError.js | 55 ++- src/model/storages/redisstorage.js | 188 +++++++- src/model/usermodel.js | 39 +- src/utils/jwt.js | 23 +- 20 files changed, 325 insertions(+), 1572 deletions(-) delete mode 100644 src/db/adapter.js delete mode 100644 src/db/mysqlstorage.js delete mode 100644 src/db/redisstorage.js diff --git a/src/actions/activate.js b/src/actions/activate.js index a5523c0a5..f8df0a52b 100644 --- a/src/actions/activate.js +++ b/src/actions/activate.js @@ -2,7 +2,7 @@ const Promise = require('bluebird'); const emailVerification = require('../utils/send-email.js'); const jwt = require('../utils/jwt.js'); const { User } = require('../model/usermodel'); -const { ModelError } = require('../model/modelError'); +const { httpErrorMapper } = require('../model/modelError'); module.exports = function verifyChallenge(opts) { // TODO: add security logs @@ -26,5 +26,5 @@ module.exports = function verifyChallenge(opts) { .tap(hook) .then(user => [user, audience]) .spread(jwt.login) - .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); + .catch(e => { throw httpErrorMapper(e); }); }; diff --git a/src/actions/alias.js b/src/actions/alias.js index 25a2bc160..5d4c306a3 100644 --- a/src/actions/alias.js +++ b/src/actions/alias.js @@ -3,7 +3,7 @@ const isActive = require('../utils/isActive'); const isBanned = require('../utils/isBanned'); const { User } = require('../model/usermodel'); -const { ModelError } = require('../model/modelError'); +const { httpErrorMapper } = require('../model/modelError'); module.exports = function assignAlias(opts) { @@ -16,5 +16,5 @@ module.exports = function assignAlias(opts) { .tap(isBanned) .then(data => ({ username, alias, data })) .then(User.User.setAlias) - .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); + .catch(e => { throw httpErrorMapper(e); }); }; diff --git a/src/actions/ban.js b/src/actions/ban.js index 8a62b8046..a6ef07d42 100644 --- a/src/actions/ban.js +++ b/src/actions/ban.js @@ -1,6 +1,6 @@ const Promise = require('bluebird'); const { User } = require('../model/usermodel'); -const { ModelError } = require('../model/modelError'); +const { httpErrorMapper } = require('../model/modelError'); /** * Bans/unbans existing user @@ -13,5 +13,5 @@ module.exports = function banUser(opts) { .then(User.getUsername) .then(username => ({ username, opts })) .then(opts.ban ? User.lock : User.unlock) - .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); + .catch(e => { throw httpErrorMapper(e); }); }; diff --git a/src/actions/challenge.js b/src/actions/challenge.js index 9352c581d..8893b9144 100644 --- a/src/actions/challenge.js +++ b/src/actions/challenge.js @@ -2,7 +2,7 @@ const Promise = require('bluebird'); const emailChallenge = require('../utils/send-email.js'); const isActive = require('../utils/isActive'); const { User } = require('../model/usermodel'); -const { ModelError, ERR_USERNAME_ALREADY_ACTIVE } = require('../model/modelError'); +const { ModelError, httpErrorMapper, ERR_USERNAME_ALREADY_ACTIVE } = require('../model/modelError'); module.exports = function sendChallenge(message) { const { username } = message; @@ -17,5 +17,5 @@ module.exports = function sendChallenge(message) { .throw(new ModelError(ERR_USERNAME_ALREADY_ACTIVE, username)) .catchReturn({ statusCode: 412 }, username) .then(emailChallenge.send) - .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); + .catch(e => { throw httpErrorMapper(e); }); }; diff --git a/src/actions/getInternalData.js b/src/actions/getInternalData.js index 2f59ac7d3..5e6665c57 100644 --- a/src/actions/getInternalData.js +++ b/src/actions/getInternalData.js @@ -2,7 +2,7 @@ const Promise = require('bluebird'); const pick = require('lodash/pick'); const { User } = require('../model/usermodel'); -const { ModelError } = require('../model/modelError'); +const { httpErrorMapper } = require('../model/modelError'); module.exports = function internalData(message) { const { fields } = message; @@ -13,5 +13,5 @@ module.exports = function internalData(message) { .then(data => { return fields ? pick(data, fields) : data; }) - .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); + .catch(e => { throw httpErrorMapper(e); }); }; diff --git a/src/actions/getMetadata.js b/src/actions/getMetadata.js index 07ab3e065..5ad8e41b4 100644 --- a/src/actions/getMetadata.js +++ b/src/actions/getMetadata.js @@ -1,6 +1,6 @@ const Promise = require('bluebird'); const { User } = require('../model/usermodel'); -const { ModelError } = require('../model/modelError'); +const { httpErrorMapper } = require('../model/modelError'); module.exports = function getMetadataAction(message) { const { audience, username, fields } = message; @@ -10,5 +10,5 @@ module.exports = function getMetadataAction(message) { .then(User.getUsername) .then(realUsername => [realUsername, audience, fields, message.public]) .spread(User.getMeta) - .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); + .catch(e => { throw httpErrorMapper(e); }); }; diff --git a/src/actions/list.js b/src/actions/list.js index 1ae9a25cf..1c8696f78 100644 --- a/src/actions/list.js +++ b/src/actions/list.js @@ -1,11 +1,11 @@ const Promise = require('bluebird'); const { User } = require('../model/usermodel'); -const { ModelError } = require('../model/modelError'); +const { httpErrorMapper } = require('../model/modelError'); module.exports = function iterateOverActiveUsers(opts) { return Promise .bind(this, opts) .then(User.getList) - .catch(e => { throw (e instanceof ModelError ? e : e.mapToHttp); }); + .catch(e => { throw httpErrorMapper(e); }); }; diff --git a/src/actions/login.js b/src/actions/login.js index 6fbedd2a8..f4b51ddb5 100644 --- a/src/actions/login.js +++ b/src/actions/login.js @@ -2,9 +2,10 @@ const Promise = require('bluebird'); const scrypt = require('../utils/scrypt.js'); const jwt = require('../utils/jwt.js'); const noop = require('lodash/noop'); - -const Users = require('../db/adapter'); - +const isActive = require('../utils/isActive'); +const isBanned = require('../utils/isBanned'); +const { User, Attempts } = require('../model/usermodel'); +const { httpErrorMapper } = require('../model/modelError'); module.exports = function login(opts) { const config = this.config.jwt; @@ -25,21 +26,21 @@ module.exports = function login(opts) { function enrichError(err) { if (remoteip) { - err.loginAttempts = Users.getAttempts(); + err.loginAttempts = Attempts.count(); } - throw err; + return err; } return Promise .bind(this, opts.username) - .then(Users.getUser) + .then(User.getOne) .then(data => [data, remoteip]) - .tap(verifyIp ? Users.checkLoginAttempts : noop) + .tap(verifyIp ? Attempts.check : noop) .tap(verifyHash) - .tap(verifyIp ? Users.dropAttempts : noop) - .tap(Users.isActive) - .tap(Users.isBanned) + .tap(verifyIp ? Attempts.drop : noop) + .tap(isActive) + .tap(isBanned) .then(getUserInfo) - .catch(verifyIp ? enrichError : e => { throw e; }); + .catch(e => { throw httpErrorMapper(verifyIp ? enrichError(e) : e); }); }; diff --git a/src/actions/remove.js b/src/actions/remove.js index 32585e1d8..710d9716b 100644 --- a/src/actions/remove.js +++ b/src/actions/remove.js @@ -1,21 +1,23 @@ const Promise = require('bluebird'); -const Errors = require('common-errors'); const { USERS_ADMIN_ROLE } = require('../constants'); -const Users = require('../db/adapter'); +const { User } = require('../model/usermodel'); +const { ModelError, httpErrorMapper, ERR_ADMIN_IS_UNTOUCHABLE } = require('../model/modelError'); + module.exports = function removeUser({ username }) { const audience = this.config.jwt.defaultAudience; return Promise.props({ - internal: Users.getUser(username), - meta: Users.getMetadata(username, audience), + internal: User.getOne.call(this, username), + meta: User.getMeta.call(this, username, audience), }) .then(({ internal, meta }) => { const isAdmin = (meta[audience].roles || []).indexOf(USERS_ADMIN_ROLE) >= 0; if (isAdmin) { - throw new Errors.HttpStatusError(400, 'can\'t remove admin user from the system'); + throw new ModelError(ERR_ADMIN_IS_UNTOUCHABLE); } - return Users.removeUser(username, internal); - }); + return User.remove.call(this, username, internal); + }) + .catch(e => { throw httpErrorMapper(e); }); }; diff --git a/src/actions/requestPassword.js b/src/actions/requestPassword.js index c47418bf6..7b216aef5 100644 --- a/src/actions/requestPassword.js +++ b/src/actions/requestPassword.js @@ -1,6 +1,10 @@ const Promise = require('bluebird'); const emailValidation = require('../utils/send-email.js'); -const Users = require('../db/adapter'); +const isActive = require('../utils/isActive'); +const isBanned = require('../utils/isBanned'); +const { User } = require('../model/usermodel'); +const { httpErrorMapper } = require('../model/modelError'); + module.exports = function requestPassword(opts) { const { username, generateNewPassword } = opts; @@ -11,9 +15,10 @@ module.exports = function requestPassword(opts) { return Promise .bind(this, username) - .then(Users.getUser) - .tap(Users.isActive) - .tap(Users.isBanned) + .then(User.getOne) + .tap(isActive) + .tap(isBanned) .then(() => emailValidation.send.call(this, username, action)) - .return({ success: true }); + .return({ success: true }) + .catch(e => { throw httpErrorMapper(e); }); }; diff --git a/src/actions/updateMetadata.js b/src/actions/updateMetadata.js index 1e791a6ed..8e7675508 100644 --- a/src/actions/updateMetadata.js +++ b/src/actions/updateMetadata.js @@ -1,10 +1,12 @@ const Promise = require('bluebird'); -const Users = require('../db/adapter'); +const { User } = require('../model/usermodel'); +const { httpErrorMapper } = require('../model/modelError'); module.exports = function updateMetadataAction(message) { return Promise .bind(this, message.username) - .then(Users.isExists) + .then(User.getUsername) .then(username => ({ ...message, username })) - .then(Users.updateMetadata); + .then(message.script ? User.executeUpdateMetaScript : User.setMeta) + .catch(e => { throw httpErrorMapper(e); }); }; diff --git a/src/actions/updatePassword.js b/src/actions/updatePassword.js index c6deb7ee5..40819ed93 100644 --- a/src/actions/updatePassword.js +++ b/src/actions/updatePassword.js @@ -2,7 +2,13 @@ const Promise = require('bluebird'); const scrypt = require('../utils/scrypt.js'); const jwt = require('../utils/jwt.js'); const emailChallenge = require('../utils/send-email.js'); -const Users = require('../db/adapter'); + +const isActive = require('../utils/isActive'); +const isBanned = require('../utils/isBanned'); + +const { User, Attempts } = require('../model/usermodel'); +const { httpErrorMapper } = require('../model/modelError'); + /** * Verifies token and deletes it if it matches @@ -20,11 +26,12 @@ function tokenReset(token) { function usernamePasswordReset(username, password) { return Promise .bind(this, username) - .then(Users.getUser) - .tap(Users.isActive) - .tap(Users.isBanned) + .then(User.getOne) + .tap(isActive) + .tap(isBanned) .tap(data => scrypt.verify(data.password, password)) - .return(username); + .return(username) + .catch(e => { throw httpErrorMapper(e); }); } /** @@ -35,12 +42,13 @@ function usernamePasswordReset(username, password) { function setPassword(_username, password) { return Promise .bind(this, _username) - .then(Users.isExists) + .then(User.getUsername) .then(username => Promise.props({ username, hash: scrypt.hash(password), })) - .then(Users.setPassword); + .then(User.setPassword) + .catch(e => { throw httpErrorMapper(e); }); } module.exports = exports = function updatePassword(opts) { @@ -65,7 +73,7 @@ module.exports = exports = function updatePassword(opts) { if (remoteip) { promise = promise.tap(function resetLock(username) { - return Users.resetIPLock(username, remoteip); + return Attempts.drop(username, remoteip); }); } diff --git a/src/actions/verify.js b/src/actions/verify.js index 0487edc59..f0d4eaad9 100644 --- a/src/actions/verify.js +++ b/src/actions/verify.js @@ -1,6 +1,7 @@ const Promise = require('bluebird'); const jwt = require('../utils/jwt.js'); -const Users = require('../db/adapter'); +const { User } = require('../model/usermodel'); +const { httpErrorMapper } = require('../model/modelError'); /** * Verifies that passed token is signed correctly, returns associated metadata with it @@ -25,7 +26,8 @@ module.exports = function verify(opts) { const username = decoded.username; return Promise.props({ username, - metadata: Users.getMetadata(username, audience), + metadata: User.getMeta.call(this, username, audience), }); - }); + }) + .catch(e => { throw httpErrorMapper(e); }); }; diff --git a/src/db/adapter.js b/src/db/adapter.js deleted file mode 100644 index 291aab779..000000000 --- a/src/db/adapter.js +++ /dev/null @@ -1,307 +0,0 @@ -/** - * Created by Stainwoortsel on 30.05.2016. - */ -const RedisStorage = require('./redisstorage'); -const Errors = require('common-errors'); - -class Users { - constructor(adapter) { - this.adapter = adapter; - } - - /** - * Lock user - * @param username - * @param reason - * @param whom - * @param remoteip - * @returns {Redis} - */ - lockUser({ username, reason, whom, remoteip }) { - return this.adapter.lockUser({ username, reason, whom, remoteip }); - } - - /** - * Unlock user - * @param username - * @returns {Redis} - */ - unlockUser(username) { - return this.adapter.unlockUser(username); - } - - /** - * Check existance of user - * @param username - * @returns {Redis} - */ - isExists(username) { - return this.adapter.isExists(username); - } - - aliasAlreadyExists(alias, thunk) { - return this.adapter.aliasAlreadyExists(alias, thunk); - } - - /** - * User is public - * @param username - * @param audience - * @returns {function()} - */ - isPublic(username, audience) { - return this.adapter.isPublic(username, audience); - } - - /** - * Check that user is active - * @param data - * @returns {boolean} - */ - isActive(data) { - return this.adapter.isActive(data); - } - - /** - * Check that user is banned - * @param data - * @returns {Promise} - */ - isBanned(data) { - return this.adapter.isBanned(data); - } - - /** - * Activate user account - * @param user - * @returns {Redis} - */ - activateAccount(user) { - return this.adapter.activateAccount(user); - } - - /** - * Get user internal data - * @param username - * @returns {Object} - */ - getUser(username) { - return this.adapter.getUser(username); - } - - /** - * Get users metadata by username and audience - * @param username - * @param audience - * @returns {Object} - */ - - getMetadata(username, _audiences, fields = {}) { - return this.adapter.getMetadata(username, _audiences, fields); - } - - - /** - * Return the list of users by specified params - * @param opts - * @returns {Array} - */ - getList(opts) { - return this.adapter.getList(opts); - } - - /** - * Check that user is admin - * @param meta - * @returns {boolean} - */ - isAdmin(meta) { - return this.adapter.isAdmin(meta); - } - - /** - * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN - * @param username - * @param alias - * @returns {Redis} - */ - storeAlias(username, alias) { - return this.adapter.storeAlias(username, alias); - } - - /** - * Assign alias to the user record, marked by username - * @param username - * @param alias - * @returns {Redis} - */ - assignAlias(username, alias) { - return this.adapter.assignAlias(username, alias); - } - - /** - * Return current login attempts count - * @returns {int} - */ - getAttempts() { - return this.adapter.getAttempts(); - } - - /** - * Drop login attempts counter - * @returns {Redis} - */ - dropAttempts() { - return this.adapter.dropAttempts(); - } - - /** - * Check login attempts - * @param data - * @returns {Redis} - */ - checkLoginAttempts(data) { - return this.adapter.checkLoginAttempts(data); - } - - /** - * Set user password - * @param username - * @param hash - * @returns {Redis} - */ - setPassword(username, hash) { - return this.adapter.setPassword(username, hash); - } - - /** - * Reset the lock by IP - * @param username - * @param ip - * @returns {Redis} - */ - resetIPLock(username, ip) { - return this.adapter.resetIPLock(username, ip); - } - - /** - * - * @param username - * @param audience - * @param metadata - * @returns {Object} - */ - updateMetadata({ username, audience, metadata }) { - return this.adapter.updateMetadata({ username, audience, metadata }); - } - - /** - * Removing user by username (and data?) - * @param username - * @param data - * @returns {Redis} - */ - removeUser(username, data) { - return this.adapter.removeUser(username, data); - } - - /** - * Verify ip limits - * @param {redisCluster} redis - * @param {Object} registrationLimits - * @param {String} ipaddress - * @return {Function} - */ - checkLimits(registrationLimits, ipaddress) { - return this.adapter.checkLimits(registrationLimits, ipaddress); - } - - /** - * Creates user with a given hash - * @param redis - * @param username - * @param activate - * @param deleteInactiveAccounts - * @param userDataKey - * @returns {Function} - */ - createUser(username, activate, deleteInactiveAccounts) { - return this.adapter.createUser(username, activate, deleteInactiveAccounts); - } - - /** - * Performs captcha check, returns thukn - * @param {String} username - * @param {String} captcha - * @param {Object} captchaConfig - * @return {Function} - */ - checkCaptcha(username, captcha) { - return this.adapter.checkCaptcha(username, captcha); - } - - /** - * Stores username to the index set - * @param username - * @returns {Redis} - */ - storeUsername(username) { - return this.adapter.storeUsername(username); - } - - /** - * Running a custom script or query - * @param script - * @returns {*|Promise} - */ - - customScript(script) { - return this.adapter.customScript(script); - } - - /** - * Store user token - * @param username - * @param token - * @returns {Redis} - */ - addToken(username, token) { - return this.adapter.addToken(username, token); - } - - /** - * Drop user token - * @param username - * @param token - * @returns {Redis} - */ - dropToken(username, token) { - return this.adapter.dropToken(username, token); - } - - /** - * Check the last access - * @param username - * @param token - * @returns {Redis} - */ - lastAccess(username, token) { - return this.adapter.lastAccess(username, token); - } - - /** - * The error wrapper for the front-level HTTP output - * @param e - */ - static mapErrors(e) { - const err = new Errors.HttpStatusError(e.status_code || 500, e.message); - if (err.status_code >= 500) { - err.message = Errors.HttpStatusError.message_map[500]; // hide the real error from the user - } - } - -} - -module.exports = function modelCreator() { - return new Users(RedisStorage); -}; diff --git a/src/db/mysqlstorage.js b/src/db/mysqlstorage.js deleted file mode 100644 index 36318b7d6..000000000 --- a/src/db/mysqlstorage.js +++ /dev/null @@ -1,442 +0,0 @@ -/** - * Created by Stainwoortsel on 14.06.2016. - */ -let loginAttempts; - -/** - * Generate hash key string - * @param args - * @returns {string} - */ -const generateKey = (...args) => { - const SEPARATOR = '!'; - return args.join(SEPARATOR); -}; - -module.exports = { - /** - * Lock user - * @param username - * @param reason - * @param whom - * @param remoteip - * @returns {Redis|{index: number, input: string}} - */ - lockUser({ username, reason, whom, remoteip }) { - /* - SET @aud = '*.localhost'; - SELECT @id=id FROM users WHERE username=:username; - UPDATE users SET isbanned=1 WHERE id=@id; - INSERT INTO user_meta (user_id, audience, k, v) - SELECT @id, @aud, 'banned', 'true' UNION - SELECT @id, @aud, 'banned:reason', :reason UNION - SELECT @id, @aud, 'banned:whom', :whom UNION - SELECT @id, @aud, 'banned:remoteip', :remoteip; - DELETE FROM user_tokens WHERE user_id=@id; - */ - }, - - /** - * Unlock user - * @param username - * @returns {Redis|{index: number, input: string}} - */ - unlockUser({ username }) { - /* - SET @aud = '*.localhost'; - SELECT @id=id FROM users WHERE username=:username; - DELETE FROM user_meta WHERE user_id=@id and k like 'banned%'; - // селект для наглядности, а вообще городят DELETE c 2 таблицами - */ - }, - - /** - * Check existance of user - * @param username - * @returns {Redis|username} - */ - isExists(username) { //ПЕРЕИМЕНОВАТЬ скорее fetchUsername resolve - /* - SELECT count(id) FROM users WHERE username=:username; - - return username | throw new Errors.HttpStatusError(404, `"${username}" does not exists`); - */ - }, - - /** - * Check the existance of alias - * @param alias - * @returns {username|''} - */ - aliasAlreadyExists(alias) { // УДАЛИТЬ в логику регистрации или setAlias который и юзать - /* - SELECT (SELECT count(id) FROM users WHERE alias=:alias) + - (SELECT count(user_id) FROM user_alias WHERE alias=:alias); - - return '' | throw new Errors.HttpStatusError(409, `"${alias}" already exists`); ??? почему пустая строка? - */ - }, - - /** - * User is public - * @param username - * @param audience - * @returns {function()} - */ - isPublic(username, audience) { // УДАЛИТЬ на самом деле, этот метод проверяет по какому полю запрашиваются - // метадата. если это емайл то мы ему кукишь, ибо низзя по имэйлу спрашивать, то есть это - // элемент метода гет-метадата, а значит пихаем в логику getMetadata - /* - SELECT ispublic FROM users WHERE username=:username ????? - || - SELECT um.v - FROM user_meta um - JOIN users u ON u.id=um.user_id AND u.username=:username - WHERE - um.audience=:audience AND - um.k = 'alias' - - return v === username | throw new Errors.HttpStatusError(404, 'username was not found') - */ - }, - - - /** - * Check that user is active - * @param data - * @returns {Promise} - */ - isActive(data) { - /* - логика из даты, лучше такие методы запихнуть в утилиты адаптера которые просто будут кидать ошибки - */ - }, - - /** - * Check that user is banned - * @param data - * @returns {Promise} - */ - isBanned(data) { - /* - см выше - */ - }, - - /** - * Activate user account - * @param user - * @returns {Redis} - */ - activateAccount(username) { - /* - UPDATE users SET isactive=1 WHERE username=:username; - достаточно просто апдейта, не надо страдать фигней - можно ошибку кидать по кол-ву обработанных строчек - если 0 то уже активировали - - - || - SELECT @id=id, @active=isactive FROM users WHERE username=:username; - // проверяем есть ли пользователь и активирован ли он? - UPDATE users SET isactive=1 WHERE id=@id; - // throw new Errors.HttpStatusError(417, `Account ${user} was already activated`); нужна ли проверка?.. - */ - }, - - /** - * Get user internal data - * @param username - * @returns {Object} - */ - getUser(username) { - /* - SELECT @id=id, ..... FROM users WHERE username=:username OR alias=:username; - // возможно нужна проверка по таблице user_aliases - // SELECT audience, k, v FROM user_meta WHERE user_id=@id; - */ - }, - - _getMeta(username, audience) { - /* - SELECT audience, k, v - FROM user_meta - JOIN - WHERE user_id=@id; - */ - }, - _remapMeta(data, audiences, fields) { - // ... - // сериализацию десериализацию можно оставить во вне, она скорее всего будет одинаковой - }, - - - /** - * Get users metadata by username and audience - * @param username - * @param audience - * @param fields - * @returns {Object} - */ - getMetadata(username, _audiences, fields = {}) { - /* - _getMeta - _remapData - */ - }, - - - /** - * Return the list of users by specified params - * @param opts - * @returns {Array} - */ - getList(opts) { - /* - - fsort -- это и селект и сортировка и всё вместе - - - - SELECT - FROM users, user_meta - - WHERE :criteria - - ORDER BY :order - LIMIT :offset, :limit - - // здесь возможен селект с join-ами на user_meta, - // если критерии выборки будут в полях таблицы user_meta - */ - }, - - /** - * Check that user is admin - * @param meta - * @returns {boolean} - */ - isAdmin(meta) { - /* - это выносим в утилиты, работает по метаданным - */ - }, - - /** - * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN - * @param username - * @param alias - * @returns {Redis} - */ - storeAlias(username, alias) { - /* - сделать метод setAlias - - INSERT INTO user_aliases(user_id, alias) - SELECT id, :alias - FROM users - WHERE username=:username; - */ - }, - - /** - * Assign alias to the user record, marked by username - * @param username - * @param alias - * @returns {Redis} - */ - assignAlias(username, alias) { - /* - сделать метод setAlias - - - SElECT @id = id FROM users WHERE username=:username; - UPDATE users SET alias=:alias, ispublic=1 WHERE id=@id; - UPDATE user_meta SET v = :alias WHERE user_id=@id AND audience='*.localhost' AND k='alias' - */ - }, - - /** - * Return current login attempts count - * @returns {int} - */ - getAttempts() { - return loginAttempts; - }, - /** - * Drop login attempts counter - * @returns {Redis} - */ - dropAttempts() { - loginAttempts = 0; - /* - DELETE FROM login_attempts WHERE ip=:ip - */ - }, - - /** - * Check login attempts - * @param data - * @param _remoteip - * @returns {Redis} - */ - checkLoginAttempts(data, _remoteip) { - /* - SELECT attempts, lastattempt FROM login_attempts WHERE ip=:ip AND username=:username; - - // здесь смотрим на кол-во попыток и на expire, само собой expire удаляем ВРУЧНУЮ! - - UPDATE login_attempts - SET attempts=attempts + 1, lastattempt=NOW() - WHERE ip=:ip AND username=:username; - - */ - }, - - /** - * Set user password - * @param username - * @param hash - * @returns {Redis} - */ - setPassword({ username, hash }) { - /* - UPDATE users SET pass=:hash WHERE username=:username - */ - }, - - /** - * Reset the lock by IP - * @param username - * @param ip - * @returns {Redis} - */ - resetIPLock(username, ip) { - /* - DELETE FROM login_attempts WHERE ip=:id AND username=:username - */ - }, - - - /** - * Process metadata update operation for a passed audience / inner method - * @param {Object} pipeline - * @param {String} audience - * @param {Object} metadata - */ - _handleAudience(pipeline, key, metadata) { - // ... updating user_meta, да это чисто внутренний метод, в sql он будет другой - }, - - - /** - * - * @param username - * @param audience - * @param metadata - * @returns {Object} - */ - updateMetadata({ username, audience, metadata, script }) { - // ... updating user_meta - // скрипт вынести за логику апдейта, толкьо для эдванст-юзыч - }, - - /** - * Removing user by username (and data?) - * @param username - * @param data - * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} - */ - removeUser(username, data) { - /* - SELECT @id = id FROM users WHERE username=:username; - DELETE FROM users WHERE id=@id; - DELETE FROM user_aliases WHERE user_id=@id; - DELETE FROM user_meta WHERE user_id=@id; - DELETE FROM user_tokens WHERE user_id=@id; - DELETE FROM login_attempts WHERE username=:username; ?? - */ - }, - - /** - * Verify ip limits - * @param {redisCluster} redis - * @param {Object} registrationLimits - * @param {String} ipaddress - * @return {Function} - */ - checkLimits(ipaddress) { - /* - INSERT INTO register2ip (ip, uuid, registered) - SELECT :ip, :uuid, NOW(); - - SELECT * FROM register2ip WHERE ip=:ipaddress - - DELETE FROM register2ip WHERE ip=:ipaddress AND registered < :old - - SELECT count(*) FROM register2ip WHERE ip=:ipaddress - - // проверка на ограничения - */ - }, - - /** - * Creates user with a given hash - * @param redis - * @param username - * @param activate - * @param userDataKey - * @returns {Function} - */ - createUser(username, activate) { - /* - можно сразу с алиасом - т.е. обеспечить атомарность - - проверить на наличие пользователя - - INSERT INTO users (username, isactive, registered) - SELECT :username, :activate, NOW(); - - // проверка на существование - */ - }, - - /** - * Performs captcha check, returns thukn - * @param {String} username - * @param {String} captcha - * @return {Function} - */ - checkCaptcha(username, captcha) { - // ... - }, - - /** - * Stores username to the index set - * @param username - * @returns {Redis} - */ - storeUsername(username) { - // по сути метод не нужен - // это все из логики регистрации - // а вообще это логика активации пользователя, - // просто при регистрации не нужен персист, ибо сразу, а для активации персист нуежн - // то есть если я его лишний раз вызову, ничего страшного - // это намек на отдельный большой метод activateUser для регистрации и активации - // ... у users в РСУБД есть поле ID, этот метод будет пустым - }, - - /** - * Execute custom script on LUA - * @param script - * @returns {Promise} - */ - customScript(script, keys) { - // ... - }, - - handleAudience(key, metadata) { - // updating user_meta - } -}; diff --git a/src/db/redisstorage.js b/src/db/redisstorage.js deleted file mode 100644 index 00e8b192f..000000000 --- a/src/db/redisstorage.js +++ /dev/null @@ -1,721 +0,0 @@ -/** - * Created by Stainwoortsel on 30.05.2016. - */ -const Promise = require('bluebird'); -const Errors = require('common-errors'); -const mapValues = require('lodash/mapValues'); -const get = require('lodash/get'); -const pick = require('lodash/pick'); -const uuid = require('node-uuid'); -const is = require('is'); -const sha256 = require('../utils/sha256.js'); -const moment = require('moment'); -const verifyGoogleCaptcha = require('../utils/verifyGoogleCaptcha'); -const mapMetaResponse = require('../utils/mapMetaResponse'); - -// JSON -const JSONStringify = JSON.stringify.bind(JSON); -const JSONParse = JSON.parse.bind(JSON); - -// constants -const { - USERS_DATA, USERS_METADATA, USERS_ALIAS_TO_LOGIN, - USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA, - USERS_ACTIVE_FLAG, USERS_INDEX, USERS_PUBLIC_INDEX, - USERS_ALIAS_FIELD, USERS_ADMIN_ROLE, -} = require('../constants.js'); - -// config's and base objects -const { redis, captcha: captchaConfig, config } = this; -const { deleteInactiveAccounts, jwt: { lockAfterAttempts, defaultAudience, hashingFunction: { ttl } } } = config; - - -// local vatiables inside the module -let remoteipKey; -let loginAttempts; - - -/** - * Generate hash key string - * @param args - * @returns {string} - */ -const generateKey = (...args) => { - const SEPARATOR = '!'; - return args.join(SEPARATOR); -}; - -module.exports = { - /** - * Lock user - * @param username - * @param reason - * @param whom - * @param remoteip - * @returns {Redis|{index: number, input: string}} - */ - lockUser({ username, reason, whom, remoteip }) { - const data = { - banned: true, - [USERS_BANNED_DATA]: { - reason, - whom, - remoteip, - }, - }; - - return redis - .pipeline() - .hset(generateKey(username, USERS_DATA), USERS_BANNED_FLAG, 'true') - // set .banned on metadata for filtering & sorting users by that field - .hmset(generateKey(username, USERS_METADATA, defaultAudience), mapValues(data, JSONStringify)) - .del(generateKey(username, USERS_TOKENS)) - .exec(); - }, - - /** - * Unlock user - * @param username - * @returns {Redis|{index: number, input: string}} - */ - unlockUser({ username }) { - return redis - .pipeline() - .hdel(generateKey(username, USERS_DATA), USERS_BANNED_FLAG) - // remove .banned on metadata for filtering & sorting users by that field - .hdel(generateKey(username, USERS_METADATA, defaultAudience), 'banned', USERS_BANNED_DATA) - .exec(); - }, - - /** - * Check existance of user - * @param username - * @returns {Redis|username} - */ - isExists(username) { - return redis - .pipeline() - .hget(USERS_ALIAS_TO_LOGIN, username) - .exists(generateKey(username, USERS_DATA)) - .exec() - .spread((alias, exists) => { - if (alias[1]) { - return alias[1]; - } - - if (!exists[1]) { - throw new Errors.HttpStatusError(404, `"${username}" does not exists`); - } - - return username; - }); - }, - - /** - * Check the existance of alias - * @param alias - * @returns {username|''} - */ - aliasAlreadyExists(alias) { - return redis - .hget(USERS_ALIAS_TO_LOGIN, alias) - .then(username => { - if (username) { - throw new Errors.HttpStatusError(409, `"${alias}" already exists`); - } - - return username; - }); - }, - - /** - * User is public - * @param username - * @param audience - * @returns {function()} - */ - isPublic(username, audience) { - return metadata => { - if (get(metadata, [audience, USERS_ALIAS_FIELD]) === username) { - return; - } - - throw new Errors.HttpStatusError(404, 'username was not found'); - }; - }, - - - /** - * Check that user is active - * @param data - * @returns {Promise} - */ - isActive(data) { - if (String(data[USERS_ACTIVE_FLAG]) !== 'true') { - return Promise.reject(new Errors.HttpStatusError(412, 'Account hasn\'t been activated')); - } - - return Promise.resolve(data); - }, - - /** - * Check that user is banned - * @param data - * @returns {Promise} - */ - isBanned(data) { - if (String(data[USERS_BANNED_FLAG]) === 'true') { - return Promise.reject(new Errors.HttpStatusError(423, 'Account has been locked')); - } - - return Promise.resolve(data); - }, - - /** - * Activate user account - * @param user - * @returns {Redis} - */ - activateAccount(user) { - const userKey = generateKey(user, USERS_DATA); - - // WARNING: `persist` is very important, otherwise we will lose user's information in 30 days - // set to active & persist - return redis - .pipeline() - .hget(userKey, USERS_ACTIVE_FLAG) - .hset(userKey, USERS_ACTIVE_FLAG, 'true') - .persist(userKey) - .sadd(USERS_INDEX, user) - .exec() - .spread(function pipeResponse(isActive) { - const status = isActive[1]; - if (status === 'true') { - throw new Errors.HttpStatusError(417, `Account ${user} was already activated`); - } - }); - }, - - /** - * Get user internal data - * @param username - * @returns {Object} - */ - getUser(username) { - const userKey = generateKey(username, USERS_DATA); - - return redis - .pipeline() - .hget(USERS_ALIAS_TO_LOGIN, username) - .exists(userKey) - .hgetallBuffer(userKey) - .exec() - .spread((aliasToUsername, exists, data) => { - if (aliasToUsername[1]) { - return this.getUser(aliasToUsername[1]); - } - - if (!exists[1]) { - throw new Errors.HttpStatusError(404, `"${username}" does not exists`); - } - - return { ...data[1], username }; - }); - }, - - _getMeta(username, audience) { - return redis.hgetallBuffer(generateKey(username, USERS_METADATA, audience)); - }, - _remapMeta(data, audiences, fields) { - const output = {}; - audiences.forEach(function transform(aud, idx) { - const datum = data[idx]; - - if (datum) { - const pickFields = fields[aud]; - output[aud] = mapValues(datum, JSONParse); - if (pickFields) { - output[aud] = pick(output[aud], pickFields); - } - } else { - output[aud] = {}; - } - }); - - return output; - }, - - - /** - * Get users metadata by username and audience - * @param username - * @param audience - * @param fields - * @returns {Object} - */ - getMetadata(username, _audiences, fields = {}) { - const audiences = Array.isArray(_audiences) ? _audiences : [_audiences]; - - return Promise - .map(audiences, audience => { - return this._getMeta(username, audience); - }) - .then(data => { - return this._remapMeta(data, audiences, fields); - }); - }, - - - /** - * Return the list of users by specified params - * @param opts - * @returns {Array} - */ - getList(opts) { - const { criteria, audience, index, strFilter, order, offset, limit } = opts; - const metaKey = generateKey('*', USERS_METADATA, audience); - - return redis - .fsort(index, metaKey, criteria, order, strFilter, offset, limit) - .then(ids => { - const length = +ids.pop(); - if (length === 0 || ids.length === 0) { - return [ - ids || [], - [], - length, - ]; - }o - - const pipeline = redis.pipeline(); - ids.forEach(id => { - pipeline.hgetallBuffer(generateKey(id, USERS_METADATA, audience)); - }); - return Promise.all([ - ids, - pipeline.exec(), - length, - ]); - }) - .spread((ids, props, length) => { - const users = ids.map(function remapData(id, idx) { - const data = props[idx][1]; - const account = { - id, - metadata: { - [audience]: data ? mapValues(data, JSONParse) : {}, - }, - }; - - return account; - }); - - return { - users, - cursor: offset + limit, - page: Math.floor(offset / limit + 1), - pages: Math.ceil(length / limit), - }; - }); - }, - - /** - * Check that user is admin - * @param meta - * @returns {boolean} - */ - isAdmin(meta) { - const audience = config.jwt.defaultAudience; - return (meta[audience].roles || []).indexOf(USERS_ADMIN_ROLE) >= 0; - }, - - /** - * Make the linkage between username and alias into the USERS_ALIAS_TO_LOGIN - * @param username - * @param alias - * @returns {Redis} - */ - storeAlias(username, alias) { - return redis.hsetnx(USERS_ALIAS_TO_LOGIN, alias, username); - }, - - /** - * Assign alias to the user record, marked by username - * @param username - * @param alias - * @returns {Redis} - */ - assignAlias(username, alias) { - return redis - .pipeline() - .sadd(USERS_PUBLIC_INDEX, username) - .hset(generateKey(username, USERS_DATA), USERS_ALIAS_FIELD, alias) - .hset(generateKey(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, JSONStringify) - .exec(); - }, - - /** - * Return current login attempts count - * @returns {int} - */ - getAttempts() { - return loginAttempts; - }, - - /** - * Drop login attempts counter - * @returns {Redis} - */ - dropAttempts() { - loginAttempts = 0; - if (remoteipKey) { - return redis.del(remoteipKey); - } - - throw new Errors.Error('Empty remote ip key'); - }, - - /** - * Check login attempts - * @param data - * @param _remoteip - * @returns {Redis} - */ - checkLoginAttempts(data, _remoteip) { - const pipeline = redis.pipeline(); - const { username } = data; - remoteipKey = generateKey(username, 'ip', _remoteip); - - pipeline.incrby(remoteipKey, 1); - if (config.jwt.keepLoginAttempts > 0) { - pipeline.expire(remoteipKey, config.jwt.keepLoginAttempts); - } - - return pipeline - .exec() - .spread(function incremented(incrementValue) { - const err = incrementValue[0]; - if (err) { - throw new Errors.data.RedisError(err); - } - - loginAttempts = incrementValue[1]; - if (loginAttempts > lockAfterAttempts) { - const duration = moment().add(config.jwt.keepLoginAttempts, 'seconds').toNow(true); - const msg = `You are locked from making login attempts for the next ${duration}`; - throw new Errors.HttpStatusError(429, msg); - } - }); - }, - - /** - * Set user password - * @param username - * @param hash - * @returns {Redis} - */ - setPassword({ username, hash }) { - return redis - .hset(generateKey(username, USERS_DATA), 'password', hash) - .return(username); - }, - - /** - * Reset the lock by IP - * @param username - * @param ip - * @returns {Redis} - */ - resetIPLock(username, ip) { - return redis.del(generateKey(username, 'ip', ip)); - }, - - - /** - * Process metadata update operation for a passed audience / inner method - * @param {Object} pipeline - * @param {String} audience - * @param {Object} metadata - */ - _handleAudience(pipeline, key, metadata) { - const $remove = metadata.$remove; - const $removeOps = $remove && $remove.length || 0; - if ($removeOps > 0) { - pipeline.hdel(key, $remove); - } - - const $set = metadata.$set; - const $setKeys = $set && Object.keys($set); - const $setLength = $setKeys && $setKeys.length || 0; - if ($setLength > 0) { - pipeline.hmset(key, mapValues($set, JSONStringify)); - } - - const $incr = metadata.$incr; - const $incrFields = $incr && Object.keys($incr); - const $incrLength = $incrFields && $incrFields.length || 0; - if ($incrLength > 0) { - $incrFields.forEach(fieldName => { - pipeline.hincrby(key, fieldName, $incr[fieldName]); - }); - } - - return { $removeOps, $setLength, $incrLength, $incrFields }; - }, - - - /** - * - * @param username - * @param audience - * @param metadata - * @returns {Object} - */ - updateMetadata({ username, audience, metadata, script }) { - const audiences = is.array(audience) ? audience : [audience]; - - // keys - const keys = audiences.map(aud => generateKey(username, USERS_METADATA, aud)); - - // if we have meta, then we can - if (metadata) { - const pipe = redis.pipeline(); - const metaOps = is.array(metadata) ? metadata : [metadata]; - const operations = metaOps.map((meta, idx) => this._handleAudience(pipe, keys[idx], meta)); - return pipe.exec().then(res => mapMetaResponse(operations, res)); - } - - // or... - return this.customScript(script, keys); - }, - - /** - * Removing user by username (and data?) - * @param username - * @param data - * @returns {*|{arity, flags, keyStart, keyStop, step}|Array|{index: number, input: string}} - */ - removeUser(username, data) { - const audience = config.jwt.defaultAudience; - const transaction = redis.multi(); - const alias = data[USERS_ALIAS_FIELD]; - if (alias) { - transaction.hdel(USERS_ALIAS_TO_LOGIN, alias); - } - - // clean indices - transaction.srem(USERS_PUBLIC_INDEX, username); - transaction.srem(USERS_INDEX, username); - - // remove metadata & internal data - transaction.del(generateKey(username, USERS_DATA)); - transaction.del(generateKey(username, USERS_METADATA, audience)); - - // remove auth tokens - transaction.del(generateKey(username, USERS_TOKENS)); - - // complete it - return transaction.exec(); - }, - - /** - * Verify ip limits - * @param {redisCluster} redis - * @param {Object} registrationLimits - * @param {String} ipaddress - * @return {Function} - */ - checkLimits(ipaddress) { - const { registrationLimits } = config; - const { ip: { time, times } } = registrationLimits; - const ipaddressLimitKey = generateKey('reg-limit', ipaddress); - const now = Date.now(); - const old = now - time; - - return function iplimits() { - return redis - .pipeline() - .zadd(ipaddressLimitKey, now, uuid.v4()) - .pexpire(ipaddressLimitKey, time) - .zremrangebyscore(ipaddressLimitKey, '-inf', old) - .zcard(ipaddressLimitKey) - .exec() - .then(props => { - const cardinality = props[3][1]; - if (cardinality > times) { - const msg = 'You can\'t register more users from your ipaddress now'; - throw new Errors.HttpStatusError(429, msg); - } - }); - }; - }, - - /** - * Creates user with a given hash - * @param redis - * @param username - * @param activate - * @param userDataKey - * @returns {Function} - */ - createUser(username, activate) { - /** - * Input from scrypt.hash - */ - const userDataKey = generateKey(username, USERS_DATA); - - return function create(hash) { - const pipeline = redis.pipeline(); - - pipeline.hsetnx(userDataKey, 'password', hash); - pipeline.hsetnx(userDataKey, USERS_ACTIVE_FLAG, activate); - - return pipeline - .exec() - .spread(function insertedUserData(passwordSetResponse) { - if (passwordSetResponse[1] === 0) { - throw new Errors.HttpStatusError(412, `User "${username}" already exists`); - } - - if (!activate && deleteInactiveAccounts >= 0) { - // WARNING: IF USER IS NOT VERIFIED WITHIN - // [by default 30] DAYS - IT WILL BE REMOVED FROM DATABASE - return redis.expire(userDataKey, deleteInactiveAccounts); - } - - return null; - }); - }; - }, - - /** - * Performs captcha check, returns thukn - * @param {String} username - * @param {String} captcha - * @return {Function} - */ - checkCaptcha(username, captcha) { - const { ttl } = captchaConfig; - return function checkTheCaptcha() { - const captchaCacheKey = captcha.response; - return redis - .pipeline() - .set(captchaCacheKey, username, 'EX', ttl, 'NX') - .get(captchaCacheKey) - .exec() - .spread(function captchaCacheResponse(setResponse, getResponse) { - if (getResponse[1] !== username) { - const msg = 'Captcha challenge you\'ve solved can not be used, please complete it again'; - throw new Errors.HttpStatusError(412, msg); - } - }) - .then(() => verifyGoogleCaptcha(captcha)); - }; - }, - - /** - * Stores username to the index set - * @param username - * @returns {Redis} - */ - storeUsername(username) { - return redis.sadd(USERS_INDEX, username); - }, - - /** - * Execute custom script on LUA - * @param script - * @returns {Promise} - */ - customScript(script, keys) { - // dynamic scripts - const $scriptKeys = Object.keys(script); - const scripts = $scriptKeys.map(scriptName => { - const { lua, argv = [] } = script[scriptName]; - const sha = sha256(lua); - const name = `ms_users_${sha}`; - if (!is.fn(redis[name])) { - redis.defineCommand(name, { lua }); - } - return redis[name](keys.length, keys, argv); - }); - - return Promise.all(scripts).then(res => { - const output = {}; - $scriptKeys.forEach((fieldName, idx) => { - output[fieldName] = res[idx]; - }); - return output; - }); - }, - - /** - * Store user token - * @param username - * @param token - * @returns {Redis} - */ - addToken(username, token) { - return redis.zadd(generateKey(username, USERS_TOKENS), Date.now(), token); - }, - - /** - * Drop user token - * @param username - * @param token - * @returns {Redis} - */ - dropToken(username, token) { - return token ? - redis.zrem(generateKey(username, USERS_TOKENS), token) : - redis.del(generateKey(username, USERS_TOKENS)); - }, - - /** - * Check the last access - * @param username - * @param token - * @returns {Redis} - */ - lastAccess(username, token) { - const tokensHolder = generateKey(username, USERS_TOKENS); - return redis.zscoreBuffer(tokensHolder, token).then(function getLastAccess(_score) { - // parseResponse - const score = parseInt(_score, 10); - - // throw if token not found or expired - if (isNaN(score) || Date.now() > score + ttl) { - throw new Errors.HttpStatusError(403, 'token has expired or was forged'); - } - - return score; - }); - }, - - handleAudience(key, metadata) { - const pipeline = redis.pipeline(); - const $remove = metadata.$remove; - const $removeOps = $remove && $remove.length || 0; - if ($removeOps > 0) { - pipeline.hdel(key, $remove); - } - - const $set = metadata.$set; - const $setKeys = $set && Object.keys($set); - const $setLength = $setKeys && $setKeys.length || 0; - if ($setLength > 0) { - pipeline.hmset(key, mapValues($set, JSONStringify)); - } - - const $incr = metadata.$incr; - const $incrFields = $incr && Object.keys($incr); - const $incrLength = $incrFields && $incrFields.length || 0; - if ($incrLength > 0) { - $incrFields.forEach(fieldName => { - pipeline.hincrby(key, fieldName, $incr[fieldName]); - }); - } - - return { $removeOps, $setLength, $incrLength, $incrFields }; - }, -}; diff --git a/src/model/modelError.js b/src/model/modelError.js index 8f7d30488..791901978 100644 --- a/src/model/modelError.js +++ b/src/model/modelError.js @@ -25,6 +25,9 @@ const mapErr = (e) => e.code; * Error types structure * @type {Object} */ + +const ERR_DEFAULT = genErr(0, 500, 'Internal error'); + const ErrorTypes = { ERR_ALIAS_ALREADY_ASSIGNED: genErr(100, 417, 'alias is already assigned'), @@ -62,7 +65,6 @@ const ErrorTypes = { genErr(151, 400, (hostname) => (`no MX record was found for hostname ${hostname}`)), ERR_EMAIL_ALREADY_SENT: genErr(152, 429, 'We\'ve already sent you an email, if it doesn\'t come - please try again in a little while or send us an email'), // eslint-disable-line - ERR_TOKEN_INVALID: genErr(160, 403, 'Invalid Token'), ERR_TOKEN_AUDIENCE_MISMATCH: @@ -97,29 +99,11 @@ const ErrorTypes = { */ const ErrorCodes = mapValues(ErrorTypes, mapErr); -/** - * Fabric for error classes with mapToHttp method, to map error into HttpStatusError - * @param name - * @param opts - * @returns {Class} HTTP-mapped error class - */ -const generateHttpMapedClass = function generateHttpMapedClass(name, opts) { - const Class = Errors.helpers.generateClass(name, opts); - - Class.prototype.mapToHttp = function mapToHttp() { - const key = findKey(ErrorTypes, { code: this.code }); - const err = ErrorTypes[key]; - return new Errors.HttpStatusError(err.http, this.generateMessage()); - }; - - return Class; -}; - /** * Model error class * @type {Class} */ -const ModelError = generateHttpMapedClass('ModelError', { +const ModelError = Errors.helpers.generateClass('ModelError', { extends: Errors.Error, args: ['code', 'data'], generateMessage: function generateMessage() { @@ -129,4 +113,33 @@ const ModelError = generateHttpMapedClass('ModelError', { }, }); -module.exports = { ...ErrorCodes, ModelError }; +/** + * Make http-map to the inner error (middleware) + * @param e + * @returns {*} + * @constructor + */ +const httpErrorMapper = function _HttpErrorMapper(e = null) { + const mapMEToHttp = function mapToHttp(_e) { + const key = findKey(ErrorTypes, { code: _e.code }); + const err = ErrorTypes[key]; + return new Errors.HttpStatusError(err.http, _e.generateMessage()); + }; + + if (e instanceof ModelError) { + return mapMEToHttp(e); + } + + if (e instanceof Errors.HttpStatusError) { + return e; + } + + if (e instanceof Errors.InvalidOperationError) { + return e; + } + + return new Errors.HttpStatusError(ERR_DEFAULT.http, ERR_DEFAULT.msg); +}; + + +module.exports = { ...ErrorCodes, ModelError, httpErrorMapper }; diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index afbae743a..655f04466 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -3,13 +3,18 @@ */ const Errors = require('common-errors'); const remapMeta = require('../../utils/remapMeta'); +const mapMetaResponse = require('../../utils/mapMetaResponse'); const mapValues = require('lodash/mapValues'); +const moment = require('moment'); +const sha256 = require('../../utils/sha256.js'); const fsort = require('redis-filtered-sort'); const noop = require('lodash/noop'); const get = require('lodash/get'); +const is = require('is'); const { ModelError, ERR_ALIAS_ALREADY_ASSIGNED, ERR_ALIAS_ALREADY_TAKEN, ERR_USERNAME_NOT_EXISTS, ERR_USERNAME_NOT_FOUND, + ERR_ATTEMPTS_LOCKED, ERR_TOKEN_FORGED, } = require('../modelError'); /* ERR_ALIAS_ALREADY_EXISTS, ERR_USERNAME_ALREADY_ACTIVE, @@ -17,8 +22,8 @@ const { ERR_ACCOUNT_NOT_ACTIVATED, ERR_ACCOUNT_ALREADY_ACTIVATED, ERR_ACCOUNT_IS_LOCKED, ERR_ACCOUNT_IS_ALREADY_EXISTS, ERR_ADMIN_IS_UNTOUCHABLE, ERR_CAPTCHA_WRONG_USERNAME, ERR_CAPTCHA_ERROR_RESPONSE, ERR_EMAIL_DISPOSABLE, ERR_EMAIL_NO_MX, ERR_EMAIL_ALREADY_SENT, ERR_TOKEN_INVALID, ERR_TOKEN_AUDIENCE_MISMATCH, ERR_TOKEN_MISS_EMAIL, - ERR_TOKEN_EXPIRED, ERR_TOKEN_FORGED, ERR_TOKEN_BAD_EMAIL, ERR_TOKEN_CANT_DECODE, ERR_PASSWORD_INVALID, - ERR_PASSWORD_INVALID_HASH, ERR_PASSWORD_INCORRECT, ERR_PASSWORD_SCRYPT_ERROR, ERR_ATTEMPTS_LOCKED, + ERR_TOKEN_EXPIRED, ERR_TOKEN_BAD_EMAIL, ERR_TOKEN_CANT_DECODE, ERR_PASSWORD_INVALID, + ERR_PASSWORD_INVALID_HASH, ERR_PASSWORD_INCORRECT, ERR_PASSWORD_SCRYPT_ERROR, ERR_ATTEMPTS_TO_MUCH_REGISTERED */ @@ -30,12 +35,8 @@ const JSONParse = JSON.parse.bind(JSON); const { USERS_DATA, USERS_METADATA, USERS_ALIAS_TO_LOGIN, USERS_ACTIVE_FLAG, USERS_INDEX, USERS_PUBLIC_INDEX, - USERS_ALIAS_FIELD, + USERS_ALIAS_FIELD, USERS_TOKENS, USERS_BANNED_FLAG, USERS_BANNED_DATA, } = require('../../constants'); -/* - USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA, - USERS_ADMIN_ROLE, - */ // config's and base objects const { redis, captcha: captchaConfig, config } = this; @@ -219,7 +220,43 @@ module.exports.User = { * @returns {String} username */ setPassword(username, hash) { - return this.adapter.setPassword(username, hash); + return redis + .hset(generateKey(username, USERS_DATA), 'password', hash) + .return(username); + }, + + + /** + * Process metadata update operation for a passed audience (inner method) + * @param {Object} pipeline + * @param {String} key (audience) + * @param {Object} metadata + * @returns {object} + */ + _handleAudience(pipeline, key, metadata) { + const $remove = metadata.$remove; + const $removeOps = $remove && $remove.length || 0; + if ($removeOps > 0) { + pipeline.hdel(key, $remove); + } + + const $set = metadata.$set; + const $setKeys = $set && Object.keys($set); + const $setLength = $setKeys && $setKeys.length || 0; + if ($setLength > 0) { + pipeline.hmset(key, mapValues($set, JSONStringify)); + } + + const $incr = metadata.$incr; + const $incrFields = $incr && Object.keys($incr); + const $incrLength = $incrFields && $incrFields.length || 0; + if ($incrLength > 0) { + $incrFields.forEach(fieldName => { + pipeline.hincrby(key, fieldName, $incr[fieldName]); + }); + } + + return { $removeOps, $setLength, $incrLength, $incrFields }; }, /** @@ -230,7 +267,46 @@ module.exports.User = { * @returns {Object} */ setMeta(username, audience, metadata) { - return this.adapter.setMeta(username, audience, metadata); + const audiences = is.array(audience) ? audience : [audience]; + const keys = audiences.map(aud => generateKey(username, USERS_METADATA, aud)); + + const pipe = redis.pipeline(); + const metaOps = is.array(metadata) ? metadata : [metadata]; + const operations = metaOps.map((meta, idx) => this._handleAudience(pipe, keys[idx], meta)); + return pipe.exec().then(res => mapMetaResponse(operations, res)); + }, + + /** + * Update meta of user by using direct script + * @param username + * @param audience + * @param script + * @returns {Object} + */ + executeUpdateMetaScript(username, audience, script) { + const audiences = is.array(audience) ? audience : [audience]; + const keys = audiences.map(aud => generateKey(username, USERS_METADATA, aud)); + + // dynamic scripts + const $scriptKeys = Object.keys(script); + const scripts = $scriptKeys.map(scriptName => { + const { lua, argv = [] } = script[scriptName]; + const sha = sha256(lua); + const name = `ms_users_${sha}`; + if (!is.fn(redis[name])) { + redis.defineCommand(name, { lua }); + } + return redis[name](keys.length, keys, argv); + }); + + // mapScriptResponse implementation + return Promise.all(scripts).then(res => { + const output = {}; + $scriptKeys.forEach((fieldName, idx) => { + output[fieldName] = res[idx]; + }); + return output; + }); }, /** @@ -252,7 +328,26 @@ module.exports.User = { * @returns {*} */ remove(username, data) { - return this.adapter.remove(username, data); + const audience = defaultAudience; + const transaction = redis.multi(); + const alias = data[USERS_ALIAS_FIELD]; + if (alias) { + transaction.hdel(USERS_ALIAS_TO_LOGIN, alias); + } + + // clean indices + transaction.srem(USERS_PUBLIC_INDEX, username); + transaction.srem(USERS_INDEX, username); + + // remove metadata & internal data + transaction.del(generateKey(username, USERS_DATA)); + transaction.del(generateKey(username, USERS_METADATA, audience)); + + // remove auth tokens + transaction.del(generateKey(username, USERS_TOKENS)); + + // complete it + return transaction.exec(); }, /** @@ -287,7 +382,23 @@ module.exports.User = { * @returns {*} */ lock(username, opts) { + const { reason, whom, remoteip } = opts; // to guarantee writing only those three variables to metadata from opts + const data = { + banned: true, + [USERS_BANNED_DATA]: { + reason: reason || '', + whom: whom || '', + remoteip: remoteip || '', + }, + }; + return redis + .pipeline() + .hset(generateKey(username, USERS_DATA), USERS_BANNED_FLAG, 'true') + // set .banned on metadata for filtering & sorting users by that field + .hmset(generateKey(username, USERS_METADATA, defaultAudience), mapValues(data, JSONStringify)) + .del(generateKey(username, USERS_TOKENS)) + .exec(); }, /** @@ -296,16 +407,42 @@ module.exports.User = { * @returns {*} */ unlock(username) { - + return redis + .pipeline() + .hdel(generateKey(username, USERS_DATA), USERS_BANNED_FLAG) + // remove .banned on metadata for filtering & sorting users by that field + .hdel(generateKey(username, USERS_METADATA, defaultAudience), 'banned', USERS_BANNED_DATA) + .exec(); }, }; let loginAttempts; module.exports.Attempts = { - check: function check(username, ip) { + check: function check({ username, ip }) { const ipKey = generateKey(username, 'ip', ip); - // ... + const pipeline = redis.pipeline(); + + pipeline.incrby(ipKey, 1); + if (config.keepLoginAttempts > 0) { + pipeline.expire(ipKey, config.keepLoginAttempts); + } + + return pipeline + .exec() + .spread(function incremented(incrementValue) { + const err = incrementValue[0]; + if (err) { + this.log.error('Redis error:', err); + return; + } + + loginAttempts = incrementValue[1]; + if (loginAttempts > lockAfterAttempts) { + const duration = moment().add(config.keepLoginAttempts, 'seconds').toNow(true); + throw new ModelError(ERR_ATTEMPTS_LOCKED, duration); + } + }); }, /** @@ -316,6 +453,7 @@ module.exports.Attempts = { */ drop: function drop(username, ip) { const ipKey = generateKey(username, 'ip', ip); + loginAttempts = 0; return redis.del(ipKey); }, @@ -329,6 +467,30 @@ module.exports.Attempts = { }; module.exports.Tokens = { + add(username, token) { + return redis.zadd(generateKey(username, USERS_TOKENS), Date.now(), token); + }, + + drop(username, token = null) { + return token ? + redis.zrem(generateKey(username, USERS_TOKENS), token) : + redis.del(generateKey(username, USERS_TOKENS)); + }, + + lastAccess(username, token) { + const tokensHolder = generateKey(username, USERS_TOKENS); + return redis.zscoreBuffer(tokensHolder, token).then(function getLastAccess(_score) { + // parseResponse + const score = parseInt(_score, 10); + + // throw if token not found or expired + if (isNaN(score) || Date.now() > score + ttl) { + throw new ModelError(ERR_TOKEN_FORGED); + } + + return score; + }); + }, }; diff --git a/src/model/usermodel.js b/src/model/usermodel.js index 3caf4d61b..395d13836 100644 --- a/src/model/usermodel.js +++ b/src/model/usermodel.js @@ -36,10 +36,11 @@ class UserModel { * @param username * @param audiences * @param fields + * @param _public * @returns {Object} */ - getMeta(username, audiences, fields = {}) { - return this.adapter.getMeta(username, audiences, fields); + getMeta(username, audiences, fields = {}, _public = null) { + return this.adapter.getMeta(username, audiences, fields, _public); } /** @@ -76,6 +77,17 @@ class UserModel { return this.adapter.setMeta(username, audience, metadata); } + /** + * Update meta of user by using direct script + * @param username + * @param audience + * @param script + * @returns {Object} + */ + executeUpdateMetaScript(username, audience, script) { + return this.adapter.executeUpdateMetaScript(username, audience, script); + } + /** * Create user account with alias and password * @param username @@ -128,13 +140,12 @@ class UserModel { } class AttemptsHelper { - constructor(adapter) { this.adapter = adapter; } - check(username, ip) { - return this.adapter.check(username, ip); + check({ username, ip }) { + return this.adapter.check({ username, ip }); } drop(username, ip) { @@ -144,8 +155,26 @@ class AttemptsHelper { count() { return this.adapter.count(); } +} + +class TokensHelper { + constructor(adapter) { + this.adapter = adapter; + } + add(username, token) { + return this.adapter.add(username, token); + } + + drop(username, token = null) { + return this.adapter.drop(username, token); + } + + lastAccess(username, token) { + return this.adapter.count(username, token); + } } module.exports.User = new UserModel(storage.User); module.exports.Attempts = new AttemptsHelper(storage.Attempts); +module.exports.Tokens = new TokensHelper(storage.Tokens); diff --git a/src/utils/jwt.js b/src/utils/jwt.js index c5dfa3274..ab5d5f2f5 100644 --- a/src/utils/jwt.js +++ b/src/utils/jwt.js @@ -1,10 +1,9 @@ -const Errors = require('common-errors'); const Promise = require('bluebird'); const jwt = Promise.promisifyAll(require('jsonwebtoken')); -const getMetadata = require('../utils/getMetadata.js'); const FlakeId = require('flake-idgen'); const flakeIdGen = new FlakeId(); -const Users = require('../db/adapter'); +const { User, Tokens } = require('../model/usermodel'); +const { ModelError, ERR_TOKEN_INVALID, ERR_TOKEN_AUDIENCE_MISMATCH } = require('../model/modelError'); /** * Logs user in and returns JWT and User Object @@ -34,10 +33,10 @@ exports.login = function login(username, _audience) { } return Promise.props({ - lastAccessUpdated: Users.addToken(username, token), + lastAccessUpdated: Tokens.add.call(this, username, token), jwt: token, username, - metadata: getMetadata.call(this, username, audience), + metadata: User.getMeta.call(this, username, audience), }) .then(function remap(props) { return { @@ -65,10 +64,10 @@ exports.logout = function logout(token, audience) { .verifyAsync(token, secret, { issuer, audience, algorithms: [algorithm] }) .catch(err => { this.log.debug('error decoding token', err); - throw new Errors.HttpStatusError(403, 'Invalid Token'); + throw new ModelError(ERR_TOKEN_INVALID); }) .then(function decodedToken(decoded) { - return Users.dropToken(decoded.username, token); + return Tokens.drop(decoded.username, token); }) .return({ success: true }); }; @@ -78,7 +77,7 @@ exports.logout = function logout(token, audience) { * @param {String} username */ exports.reset = function reset(username) { - return Users.dropToken(username); + return Tokens.drop(username); }; /** @@ -97,19 +96,19 @@ exports.verify = function verifyToken(token, audience, peek) { .verifyAsync(token, secret, { issuer, algorithms: [algorithm] }) .catch(err => { this.log.debug('invalid token passed: %s', token, err); - throw new Errors.HttpStatusError(403, 'invalid token'); + throw new ModelError(ERR_TOKEN_INVALID); }) .then(function decodedToken(decoded) { if (audience.indexOf(decoded.aud) === -1) { - throw new Errors.HttpStatusError(403, 'audience mismatch'); + throw new ModelError(ERR_TOKEN_AUDIENCE_MISMATCH); } const { username } = decoded; - let lastAccess = Users.lastAccess(username, token); + let lastAccess = Tokens.lastAccess(username, token); if (!peek) { lastAccess = lastAccess.then(function refreshLastAccess() { - return Users.addToken(username, token); + return Tokens.add(username, token); }); } From 8a5de756914e236abc79fcc51bcd05b4e42b1365 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Tue, 21 Jun 2016 03:32:53 +0300 Subject: [PATCH 15/38] feat: usermodel & redisstorage: throttle state and token methods, utils: send-email --- src/actions/activate.js | 4 +- src/actions/alias.js | 5 +- src/actions/ban.js | 4 +- src/actions/challenge.js | 5 +- src/actions/getInternalData.js | 5 +- src/actions/getMetadata.js | 4 +- src/actions/list.js | 4 +- src/actions/login.js | 3 +- src/actions/register.js | 40 ++--- src/actions/remove.js | 6 +- src/actions/requestPassword.js | 4 +- src/actions/updateMetadata.js | 4 +- src/actions/updatePassword.js | 9 +- src/actions/verify.js | 4 +- src/custom/cappasity-users-activate.js | 14 +- src/defaults.js | 7 + src/model/modelError.js | 3 + src/model/storages/redisstorage.js | 213 +++++++++++++++++++++++-- src/model/usermodel.js | 147 ++++++++++++++++- src/utils/aliasExists.js | 24 --- src/utils/checkCaptcha.js | 47 ------ src/utils/getInternalData.js | 25 --- src/utils/getMetadata.js | 34 ---- src/utils/isDisposable.js | 4 +- src/utils/key.js | 9 -- src/utils/mapMetaResponse.js | 2 +- src/utils/mxExists.js | 5 +- src/utils/scrypt.js | 11 +- src/utils/send-email.js | 55 +++---- src/utils/updateMetadata.js | 125 --------------- src/utils/userExists.js | 23 --- src/utils/verifyGoogleCaptcha.js | 10 +- 32 files changed, 425 insertions(+), 434 deletions(-) delete mode 100644 src/utils/aliasExists.js delete mode 100644 src/utils/checkCaptcha.js delete mode 100644 src/utils/getInternalData.js delete mode 100644 src/utils/getMetadata.js delete mode 100644 src/utils/key.js delete mode 100644 src/utils/updateMetadata.js delete mode 100644 src/utils/userExists.js diff --git a/src/actions/activate.js b/src/actions/activate.js index f8df0a52b..cd0dba209 100644 --- a/src/actions/activate.js +++ b/src/actions/activate.js @@ -2,7 +2,6 @@ const Promise = require('bluebird'); const emailVerification = require('../utils/send-email.js'); const jwt = require('../utils/jwt.js'); const { User } = require('../model/usermodel'); -const { httpErrorMapper } = require('../model/modelError'); module.exports = function verifyChallenge(opts) { // TODO: add security logs @@ -25,6 +24,5 @@ module.exports = function verifyChallenge(opts) { .tap(User.activate) .tap(hook) .then(user => [user, audience]) - .spread(jwt.login) - .catch(e => { throw httpErrorMapper(e); }); + .spread(jwt.login); }; diff --git a/src/actions/alias.js b/src/actions/alias.js index 5d4c306a3..2aa6664cc 100644 --- a/src/actions/alias.js +++ b/src/actions/alias.js @@ -1,9 +1,7 @@ const Promise = require('bluebird'); const isActive = require('../utils/isActive'); const isBanned = require('../utils/isBanned'); - const { User } = require('../model/usermodel'); -const { httpErrorMapper } = require('../model/modelError'); module.exports = function assignAlias(opts) { @@ -15,6 +13,5 @@ module.exports = function assignAlias(opts) { .tap(isActive) .tap(isBanned) .then(data => ({ username, alias, data })) - .then(User.User.setAlias) - .catch(e => { throw httpErrorMapper(e); }); + .then(User.setAlias); }; diff --git a/src/actions/ban.js b/src/actions/ban.js index a6ef07d42..1a0dbff2e 100644 --- a/src/actions/ban.js +++ b/src/actions/ban.js @@ -1,6 +1,5 @@ const Promise = require('bluebird'); const { User } = require('../model/usermodel'); -const { httpErrorMapper } = require('../model/modelError'); /** * Bans/unbans existing user @@ -12,6 +11,5 @@ module.exports = function banUser(opts) { .bind(this, opts.username) .then(User.getUsername) .then(username => ({ username, opts })) - .then(opts.ban ? User.lock : User.unlock) - .catch(e => { throw httpErrorMapper(e); }); + .then(opts.ban ? User.lock : User.unlock); }; diff --git a/src/actions/challenge.js b/src/actions/challenge.js index 8893b9144..b3ae36fe0 100644 --- a/src/actions/challenge.js +++ b/src/actions/challenge.js @@ -2,7 +2,7 @@ const Promise = require('bluebird'); const emailChallenge = require('../utils/send-email.js'); const isActive = require('../utils/isActive'); const { User } = require('../model/usermodel'); -const { ModelError, httpErrorMapper, ERR_USERNAME_ALREADY_ACTIVE } = require('../model/modelError'); +const { ModelError, ERR_USERNAME_ALREADY_ACTIVE } = require('../model/modelError'); module.exports = function sendChallenge(message) { const { username } = message; @@ -16,6 +16,5 @@ module.exports = function sendChallenge(message) { .tap(isActive) .throw(new ModelError(ERR_USERNAME_ALREADY_ACTIVE, username)) .catchReturn({ statusCode: 412 }, username) - .then(emailChallenge.send) - .catch(e => { throw httpErrorMapper(e); }); + .then(emailChallenge.send); }; diff --git a/src/actions/getInternalData.js b/src/actions/getInternalData.js index 5e6665c57..f57f1f7ce 100644 --- a/src/actions/getInternalData.js +++ b/src/actions/getInternalData.js @@ -1,8 +1,6 @@ const Promise = require('bluebird'); const pick = require('lodash/pick'); - const { User } = require('../model/usermodel'); -const { httpErrorMapper } = require('../model/modelError'); module.exports = function internalData(message) { const { fields } = message; @@ -12,6 +10,5 @@ module.exports = function internalData(message) { .then(User.getOne) .then(data => { return fields ? pick(data, fields) : data; - }) - .catch(e => { throw httpErrorMapper(e); }); + }); }; diff --git a/src/actions/getMetadata.js b/src/actions/getMetadata.js index 5ad8e41b4..4bd4cc55f 100644 --- a/src/actions/getMetadata.js +++ b/src/actions/getMetadata.js @@ -1,6 +1,5 @@ const Promise = require('bluebird'); const { User } = require('../model/usermodel'); -const { httpErrorMapper } = require('../model/modelError'); module.exports = function getMetadataAction(message) { const { audience, username, fields } = message; @@ -9,6 +8,5 @@ module.exports = function getMetadataAction(message) { .bind(this, username) .then(User.getUsername) .then(realUsername => [realUsername, audience, fields, message.public]) - .spread(User.getMeta) - .catch(e => { throw httpErrorMapper(e); }); + .spread(User.getMeta); }; diff --git a/src/actions/list.js b/src/actions/list.js index 1c8696f78..5ad6f117e 100644 --- a/src/actions/list.js +++ b/src/actions/list.js @@ -1,11 +1,9 @@ const Promise = require('bluebird'); const { User } = require('../model/usermodel'); -const { httpErrorMapper } = require('../model/modelError'); module.exports = function iterateOverActiveUsers(opts) { return Promise .bind(this, opts) - .then(User.getList) - .catch(e => { throw httpErrorMapper(e); }); + .then(User.getList); }; diff --git a/src/actions/login.js b/src/actions/login.js index f4b51ddb5..bee545eb9 100644 --- a/src/actions/login.js +++ b/src/actions/login.js @@ -5,7 +5,6 @@ const noop = require('lodash/noop'); const isActive = require('../utils/isActive'); const isBanned = require('../utils/isBanned'); const { User, Attempts } = require('../model/usermodel'); -const { httpErrorMapper } = require('../model/modelError'); module.exports = function login(opts) { const config = this.config.jwt; @@ -42,5 +41,5 @@ module.exports = function login(opts) { .tap(isActive) .tap(isBanned) .then(getUserInfo) - .catch(e => { throw httpErrorMapper(verifyIp ? enrichError(e) : e); }); + .catch(verifyIp ? enrichError : e => { throw e; }); }; diff --git a/src/actions/register.js b/src/actions/register.js index 92cba4b12..87bbcb9bc 100644 --- a/src/actions/register.js +++ b/src/actions/register.js @@ -1,15 +1,15 @@ const Promise = require('bluebird'); -const Errors = require('common-errors'); const scrypt = require('../utils/scrypt.js'); const emailValidation = require('../utils/send-email.js'); const jwt = require('../utils/jwt.js'); +const verifyGoogleCaptcha = require('../utils/verifyGoogleCaptcha'); const { MAIL_REGISTER } = require('../constants.js'); const isDisposable = require('../utils/isDisposable.js'); const mxExists = require('../utils/mxExists.js'); const noop = require('lodash/noop'); -const assignAlias = require('./alias.js'); -const Users = require('../db/adapter'); +const { User, Utils } = require('../model/usermodel'); +const { ModelError, ERR_ACCOUNT_MUST_BE_ACTIVATED, ERR_USERNAME_ALREADY_EXISTS } = require('../model/modelError'); /** * Registration handler @@ -17,8 +17,7 @@ const Users = require('../db/adapter'); * @return {Promise} */ module.exports = function registerUser(message) { - const { config } = this; - const { registrationLimits } = config; + const { config: registrationLimits } = this; // message const { username, alias, password, audience, ipaddress, skipChallenge, activate } = message; @@ -30,7 +29,7 @@ module.exports = function registerUser(message) { // make sure that if alias is truthy then activate is also truthy if (alias && !activate) { - throw new Errors.HttpStatusError(400, 'Account must be activated when setting alias during registration'); + throw new ModelError(ERR_ACCOUNT_MUST_BE_ACTIVATED); } let promise = Promise.bind(this, username); @@ -38,7 +37,7 @@ module.exports = function registerUser(message) { // optional captcha verification if (captcha) { logger.debug('verifying captcha'); - promise = promise.tap(Users.checkCaptcha(username, captcha)); + promise = promise.tap(Utils.checkCaptcha.call(this, username, captcha, verifyGoogleCaptcha)); } if (registrationLimits) { @@ -51,17 +50,17 @@ module.exports = function registerUser(message) { } if (registrationLimits.ip && ipaddress) { - promise = promise.tap(Users.checkLimits(ipaddress)); + promise = promise.tap(Utils.checkIPLimits.call(this, ipaddress)); } } // step 2, verify that user _still_ does not exist promise = promise // verify user does not exist at this point - .tap(Users.isExists) - .throw(new Errors.HttpStatusError(409, `"${username}" already exists`)) + .tap(User.getUsername) + .throw(new ModelError(ERR_USERNAME_ALREADY_EXISTS, username)) .catchReturn({ statusCode: 404 }, username) - .tap(alias ? () => Users.aliasAlreadyExists(alias) : noop) + .tap(alias ? () => User.checkAlias.call(this, alias) : noop) // step 3 - encrypt password .then(() => { if (password) { @@ -77,7 +76,7 @@ module.exports = function registerUser(message) { }) .then(scrypt.hash) // step 4 - create user if it wasn't created by some1 else trying to use race-conditions - .then(Users.createUser(username, activate)) + .then((hash) => { User.create.call(this, username, alias, hash, activate); }) // step 5 - save metadata if present .return({ username, @@ -89,7 +88,7 @@ module.exports = function registerUser(message) { }, }, }) - .then(Users.updateMetadata) + .then(User.setMeta) .return(username); // no instant activation -> send email or skip it based on the settings @@ -101,21 +100,10 @@ module.exports = function registerUser(message) { // perform instant activation return promise - // add to redis index - .then(() => Users.storeUsername(username)) - // call hook + // call hook .return(['users:activate', username, audience]) .spread(this.hook) - // assign alias if specified - .tap(() => { - if (!alias) { - return null; - } - - // adds on-registration alias to the user - return assignAlias.call(this, { username, alias }); - }) - // login user + // login user .return([username, audience]) .spread(jwt.login); }; diff --git a/src/actions/remove.js b/src/actions/remove.js index 710d9716b..35c7ae06a 100644 --- a/src/actions/remove.js +++ b/src/actions/remove.js @@ -1,7 +1,7 @@ const Promise = require('bluebird'); const { USERS_ADMIN_ROLE } = require('../constants'); const { User } = require('../model/usermodel'); -const { ModelError, httpErrorMapper, ERR_ADMIN_IS_UNTOUCHABLE } = require('../model/modelError'); +const { ModelError, ERR_ADMIN_IS_UNTOUCHABLE } = require('../model/modelError'); module.exports = function removeUser({ username }) { @@ -16,8 +16,6 @@ module.exports = function removeUser({ username }) { if (isAdmin) { throw new ModelError(ERR_ADMIN_IS_UNTOUCHABLE); } - return User.remove.call(this, username, internal); - }) - .catch(e => { throw httpErrorMapper(e); }); + }); }; diff --git a/src/actions/requestPassword.js b/src/actions/requestPassword.js index 7b216aef5..1b3d07316 100644 --- a/src/actions/requestPassword.js +++ b/src/actions/requestPassword.js @@ -3,7 +3,6 @@ const emailValidation = require('../utils/send-email.js'); const isActive = require('../utils/isActive'); const isBanned = require('../utils/isBanned'); const { User } = require('../model/usermodel'); -const { httpErrorMapper } = require('../model/modelError'); module.exports = function requestPassword(opts) { @@ -19,6 +18,5 @@ module.exports = function requestPassword(opts) { .tap(isActive) .tap(isBanned) .then(() => emailValidation.send.call(this, username, action)) - .return({ success: true }) - .catch(e => { throw httpErrorMapper(e); }); + .return({ success: true }); }; diff --git a/src/actions/updateMetadata.js b/src/actions/updateMetadata.js index 8e7675508..56fb9d739 100644 --- a/src/actions/updateMetadata.js +++ b/src/actions/updateMetadata.js @@ -1,12 +1,10 @@ const Promise = require('bluebird'); const { User } = require('../model/usermodel'); -const { httpErrorMapper } = require('../model/modelError'); module.exports = function updateMetadataAction(message) { return Promise .bind(this, message.username) .then(User.getUsername) .then(username => ({ ...message, username })) - .then(message.script ? User.executeUpdateMetaScript : User.setMeta) - .catch(e => { throw httpErrorMapper(e); }); + .then(message.script ? User.executeUpdateMetaScript : User.setMeta); }; diff --git a/src/actions/updatePassword.js b/src/actions/updatePassword.js index 40819ed93..0ec7fb754 100644 --- a/src/actions/updatePassword.js +++ b/src/actions/updatePassword.js @@ -2,12 +2,9 @@ const Promise = require('bluebird'); const scrypt = require('../utils/scrypt.js'); const jwt = require('../utils/jwt.js'); const emailChallenge = require('../utils/send-email.js'); - const isActive = require('../utils/isActive'); const isBanned = require('../utils/isBanned'); - const { User, Attempts } = require('../model/usermodel'); -const { httpErrorMapper } = require('../model/modelError'); /** @@ -30,8 +27,7 @@ function usernamePasswordReset(username, password) { .tap(isActive) .tap(isBanned) .tap(data => scrypt.verify(data.password, password)) - .return(username) - .catch(e => { throw httpErrorMapper(e); }); + .return(username); } /** @@ -47,8 +43,7 @@ function setPassword(_username, password) { username, hash: scrypt.hash(password), })) - .then(User.setPassword) - .catch(e => { throw httpErrorMapper(e); }); + .then(User.setPassword); } module.exports = exports = function updatePassword(opts) { diff --git a/src/actions/verify.js b/src/actions/verify.js index f0d4eaad9..2c72975c1 100644 --- a/src/actions/verify.js +++ b/src/actions/verify.js @@ -1,7 +1,6 @@ const Promise = require('bluebird'); const jwt = require('../utils/jwt.js'); const { User } = require('../model/usermodel'); -const { httpErrorMapper } = require('../model/modelError'); /** * Verifies that passed token is signed correctly, returns associated metadata with it @@ -28,6 +27,5 @@ module.exports = function verify(opts) { username, metadata: User.getMeta.call(this, username, audience), }); - }) - .catch(e => { throw httpErrorMapper(e); }); + }); }; diff --git a/src/custom/cappasity-users-activate.js b/src/custom/cappasity-users-activate.js index 1709a4636..fdd1cafb7 100644 --- a/src/custom/cappasity-users-activate.js +++ b/src/custom/cappasity-users-activate.js @@ -1,15 +1,15 @@ const find = require('lodash/find'); const moment = require('moment'); -const setMetadata = require('../utils/updateMetadata.js'); +const { User } = require('../model/usermodel'); /** * Adds metadata from billing into usermix * @param {String} username + * @param {String} audience * @return {Promise} */ module.exports = function mixPlan(username, audience) { - const { amqp, config } = this; - const { payments } = config; + const { amqp, config: payments } = this; const route = [payments.prefix, payments.routes.planGet].join('.'); const id = 'free'; @@ -19,10 +19,7 @@ module.exports = function mixPlan(username, audience) { .then(function mix(plan) { const subscription = find(plan.subs, ['name', 'month']); const nextCycle = moment().add(1, 'month').valueOf(); - const update = { - username, - audience, - metadata: { + const metadata = { $set: { plan: id, agreement: id, @@ -32,9 +29,8 @@ module.exports = function mixPlan(username, audience) { subscriptionPrice: '0', subscriptionInterval: 'month', }, - }, }; - return setMetadata.call(this, update); + return User.setMeta.call(this, username, audience, metadata); }); }; diff --git a/src/defaults.js b/src/defaults.js index 098b6a395..fb51a6b4e 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -1,4 +1,5 @@ const path = require('path'); +const { httpErrorMapper } = require('./model/modelError'); /** * Contains default options for users microservice @@ -19,6 +20,12 @@ module.exports = { initRoutes: true, // automatically init router initRouter: true, + // error wrapping to http state + onComplete(err) { + if (err) { + throw httpErrorMapper(err); + } + }, }, captcha: { secret: 'put-your-real-gcaptcha-secret-here', diff --git a/src/model/modelError.js b/src/model/modelError.js index 791901978..a47312ef2 100644 --- a/src/model/modelError.js +++ b/src/model/modelError.js @@ -28,6 +28,9 @@ const mapErr = (e) => e.code; const ERR_DEFAULT = genErr(0, 500, 'Internal error'); +/** + * Error types with inner code, HTTP-code and error message + */ const ErrorTypes = { ERR_ALIAS_ALREADY_ASSIGNED: genErr(100, 417, 'alias is already assigned'), diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index 655f04466..06b23321a 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -1,30 +1,31 @@ /** * Created by Stainwoortsel on 17.06.2016. */ -const Errors = require('common-errors'); const remapMeta = require('../../utils/remapMeta'); const mapMetaResponse = require('../../utils/mapMetaResponse'); const mapValues = require('lodash/mapValues'); const moment = require('moment'); const sha256 = require('../../utils/sha256.js'); const fsort = require('redis-filtered-sort'); +const uuid = require('node-uuid'); const noop = require('lodash/noop'); const get = require('lodash/get'); const is = require('is'); const { ModelError, ERR_ALIAS_ALREADY_ASSIGNED, ERR_ALIAS_ALREADY_TAKEN, ERR_USERNAME_NOT_EXISTS, ERR_USERNAME_NOT_FOUND, - ERR_ATTEMPTS_LOCKED, ERR_TOKEN_FORGED, + ERR_ATTEMPTS_LOCKED, ERR_TOKEN_FORGED, ERR_CAPTCHA_WRONG_USERNAME, ERR_ATTEMPTS_TO_MUCH_REGISTERED, + ERR_ALIAS_ALREADY_EXISTS, ERR_ACCOUNT_IS_ALREADY_EXISTS, ERR_ACCOUNT_ALREADY_ACTIVATED, } = require('../modelError'); /* - ERR_ALIAS_ALREADY_EXISTS, ERR_USERNAME_ALREADY_ACTIVE, + ERR_USERNAME_ALREADY_ACTIVE, ERR_USERNAME_ALREADY_EXISTS, ERR_ACCOUNT_MUST_BE_ACTIVATED, - ERR_ACCOUNT_NOT_ACTIVATED, ERR_ACCOUNT_ALREADY_ACTIVATED, ERR_ACCOUNT_IS_LOCKED, ERR_ACCOUNT_IS_ALREADY_EXISTS, - ERR_ADMIN_IS_UNTOUCHABLE, ERR_CAPTCHA_WRONG_USERNAME, ERR_CAPTCHA_ERROR_RESPONSE, ERR_EMAIL_DISPOSABLE, + ERR_ACCOUNT_NOT_ACTIVATED, ERR_ACCOUNT_IS_LOCKED + ERR_ADMIN_IS_UNTOUCHABLE, ERR_CAPTCHA_ERROR_RESPONSE, ERR_EMAIL_DISPOSABLE, ERR_EMAIL_NO_MX, ERR_EMAIL_ALREADY_SENT, ERR_TOKEN_INVALID, ERR_TOKEN_AUDIENCE_MISMATCH, ERR_TOKEN_MISS_EMAIL, ERR_TOKEN_EXPIRED, ERR_TOKEN_BAD_EMAIL, ERR_TOKEN_CANT_DECODE, ERR_PASSWORD_INVALID, ERR_PASSWORD_INVALID_HASH, ERR_PASSWORD_INCORRECT, ERR_PASSWORD_SCRYPT_ERROR, - ERR_ATTEMPTS_TO_MUCH_REGISTERED + */ // JSON @@ -39,7 +40,7 @@ const { } = require('../../constants'); // config's and base objects -const { redis, captcha: captchaConfig, config } = this; +const { redis, captcha: captchaConfig, registrationLimits, config } = this; const { deleteInactiveAccounts, jwt: { lockAfterAttempts, defaultAudience, hashingFunction: { ttl } } } = config; /** @@ -195,8 +196,20 @@ module.exports.User = { }); }, - setAlias(username, alias, data) { - if (data[USERS_ALIAS_FIELD]) { + checkAlias(alias) { + return redis + .hget(USERS_ALIAS_TO_LOGIN, alias) + .then(username => { + if (username) { + throw new ModelError(ERR_ALIAS_ALREADY_EXISTS, alias); + } + + return username; + }); + }, + + setAlias(username, alias, data = null) { + if (data && data[USERS_ALIAS_FIELD]) { throw new ModelError(ERR_ALIAS_ALREADY_ASSIGNED); } @@ -318,7 +331,39 @@ module.exports.User = { * @returns {*} */ create(username, alias, hash, activate) { - return this.adapter.create(username, alias, hash, activate); + const userDataKey = generateKey(username, USERS_DATA); + const pipeline = redis.pipeline(); + + pipeline + // add password + .hsetnx(userDataKey, 'password', hash) + // set activation flag + .hsetnx(userDataKey, USERS_ACTIVE_FLAG, activate); + + // if we can activate user + if (activate) { + // store username to index + pipeline.sadd(USERS_INDEX, username); + } + + // well done! let's execute + return pipeline + .exec() + .spread(function insertedUserData(passwordSetResponse) { + if (passwordSetResponse[1] === 0) { + throw new ModelError(ERR_ACCOUNT_IS_ALREADY_EXISTS, username); + } + + if (!activate && deleteInactiveAccounts >= 0) { + // WARNING: IF USER IS NOT VERIFIED WITHIN + // [by default 30] DAYS - IT WILL BE REMOVED FROM DATABASE + return redis.expire(userDataKey, deleteInactiveAccounts); + } + + return null; + }) + // setting alias, if we can + .tap(activate && alias ? () => { this.setAlias(username, alias); } : noop); }, /** @@ -370,7 +415,7 @@ module.exports.User = { .spread(function pipeResponse(isActive) { const status = isActive[1]; if (status === 'true') { - throw new Errors.HttpStatusError(417, `Account ${username} was already activated`); + throw new ModelError(ERR_ACCOUNT_ALREADY_ACTIVATED, username); } }); }, @@ -418,7 +463,12 @@ module.exports.User = { let loginAttempts; module.exports.Attempts = { - + /** + * Check login attempts + * @param username + * @param ip + * @returns {*} + */ check: function check({ username, ip }) { const ipKey = generateKey(username, 'ip', ip); const pipeline = redis.pipeline(); @@ -459,7 +509,7 @@ module.exports.Attempts = { /** * Get attempts count - * @returns {*} + * @returns {integer} */ count: function count() { return loginAttempts; @@ -467,16 +517,34 @@ module.exports.Attempts = { }; module.exports.Tokens = { + /** + * Add the token + * @param username + * @param token + * @returns {*} + */ add(username, token) { return redis.zadd(generateKey(username, USERS_TOKENS), Date.now(), token); }, + /** + * Drop the token + * @param username + * @param token + * @returns {*} + */ drop(username, token = null) { return token ? redis.zrem(generateKey(username, USERS_TOKENS), token) : redis.del(generateKey(username, USERS_TOKENS)); }, + /** + * Get last token score + * @param username + * @param token + * @returns {integer} + */ lastAccess(username, token) { const tokensHolder = generateKey(username, USERS_TOKENS); return redis.zscoreBuffer(tokensHolder, token).then(function getLastAccess(_score) { @@ -492,8 +560,127 @@ module.exports.Tokens = { }); }, + /** + * Get special email throttle state + * @param type + * @param email + * @returns {bool} state + */ + getEmailThrottleState(type, email) { + const throttleEmailsKey = generateKey(`vthrottle-${type}`, email); + return redis.get(throttleEmailsKey); + }, + + /** + * Set special email throttle state + * @param type + * @param email + * @returns {*} + */ + setEmailThrottleState(type, email) { + const throttleEmailsKey = generateKey(`vthrottle-${type}`, email); + const { validation: throttle } = config; + + const throttleArgs = [throttleEmailsKey, 1, 'NX']; + if (throttle > 0) { + throttleArgs.splice(2, 0, 'EX', throttle); + } + return redis.set(throttleArgs); + }, + + /** + * Get special email throttle token + * @param type + * @param token + * @returns {string} email + */ + getEmailThrottleToken(type, token) { + const secretKey = generateKey(`vsecret-${type}`, token); + return redis.get(secretKey) + }, + + /** + * Set special email throttle token + * @param type + * @param email + * @param token + * @returns {*} + */ + setEmailThrottleToken(type, email, token) { + const { validation: ttl } = config; + const secretKey = generateKey(`vsecret-${type}`, token); + const args = [secretKey, email]; + if (ttl > 0) { + args.push('EX', ttl); + } + return redis.set(args); + }, + + /** + * Drop special email throttle token + * @param type + * @param token + * @returns {*} + */ + dropEmailThrottleToken(type, token) { + const secretKey = generateKey(`vsecret-${type}`, token); + return redis.del(secretKey); + }, + }; module.exports.Utils = { + /** + * Check IP limits for registration + * @param ipaddress + * @returns {*} + */ + checkIPLimits(ipaddress) { + const { ip: { time, times } } = registrationLimits; + const ipaddressLimitKey = generateKey('reg-limit', ipaddress); + const now = Date.now(); + const old = now - time; + + return function iplimits() { + return redis + .pipeline() + .zadd(ipaddressLimitKey, now, uuid.v4()) + .pexpire(ipaddressLimitKey, time) + .zremrangebyscore(ipaddressLimitKey, '-inf', old) + .zcard(ipaddressLimitKey) + .exec() + .then(props => { + const cardinality = props[3][1]; + if (cardinality > times) { + throw new ModelError(ERR_ATTEMPTS_TO_MUCH_REGISTERED); + } + }); + }; + }, + /** + * Check captcha + * @param username + * @param captcha + * @param next + * @returns {*} + */ + checkCaptcha(username, captcha, next = null) { + const that = this; + return function checkCaptcha() { + const captchaCacheKey = captcha.response; + return redis + .pipeline() + .set(captchaCacheKey, username, 'EX', captchaConfig.ttl, 'NX') + .get(captchaCacheKey) + .exec() + .spread(function captchaCacheResponse(setResponse, getResponse) { + if (getResponse[1] !== username) { + throw new ModelError(ERR_CAPTCHA_WRONG_USERNAME); + } + }) + // check google captcha + .then(next ? () => next.call(that, captcha) : noop); + }; + }, }; diff --git a/src/model/usermodel.js b/src/model/usermodel.js index 395d13836..0db73c6b1 100644 --- a/src/model/usermodel.js +++ b/src/model/usermodel.js @@ -3,7 +3,9 @@ */ const storage = require('./storages/redisstorage'); - +/** + * Adapter pattern class with user model methods + */ class UserModel { /** * Create user model @@ -52,6 +54,21 @@ class UserModel { return this.adapter.getUsername(username); } + /** + * Check alias existence + * @param alias + * @returns {*} + */ + checkAlias(alias) { + return this.adapter.checkAlias(alias); + } + + /** + * Sets alias to the user by username + * @param username + * @param alias + * @returns {*} + */ setAlias(username, alias) { return this.adapter.setAlias(username, alias); } @@ -139,42 +156,164 @@ class UserModel { } } +/** + * Adapter pattern class for user login attempts counting + */ class AttemptsHelper { constructor(adapter) { this.adapter = adapter; } + /** + * Check login attempts + * @param username + * @param ip + * @returns {*} + */ check({ username, ip }) { return this.adapter.check({ username, ip }); } + /** + * Drop login attempts + * @param username + * @param ip + * @returns {*} + */ drop(username, ip) { return this.adapter.drop(username, ip); } + /** + * Get attempts count + * @returns {integer} + */ count() { return this.adapter.count(); } } +/** + * Adapter pattern class for user tokens + */ class TokensHelper { constructor(adapter) { this.adapter = adapter; } + /** + * Add the token + * @param username + * @param token + * @returns {*} + */ add(username, token) { return this.adapter.add(username, token); } + /** + * Drop the token + * @param username + * @param token + * @returns {*} + */ drop(username, token = null) { return this.adapter.drop(username, token); } + /** + * Get last token score + * @param username + * @param token + * @returns {integer} + */ lastAccess(username, token) { return this.adapter.count(username, token); } + + /** + * Get special email throttle state + * @param type + * @param email + * @returns {bool} state + */ + getEmailThrottleState(type, email) { + return this.adapter.getEmailThrottleState(type, email); + } + + /** + * Set special email throttle state + * @param type + * @param email + * @returns {*} + */ + setEmailThrottleState(type, email) { + return this.adapter.setEmailThrottleState(type, email); + } + + /** + * Get special email throttle token + * @param type + * @param token + * @returns {string} email + */ + getEmailThrottleToken(type, token) { + return this.adapter.getEmailThrottleToken(type, token); + } + + /** + * Set special email throttle token + * @param type + * @param email + * @param token + * @returns {*} + */ + setEmailThrottleToken(type, email, token) { + return this.adapter.setEmailThrottleToken(type, email, token); + } + + /** + * Drop special email throttle token + * @param type + * @param token + * @returns {*} + */ + dropEmailThrottleToken(type, token) { + return this.adapter.dropEmailThrottleToken(type, token); + } + +} + +/** + * Adapter pattern class for util methods with IP + */ +class Utils { + constructor(adapter) { + this.adapter = adapter; + } + + /** + * Check IP limits for registration + * @param ipaddress + * @returns {*} + */ + checkIPLimits(ipaddress) { + return this.adapter.checkIPLimits(ipaddress); + } + + /** + * Check captcha + * @param username + * @param captcha + * @param next + * @returns {*} + */ + checkCaptcha(username, captcha, next = null) { + return this.adapter.checkCaptcha(username, captcha, next); + } } -module.exports.User = new UserModel(storage.User); -module.exports.Attempts = new AttemptsHelper(storage.Attempts); -module.exports.Tokens = new TokensHelper(storage.Tokens); +exports.User = new UserModel(storage.User); +exports.Attempts = new AttemptsHelper(storage.Attempts); +exports.Tokens = new TokensHelper(storage.Tokens); +exports.Utils = new Utils(storage.Utils); diff --git a/src/utils/aliasExists.js b/src/utils/aliasExists.js deleted file mode 100644 index 0800375cc..000000000 --- a/src/utils/aliasExists.js +++ /dev/null @@ -1,24 +0,0 @@ -const Errors = require('common-errors'); -const { USERS_ALIAS_TO_LOGIN } = require('../constants.js'); - -function resolveAlias(alias) { - return this.redis - .hget(USERS_ALIAS_TO_LOGIN, alias) - .then(username => { - if (username) { - throw new Errors.HttpStatusError(409, `"${alias}" already exists`); - } - - return username; - }); -} - -module.exports = function aliasExists(alias, thunk) { - if (thunk) { - return function resolveAliasThunk() { - return resolveAlias.call(this, alias); - }; - } - - return resolveAlias.call(this, alias); -}; diff --git a/src/utils/checkCaptcha.js b/src/utils/checkCaptcha.js deleted file mode 100644 index ce4711ffc..000000000 --- a/src/utils/checkCaptcha.js +++ /dev/null @@ -1,47 +0,0 @@ -const Promise = require('bluebird'); -const Errors = require('common-errors'); -const request = require('request-promise'); -const defaults = require('lodash/defaults'); -const pick = require('lodash/pick'); -const fmt = require('util').format; - -/** - * Performs captcha check, returns thukn - * @param {redisCluster} redis - * @param {String} username - * @param {String} captcha - * @param {Object} captchaConfig - * @return {Function} - */ -module.exports = function makeCaptchaCheck(redis, username, captcha, captchaConfig) { - const { secret, ttl, uri } = captchaConfig; - return function checkCaptcha() { - const captchaCacheKey = captcha.response; - return redis - .pipeline() - .set(captchaCacheKey, username, 'EX', ttl, 'NX') - .get(captchaCacheKey) - .exec() - .spread(function captchaCacheResponse(setResponse, getResponse) { - if (getResponse[1] !== username) { - const msg = 'Captcha challenge you\'ve solved can not be used, please complete it again'; - throw new Errors.HttpStatusError(412, msg); - } - }) - .then(function verifyGoogleCaptcha() { - return request - .post({ uri, qs: defaults(captcha, { secret }), json: true }) - .then(function captchaSuccess(body) { - if (!body.success) { - return Promise.reject({ statusCode: 200, error: body }); - } - - return true; - }) - .catch(function captchaError(err) { - const errData = JSON.stringify(pick(err, ['statusCode', 'error'])); - throw new Errors.HttpStatusError(412, fmt('Captcha response: %s', errData)); - }); - }); - }; -}; diff --git a/src/utils/getInternalData.js b/src/utils/getInternalData.js deleted file mode 100644 index 9e82c93c2..000000000 --- a/src/utils/getInternalData.js +++ /dev/null @@ -1,25 +0,0 @@ -const Errors = require('common-errors'); -const redisKey = require('../utils/key.js'); -const { USERS_DATA, USERS_ALIAS_TO_LOGIN } = require('../constants.js'); - -module.exports = function getInternalData(username) { - const { redis } = this; - const userKey = redisKey(username, USERS_DATA); - return redis - .pipeline() - .hget(USERS_ALIAS_TO_LOGIN, username) - .exists(userKey) - .hgetallBuffer(userKey) - .exec() - .spread((aliasToUsername, exists, data) => { - if (aliasToUsername[1]) { - return getInternalData.call(this, aliasToUsername[1]); - } - - if (!exists[1]) { - throw new Errors.HttpStatusError(404, `"${username}" does not exists`); - } - - return { ...data[1], username }; - }); -}; diff --git a/src/utils/getMetadata.js b/src/utils/getMetadata.js deleted file mode 100644 index ebe3374c7..000000000 --- a/src/utils/getMetadata.js +++ /dev/null @@ -1,34 +0,0 @@ -const mapValues = require('lodash/mapValues'); -const pick = require('lodash/pick'); -const redisKey = require('../utils/key.js'); -const Promise = require('bluebird'); -const { isArray } = Array; -const JSONParse = JSON.parse.bind(JSON); -const { USERS_METADATA } = require('../constants.js'); - -module.exports = function getMetadata(username, _audiences, fields = {}) { - const { redis } = this; - const audiences = isArray(_audiences) ? _audiences : [_audiences]; - - return Promise.map(audiences, audience => { - return redis.hgetallBuffer(redisKey(username, USERS_METADATA, audience)); - }) - .then(function remapAudienceData(data) { - const output = {}; - audiences.forEach(function transform(aud, idx) { - const datum = data[idx]; - - if (datum) { - const pickFields = fields[aud]; - output[aud] = mapValues(datum, JSONParse); - if (pickFields) { - output[aud] = pick(output[aud], pickFields); - } - } else { - output[aud] = {}; - } - }); - - return output; - }); -}; diff --git a/src/utils/isDisposable.js b/src/utils/isDisposable.js index 53169b18b..4f78e5043 100644 --- a/src/utils/isDisposable.js +++ b/src/utils/isDisposable.js @@ -1,5 +1,5 @@ -const Errors = require('common-errors'); const disposableDomains = require('disposable-email-domains'); +const { ModelError, ERR_EMAIL_DISPOSABLE } = require('../model/modelError'); // init pointers const disposablePointers = {}; @@ -17,7 +17,7 @@ module.exports = function isDisposable(email) { const domain = email.split('@')[1]; return function testDisposable() { if (disposablePointers[domain]) { - throw new Errors.HttpStatusError(400, 'you must use non-disposable email to register'); + throw new ModelError(ERR_EMAIL_DISPOSABLE); } return email; diff --git a/src/utils/key.js b/src/utils/key.js deleted file mode 100644 index e4b21b494..000000000 --- a/src/utils/key.js +++ /dev/null @@ -1,9 +0,0 @@ -const SEPARATOR = '!'; - -/** - * Creates string key from passed arguments - * @return {String} - */ -module.exports = function combineKey(...args) { - return args.join(SEPARATOR); -}; diff --git a/src/utils/mapMetaResponse.js b/src/utils/mapMetaResponse.js index 186ea5998..0d29ffd51 100644 --- a/src/utils/mapMetaResponse.js +++ b/src/utils/mapMetaResponse.js @@ -2,7 +2,7 @@ * Created by Stainwoortsel on 05.06.2016. */ /** - * Is a common method for mapping updateMetadata ops + * Is a common method for mapping User.setMeta ops * @param operations * @param responses * @returns {Promise} diff --git a/src/utils/mxExists.js b/src/utils/mxExists.js index d0344a4a3..d62020bad 100644 --- a/src/utils/mxExists.js +++ b/src/utils/mxExists.js @@ -1,7 +1,6 @@ -const Errors = require('common-errors'); const Promise = require('bluebird'); const dns = Promise.promisifyAll(require('dns')); - +const { ModelError, ERR_EMAIL_NO_MX } = require('../model/modelError'); /** * Checks whether MX record exists or not * @param {String} email @@ -27,7 +26,7 @@ module.exports = function mxExists(email) { return null; } - throw new Errors.HttpStatusError(400, `no MX record was found for hostname ${hostname}`); + throw new ModelError(ERR_EMAIL_NO_MX, hostname); }); }; }; diff --git a/src/utils/scrypt.js b/src/utils/scrypt.js index 2d07e1ca8..68a0e091f 100644 --- a/src/utils/scrypt.js +++ b/src/utils/scrypt.js @@ -1,14 +1,15 @@ -const Errors = require('common-errors'); const Promise = require('bluebird'); const scrypt = Promise.promisifyAll(require('scrypt')); const bytes = require('bytes'); +const { ModelError, ERR_PASSWORD_INVALID, ERR_PASSWORD_INCORRECT, + ERR_PASSWORD_SCRYPT_ERROR, ERR_PASSWORD_INVALID_HASH } = require('../model/modelError'); // setup scrypt const scryptParams = scrypt.paramsSync(0.1, bytes('32mb')); exports.hash = function hashPassword(password) { if (!password) { - throw new Errors.HttpStatusError(500, 'invalid password passed'); + throw new ModelError(ERR_PASSWORD_INVALID); } return scrypt.kdfAsync(new Buffer(password, 'utf-8'), scryptParams); @@ -16,17 +17,17 @@ exports.hash = function hashPassword(password) { exports.verify = function verifyPassword(hash, password) { if (!Buffer.isBuffer(hash) || hash.length === 0) { - throw new Errors.HttpStatusError(500, 'invalid password hash retrieved from redis'); + throw new ModelError(ERR_PASSWORD_INVALID_HASH); } return scrypt .verifyKdfAsync(hash, new Buffer(password, 'utf-8')) .catch(function scryptError(err) { - throw new Errors.HttpStatusError(403, err.scrypt_err_message || err.message); + throw new ModelError(ERR_PASSWORD_SCRYPT_ERROR, err); }) .then(function verifyResult(result) { if (result !== true) { - throw new Errors.HttpStatusError(403, 'incorrect password'); + throw new ModelError(ERR_PASSWORD_INCORRECT) } }); }; diff --git a/src/utils/send-email.js b/src/utils/send-email.js index 24ae64b73..cef0242ee 100644 --- a/src/utils/send-email.js +++ b/src/utils/send-email.js @@ -1,14 +1,15 @@ const Errors = require('common-errors'); const Promise = require('bluebird'); // jshint ignore: line const uuid = require('node-uuid'); -const redisKey = require('../utils/key.js'); const crypto = require('crypto'); const URLSafeBase64 = require('urlsafe-base64'); const render = require('ms-mailer-templates'); const { updatePassword } = require('../actions/updatePassword.js'); const generatePassword = require('password-generator'); const { MAIL_ACTIVATE, MAIL_RESET, MAIL_PASSWORD, MAIL_REGISTER } = require('../constants.js'); - +const { Tokens } = require('../model/usermodel'); +const { ModelError, ERR_TOKEN_CANT_DECODE, ERR_EMAIL_ALREADY_SENT, ERR_TOKEN_BAD_EMAIL, + ERR_TOKEN_EXPIRED, ERR_TOKEN_MISS_EMAIL} = require('../model/modelError'); /** * Throttled error * @param {Mixed} reply @@ -16,7 +17,7 @@ const { MAIL_ACTIVATE, MAIL_RESET, MAIL_PASSWORD, MAIL_REGISTER } = require('../ function isThrottled(compare) { return function comparator(reply) { if (!!reply === compare) { - throw new Errors.HttpStatusError(429, 'We\'ve already sent you an email, if it doesn\'t come - please try again in a little while or send us an email'); // eslint-disable-line + throw new ModelError(ERR_EMAIL_ALREADY_SENT); } }; } @@ -86,7 +87,7 @@ exports.safeDecode = function safeDecode(algorithm, secret, string) { .bind(this) .catch(function remapError(err) { this.log.warn('cant decode token', err); - throw new Errors.HttpStatusError(403, 'could not decode token'); + throw new ModelError(ERR_TOKEN_CANT_DECODE); }); }; @@ -98,17 +99,17 @@ exports.safeDecode = function safeDecode(algorithm, secret, string) { * @return {Promise} */ exports.send = function sendEmail(email, type = MAIL_ACTIVATE, wait = false) { - const { redis, config, mailer } = this; + const { config, mailer } = this; const { validation, server } = config; - const { ttl, throttle, subjects, senders, paths, secret, algorithm, email: mailingAccount } = validation; // eslint-disable-line + const { subjects, senders, paths, secret, algorithm, email: mailingAccount } = validation; // eslint-disable-line // method specific stuff - const throttleEmailsKey = redisKey(`vthrottle-${type}`, email); const activationSecret = uuid.v4(); const logger = this.log.child({ action: 'sendEmail', email }); - return redis - .get(throttleEmailsKey) + return Promise + .bind(this) + .then(() => Tokens.getEmailThrottleState(type, email)) .then(isThrottled(true)) .then(function generateContent() { // generate context @@ -148,21 +149,11 @@ exports.send = function sendEmail(email, type = MAIL_ACTIVATE, wait = false) { return updatePassword.call(this, email, context.password); } - const throttleArgs = [throttleEmailsKey, 1, 'NX']; - if (throttle > 0) { - throttleArgs.splice(2, 0, 'EX', throttle); - } - return redis - .set(throttleArgs) + return Promise + .bind(this) + .then(() => Tokens.setEmailThrottleState(type, email)) .then(isThrottled(false)) - .then(function updateSecret() { - const secretKey = redisKey(`vsecret-${type}`, activationSecret); - const args = [secretKey, email]; - if (ttl > 0) { - args.push('EX', ttl); - } - return redis.set(args); - }); + .then(() => Tokens.setEmailThrottleToken(type, email, activationSecret)); }) .then(function definedSubjectAndSend({ context, emailTemplate }) { const mail = { @@ -199,8 +190,7 @@ exports.send = function sendEmail(email, type = MAIL_ACTIVATE, wait = false) { * @return {Promise} */ exports.verify = function verifyToken(string, namespace = MAIL_ACTIVATE, expires) { - const { redis, config } = this; - const { validation } = config; + const { config: validation } = this; const { secret: validationSecret, algorithm } = validation; return exports @@ -210,24 +200,23 @@ exports.verify = function verifyToken(string, namespace = MAIL_ACTIVATE, expires const { email, token } = message; if (!email || !token) { - const msg = 'Decoded token misses references to email and/or secret'; - throw new Errors.HttpStatusError(403, msg); + throw new ModelError(ERR_TOKEN_MISS_EMAIL); } - const secretKey = redisKey(`vsecret-${namespace}`, token); - return redis - .get(secretKey) + return Promise + .bind(this) + .then(() => Tokens.getEmailThrottleToken(namespace, token)) .then(function inspectAssociatedData(associatedEmail) { if (!associatedEmail) { - throw new Errors.HttpStatusError(404, 'token expired or is invalid'); + throw new ModelError(ERR_TOKEN_EXPIRED); } if (associatedEmail !== email) { - throw new Errors.HttpStatusError(412, 'associated email doesn\'t match token'); + throw new ModelError(ERR_TOKEN_BAD_EMAIL); } if (expires) { - return redis.del(secretKey); + return Tokens.dropEmailThrottleToken.call(this, namespace, token); } return null; diff --git a/src/utils/updateMetadata.js b/src/utils/updateMetadata.js deleted file mode 100644 index 2956b757c..000000000 --- a/src/utils/updateMetadata.js +++ /dev/null @@ -1,125 +0,0 @@ -const Promise = require('bluebird'); -const mapValues = require('lodash/mapValues'); -const redisKey = require('../utils/key.js'); -const JSONStringify = JSON.stringify.bind(JSON); -const is = require('is'); -const sha256 = require('./sha256.js'); -const { USERS_METADATA } = require('../constants.js'); - -/** - * Process metadata update operation for a passed audience - * @param {Object} pipeline - * @param {String} audience - * @param {Object} metadata - */ -function handleAudience(pipeline, key, metadata) { - const $remove = metadata.$remove; - const $removeOps = $remove && $remove.length || 0; - if ($removeOps > 0) { - pipeline.hdel(key, $remove); - } - - const $set = metadata.$set; - const $setKeys = $set && Object.keys($set); - const $setLength = $setKeys && $setKeys.length || 0; - if ($setLength > 0) { - pipeline.hmset(key, mapValues($set, JSONStringify)); - } - - const $incr = metadata.$incr; - const $incrFields = $incr && Object.keys($incr); - const $incrLength = $incrFields && $incrFields.length || 0; - if ($incrLength > 0) { - $incrFields.forEach(fieldName => { - pipeline.hincrby(key, fieldName, $incr[fieldName]); - }); - } - - return { $removeOps, $setLength, $incrLength, $incrFields }; -} - -/** - * Maps updateMetadata ops - * @param {Array} responses - * @param {Array} operations - * @return {Object|Array} - */ -function mapMetaResponse(operations, responses) { - let cursor = 0; - return Promise.map(operations, props => { - const { $removeOps, $setLength, $incrLength, $incrFields } = props; - const output = {}; - - if ($removeOps > 0) { - output.$remove = responses[cursor][1]; - cursor++; - } - - if ($setLength > 0) { - output.$set = responses[cursor][1]; - cursor++; - } - - if ($incrLength > 0) { - const $incrResponse = output.$incr = {}; - $incrFields.forEach(fieldName => { - $incrResponse[fieldName] = responses[cursor][1]; - cursor++; - }); - } - - return output; - }) - .then(ops => { - return ops.length > 1 ? ops : ops[0]; - }); -} - -/** - * Handle script, mutually exclusive with metadata - * @param {Array} scriptKeys - * @param {Array} responses - */ -function mapScriptResponse(scriptKeys, responses) { - const output = {}; - scriptKeys.forEach((fieldName, idx) => { - output[fieldName] = responses[idx]; - }); - return output; -} - -/** - * Updates metadata on a user object - * @param {Object} opts - * @return {Promise} - */ -module.exports = function updateMetadata(opts) { - const { redis } = this; - const { username, audience, metadata, script } = opts; - const audiences = is.array(audience) ? audience : [audience]; - - // keys - const keys = audiences.map(aud => redisKey(username, USERS_METADATA, aud)); - - // if we have meta, then we can - if (metadata) { - const pipe = redis.pipeline(); - const metaOps = is.array(metadata) ? metadata : [metadata]; - const operations = metaOps.map((meta, idx) => handleAudience(pipe, keys[idx], meta)); - return pipe.exec().then(res => mapMetaResponse(operations, res)); - } - - // dynamic scripts - const $scriptKeys = Object.keys(script); - const scripts = $scriptKeys.map(scriptName => { - const { lua, argv = [] } = script[scriptName]; - const sha = sha256(lua); - const name = `ms_users_${sha}`; - if (!is.fn(redis[name])) { - redis.defineCommand(name, { lua }); - } - return redis[name](keys.length, keys, argv); - }); - - return Promise.all(scripts).then(res => mapScriptResponse($scriptKeys, res)); -}; diff --git a/src/utils/userExists.js b/src/utils/userExists.js deleted file mode 100644 index be39d8c12..000000000 --- a/src/utils/userExists.js +++ /dev/null @@ -1,23 +0,0 @@ -const Errors = require('common-errors'); -const redisKey = require('../utils/key.js'); -const { USERS_DATA, USERS_ALIAS_TO_LOGIN } = require('../constants.js'); - -module.exports = function userExists(username) { - return this - .redis - .pipeline() - .hget(USERS_ALIAS_TO_LOGIN, username) - .exists(redisKey(username, USERS_DATA)) - .exec() - .spread((alias, exists) => { - if (alias[1]) { - return alias[1]; - } - - if (!exists[1]) { - throw new Errors.HttpStatusError(404, `"${username}" does not exists`); - } - - return username; - }); -}; diff --git a/src/utils/verifyGoogleCaptcha.js b/src/utils/verifyGoogleCaptcha.js index b287b6f45..a1c559b7f 100644 --- a/src/utils/verifyGoogleCaptcha.js +++ b/src/utils/verifyGoogleCaptcha.js @@ -2,14 +2,12 @@ * Created by Stainwoortsel on 05.06.2016. */ const defaults = require('lodash/defaults'); -const Errors = require('common-errors'); const request = require('request-promise'); const pick = require('lodash/pick'); -const fmt = require('util').format; -const { captcha: captchaConfig } = this; // ????? is THIS available here? +const { ModelError, ERR_CAPTCHA_ERROR_RESPONSE } = require('../model/modelError'); -module.exports = function verifyGoogleCaptcha(captcha) { // captchaConfig - const { secret, uri } = captchaConfig; +module.exports = function verifyGoogleCaptcha(captcha) { + const { config: { captcha: { captchaConfig: { secret, uri } } } } = this; return request .post({ uri, qs: defaults(captcha, { secret }), json: true }) .then(function captchaSuccess(body) { @@ -21,6 +19,6 @@ module.exports = function verifyGoogleCaptcha(captcha) { // captchaConfig }) .catch(function captchaError(err) { const errData = JSON.stringify(pick(err, ['statusCode', 'error'])); - throw new Errors.HttpStatusError(412, fmt('Captcha response: %s', errData)); + throw new ModelError(ERR_CAPTCHA_ERROR_RESPONSE, errData); }); }; From 3829f5ea9da5b6b7702939cd1250ceff61ee3333 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Tue, 21 Jun 2016 03:39:20 +0300 Subject: [PATCH 16/38] style: some linter fixes: util/scrypt, util/send-emal, model/redisstorage, model/usermodel --- src/custom/cappasity-users-activate.js | 18 +++++++++--------- src/model/storages/redisstorage.js | 6 +++--- src/model/usermodel.js | 1 - src/utils/scrypt.js | 4 ++-- src/utils/send-email.js | 2 +- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/custom/cappasity-users-activate.js b/src/custom/cappasity-users-activate.js index fdd1cafb7..3ad187e18 100644 --- a/src/custom/cappasity-users-activate.js +++ b/src/custom/cappasity-users-activate.js @@ -20,15 +20,15 @@ module.exports = function mixPlan(username, audience) { const subscription = find(plan.subs, ['name', 'month']); const nextCycle = moment().add(1, 'month').valueOf(); const metadata = { - $set: { - plan: id, - agreement: id, - nextCycle, - models: subscription.models, - modelPrice: subscription.price, - subscriptionPrice: '0', - subscriptionInterval: 'month', - }, + $set: { + plan: id, + agreement: id, + nextCycle, + models: subscription.models, + modelPrice: subscription.price, + subscriptionPrice: '0', + subscriptionInterval: 'month', + }, }; return User.setMeta.call(this, username, audience, metadata); diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index 06b23321a..5891f32aa 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -596,7 +596,7 @@ module.exports.Tokens = { */ getEmailThrottleToken(type, token) { const secretKey = generateKey(`vsecret-${type}`, token); - return redis.get(secretKey) + return redis.get(secretKey); }, /** @@ -607,11 +607,11 @@ module.exports.Tokens = { * @returns {*} */ setEmailThrottleToken(type, email, token) { - const { validation: ttl } = config; + const { validation } = config; const secretKey = generateKey(`vsecret-${type}`, token); const args = [secretKey, email]; if (ttl > 0) { - args.push('EX', ttl); + args.push('EX', validation.ttl); } return redis.set(args); }, diff --git a/src/model/usermodel.js b/src/model/usermodel.js index 0db73c6b1..0c715ed6e 100644 --- a/src/model/usermodel.js +++ b/src/model/usermodel.js @@ -281,7 +281,6 @@ class TokensHelper { dropEmailThrottleToken(type, token) { return this.adapter.dropEmailThrottleToken(type, token); } - } /** diff --git a/src/utils/scrypt.js b/src/utils/scrypt.js index 68a0e091f..c16283cfa 100644 --- a/src/utils/scrypt.js +++ b/src/utils/scrypt.js @@ -1,7 +1,7 @@ const Promise = require('bluebird'); const scrypt = Promise.promisifyAll(require('scrypt')); const bytes = require('bytes'); -const { ModelError, ERR_PASSWORD_INVALID, ERR_PASSWORD_INCORRECT, +const { ModelError, ERR_PASSWORD_INVALID, ERR_PASSWORD_INCORRECT, ERR_PASSWORD_SCRYPT_ERROR, ERR_PASSWORD_INVALID_HASH } = require('../model/modelError'); // setup scrypt @@ -27,7 +27,7 @@ exports.verify = function verifyPassword(hash, password) { }) .then(function verifyResult(result) { if (result !== true) { - throw new ModelError(ERR_PASSWORD_INCORRECT) + throw new ModelError(ERR_PASSWORD_INCORRECT); } }); }; diff --git a/src/utils/send-email.js b/src/utils/send-email.js index cef0242ee..599de549e 100644 --- a/src/utils/send-email.js +++ b/src/utils/send-email.js @@ -9,7 +9,7 @@ const generatePassword = require('password-generator'); const { MAIL_ACTIVATE, MAIL_RESET, MAIL_PASSWORD, MAIL_REGISTER } = require('../constants.js'); const { Tokens } = require('../model/usermodel'); const { ModelError, ERR_TOKEN_CANT_DECODE, ERR_EMAIL_ALREADY_SENT, ERR_TOKEN_BAD_EMAIL, - ERR_TOKEN_EXPIRED, ERR_TOKEN_MISS_EMAIL} = require('../model/modelError'); + ERR_TOKEN_EXPIRED, ERR_TOKEN_MISS_EMAIL } = require('../model/modelError'); /** * Throttled error * @param {Mixed} reply From 60746e90499b132c9984b5b0424e40b717fb2b69 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 24 Jun 2016 02:18:10 +0300 Subject: [PATCH 17/38] fix: fixing docker.sh --- ms-users.zip | Bin 0 -> 65342 bytes src/utils/key.js | 9 +++++++++ test/config.js | 4 ++-- test/docker-compose.yml | 2 +- test/docker.sh | 28 +++++++++++++++------------- 5 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 ms-users.zip create mode 100644 src/utils/key.js diff --git a/ms-users.zip b/ms-users.zip new file mode 100644 index 0000000000000000000000000000000000000000..0854964379981975e73dac5c1e70e7d80b34b2b2 GIT binary patch literal 65342 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 literal 0 HcmV?d00001 diff --git a/src/utils/key.js b/src/utils/key.js new file mode 100644 index 000000000..e4b21b494 --- /dev/null +++ b/src/utils/key.js @@ -0,0 +1,9 @@ +const SEPARATOR = '!'; + +/** + * Creates string key from passed arguments + * @return {String} + */ +module.exports = function combineKey(...args) { + return args.join(SEPARATOR); +}; diff --git a/test/config.js b/test/config.js index 8e19ce7ea..d7c34e7b8 100644 --- a/test/config.js +++ b/test/config.js @@ -99,9 +99,9 @@ function startService() { } function clearRedis() { - const nodes = this.users._redis.masterNodes; + const nodes = this.users.redis.nodes('master'); return Promise - .map(Object.keys(nodes), nodeKey => nodes[nodeKey].flushdb()) + .map(nodes, node => node.flushdb()) .finally(() => this.users.close()) .finally(() => { this.users = null; diff --git a/test/docker-compose.yml b/test/docker-compose.yml index 95c258c2a..5ffd9f674 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -55,4 +55,4 @@ tester: environment: NODE_ENV: "docker" SKIP_REBUILD: ${SKIP_REBUILD} - command: 'true' + command: tail -f /dev/null diff --git a/test/docker.sh b/test/docker.sh index 7eb317d69..f3c5afb30 100755 --- a/test/docker.sh +++ b/test/docker.sh @@ -10,30 +10,32 @@ MOCHA=$BIN/_mocha COVER="$BIN/isparta cover" NODE=$BIN/babel-node TESTS=${TESTS:-test/suites/*.js} - -if [ -z "$NODE_VER" ]; then - NODE_VER="6.2.0" -fi +NODE_VER=${NODE_VER:-6.2.1} +COMPOSE_VER=${COMPOSE_VER:-1.7.1} if ! [ -x "$COMPOSE" ]; then mkdir $DIR/.bin - curl -L https://github.com/docker/compose/releases/download/1.5.2/docker-compose-`uname -s`-`uname -m` > $DIR/.bin/docker-compose + curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VER}/docker-compose-`uname -s`-`uname -m` > $DIR/.bin/docker-compose chmod +x $DIR/.bin/docker-compose - COMPOSE=$(which docker-compose) +# COMPOSE=$(which docker-compose) + COMPOSE="c:/dev/docker/docker-compose.exe" fi function finish { - "$COMPOSE" -f $DC stop - "$COMPOSE" -f $DC rm -f + $COMPOSE -f $DC stop + $COMPOSE -f $DC rm -f } trap finish EXIT -export IMAGE=makeomatic/alpine-node:$NODE_VER -"$COMPOSE" -f $DC up -d +export IMAGE=makeomatic/node:$NODE_VER +$COMPOSE -f $DC up -d + +# add glibc +$COMPOSE -f $DC exec tester /bin/sh -c "apk --no-cache add build-base python" || exit 1 if [[ "$SKIP_REBUILD" != "1" ]]; then echo "rebuilding native dependencies..." - "$COMPOSE" -f $DC run --rm tester npm rebuild + $COMPOSE -f $DC exec tester npm rebuild fi echo "cleaning old coverage" @@ -41,11 +43,11 @@ rm -rf ./coverage echo "running tests" for fn in $TESTS; do - "$COMPOSE" -f $DC run --rm tester /bin/sh -c "$NODE $COVER --dir ./coverage/${fn##*/} $MOCHA -- $fn" || exit 1 + $COMPOSE -f $DC exec tester /bin/sh -c "$NODE $COVER --dir ./coverage/${fn##*/} $MOCHA -- $fn" || exit 1 done echo "started generating combined coverage" -"$COMPOSE" -f $DC run --rm tester node ./test/aggregate-report.js +$COMPOSE -f $DC exec tester node ./test/aggregate-report.js echo "uploading coverage report from ./coverage/lcov.info" if [[ "$CI" == "true" ]]; then From 0dfc8c9d6feeb1d4ae4c422ccd1611eb04a633d6 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Mon, 27 Jun 2016 02:38:54 +0300 Subject: [PATCH 18/38] fix: fixing 'this'-context trouble, fixing loginAttempts variable --- src/actions/login.js | 8 +- src/actions/updatePassword.js | 3 +- src/model/storages/redisstorage.js | 23 +--- src/model/usermodel.js | 166 ++++++++++++++--------------- 4 files changed, 93 insertions(+), 107 deletions(-) diff --git a/src/actions/login.js b/src/actions/login.js index 4f081dab8..37007a481 100644 --- a/src/actions/login.js +++ b/src/actions/login.js @@ -14,6 +14,8 @@ module.exports = function login(opts) { const remoteip = opts.remoteip || false; const verifyIp = remoteip && lockAfterAttempts > 0; + const theAttempts = new Attempts(this); + function verifyHash(data) { return scrypt.verify(data.password, password); } @@ -24,7 +26,7 @@ module.exports = function login(opts) { function enrichError(err) { if (remoteip) { - err.loginAttempts = Attempts.count(); + err.loginAttempts = theAttempts.count(); } return err; @@ -34,9 +36,9 @@ module.exports = function login(opts) { .bind(this, opts.username) .then(User.getOne) .then(data => [data, remoteip]) - .tap(verifyIp ? Attempts.check : noop) + .tap(verifyIp ? ({ username, ip }) => theAttempts.check(username, ip) : noop) .tap(verifyHash) - .tap(verifyIp ? Attempts.drop : noop) + .tap(verifyIp ? (username, ip) => theAttempts.drop(username, ip) : noop) .tap(isActive) .tap(isBanned) .then(getUserInfo) diff --git a/src/actions/updatePassword.js b/src/actions/updatePassword.js index 0ec7fb754..b66e33476 100644 --- a/src/actions/updatePassword.js +++ b/src/actions/updatePassword.js @@ -68,7 +68,8 @@ module.exports = exports = function updatePassword(opts) { if (remoteip) { promise = promise.tap(function resetLock(username) { - return Attempts.drop(username, remoteip); + const theAttempts = new Attempts(this); + return theAttempts.drop(username, remoteip); }); } diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index f99319252..fd67a6d92 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -4,7 +4,6 @@ const remapMeta = require('../../utils/remapMeta'); const mapMetaResponse = require('../../utils/mapMetaResponse'); const mapValues = require('lodash/mapValues'); -const moment = require('moment'); const sha256 = require('../../utils/sha256.js'); const fsort = require('redis-filtered-sort'); const uuid = require('node-uuid'); @@ -14,7 +13,7 @@ const is = require('is'); const { ModelError, ERR_ALIAS_ALREADY_ASSIGNED, ERR_ALIAS_ALREADY_TAKEN, ERR_USERNAME_NOT_EXISTS, ERR_USERNAME_NOT_FOUND, - ERR_ATTEMPTS_LOCKED, ERR_TOKEN_FORGED, ERR_CAPTCHA_WRONG_USERNAME, ERR_ATTEMPTS_TO_MUCH_REGISTERED, + ERR_TOKEN_FORGED, ERR_CAPTCHA_WRONG_USERNAME, ERR_ATTEMPTS_TO_MUCH_REGISTERED, ERR_ALIAS_ALREADY_EXISTS, ERR_ACCOUNT_IS_ALREADY_EXISTS, ERR_ACCOUNT_ALREADY_ACTIVATED, } = require('../modelError'); /* @@ -480,7 +479,6 @@ exports.User = { }, }; -let loginAttempts; exports.Attempts = { /** * Check login attempts @@ -490,7 +488,6 @@ exports.Attempts = { */ check: function check({ username, ip }) { const { redis, config } = this; - const { jwt: { lockAfterAttempts } } = config; const ipKey = generateKey(username, 'ip', ip); const pipeline = redis.pipeline(); @@ -505,14 +502,9 @@ exports.Attempts = { const err = incrementValue[0]; if (err) { this.log.error('Redis error:', err); - return; - } - - loginAttempts = incrementValue[1]; - if (loginAttempts > lockAfterAttempts) { - const duration = moment().add(config.keepLoginAttempts, 'seconds').toNow(true); - throw new ModelError(ERR_ATTEMPTS_LOCKED, duration); + return null; } + return incrementValue[1]; }); }, @@ -525,17 +517,8 @@ exports.Attempts = { drop: function drop(username, ip) { const { redis } = this; const ipKey = generateKey(username, 'ip', ip); - loginAttempts = 0; return redis.del(ipKey); }, - - /** - * Get attempts count - * @returns {integer} - */ - count: function count() { - return loginAttempts; - }, }; exports.Tokens = { diff --git a/src/model/usermodel.js b/src/model/usermodel.js index 0c715ed6e..6f6781992 100644 --- a/src/model/usermodel.js +++ b/src/model/usermodel.js @@ -1,28 +1,23 @@ /** * Created by Stainwoortsel on 17.06.2016. */ +const Promise = require('bluebird'); const storage = require('./storages/redisstorage'); +const { ModelError, ERR_ATTEMPTS_LOCKED } = require('./modelError'); +const moment = require('moment'); /** * Adapter pattern class with user model methods */ -class UserModel { - /** - * Create user model - * @param adapter - */ - constructor(adapter) { - this.adapter = adapter; - } - +exports.User = { /** * Get user by username * @param username * @returns {Object} */ getOne(username) { - return this.adapter.getOne(username); - } + return storage.User.getOne.call(this, username); + }, /** * Get list of users by params @@ -30,8 +25,8 @@ class UserModel { * @returns {Array} */ getList(opts) { - return this.adapter.getList(opts); - } + return storage.User.getList.call(this, opts); + }, /** * Get metadata of user @@ -42,8 +37,8 @@ class UserModel { * @returns {Object} */ getMeta(username, audiences, fields = {}, _public = null) { - return this.adapter.getMeta(username, audiences, fields, _public); - } + return storage.User.getMeta.call(this, username, audiences, fields, _public); + }, /** * Get ~real~ username by username or alias @@ -51,8 +46,8 @@ class UserModel { * @returns {String} username */ getUsername(username) { - return this.adapter.getUsername(username); - } + return storage.User.getUsername.call(this, username); + }, /** * Check alias existence @@ -60,8 +55,8 @@ class UserModel { * @returns {*} */ checkAlias(alias) { - return this.adapter.checkAlias(alias); - } + return storage.User.checkAlias.call(this, alias); + }, /** * Sets alias to the user by username @@ -70,8 +65,8 @@ class UserModel { * @returns {*} */ setAlias(username, alias) { - return this.adapter.setAlias(username, alias); - } + return storage.User.setAlias.call(this, username, alias); + }, /** * Set user password @@ -80,8 +75,8 @@ class UserModel { * @returns {String} username */ setPassword(username, hash) { - return this.adapter.setPassword(username, hash); - } + return storage.User.setPassword.call(this, username, hash); + }, /** * Updates metadata of user by username and audience @@ -91,8 +86,8 @@ class UserModel { * @returns {Object} */ setMeta(username, audience, metadata) { - return this.adapter.setMeta(username, audience, metadata); - } + return storage.User.setMeta.call(this, username, audience, metadata); + }, /** * Update meta of user by using direct script @@ -102,8 +97,8 @@ class UserModel { * @returns {Object} */ executeUpdateMetaScript(username, audience, script) { - return this.adapter.executeUpdateMetaScript(username, audience, script); - } + return storage.User.executeUpdateMetaScript.call(this, username, audience, script); + }, /** * Create user account with alias and password @@ -114,8 +109,8 @@ class UserModel { * @returns {*} */ create(username, alias, hash, activate) { - return this.adapter.create(username, alias, hash, activate); - } + return storage.User.create.call(this, username, alias, hash, activate); + }, /** * Remove user @@ -124,8 +119,8 @@ class UserModel { * @returns {*} */ remove(username, data) { - return this.adapter.remove(username, data); - } + return storage.User.remove.call(this, username, data); + }, /** * Activate user @@ -133,8 +128,8 @@ class UserModel { * @returns {*} */ activate(username) { - return this.adapter.activate(username); - } + return storage.User.activate.call(this, username); + }, /** * Ban user @@ -143,8 +138,8 @@ class UserModel { * @returns {*} */ lock(username, opts) { - return this.adapter.lock(username, opts); - } + return storage.User.lock.call(this, username, opts); + }, /** * Unlock banned user @@ -152,16 +147,21 @@ class UserModel { * @returns {*} */ unlock(username) { - return this.adapter.unlock(username); - } -} + return storage.User.unlock.call(this, username); + }, +}; /** * Adapter pattern class for user login attempts counting */ -class AttemptsHelper { - constructor(adapter) { - this.adapter = adapter; +class AttemptsClass { + /** + * Attempts class constructor, _context parameter is the context of service + * @param _context + */ + constructor(_context) { + this.context = _context; + this.loginAttempts = 0; } /** @@ -170,8 +170,19 @@ class AttemptsHelper { * @param ip * @returns {*} */ - check({ username, ip }) { - return this.adapter.check({ username, ip }); + check(username, ip) { + const { config: { jwt: { lockAfterAttempts }, keepLoginAttempts } } = this.context; + return Promise.bind(this.context, { username, ip }) + .then(storage.Attempts.check) + .then((attempts) => { + if (attempts === null) return; + + this.loginAttempts = attempts; + if (this.loginAttempts > lockAfterAttempts) { + const duration = moment().add(keepLoginAttempts, 'seconds').toNow(true); + throw new ModelError(ERR_ATTEMPTS_LOCKED, duration); + } + }); } /** @@ -181,7 +192,8 @@ class AttemptsHelper { * @returns {*} */ drop(username, ip) { - return this.adapter.drop(username, ip); + this.loginAttempts = 0; + return storage.Attempts.drop.call(this, username, ip); } /** @@ -189,18 +201,15 @@ class AttemptsHelper { * @returns {integer} */ count() { - return this.adapter.count(); + return this.loginAttempts; } } +exports.Attempts = AttemptsClass; -/** + /** * Adapter pattern class for user tokens */ -class TokensHelper { - constructor(adapter) { - this.adapter = adapter; - } - +exports.Tokens = { /** * Add the token * @param username @@ -208,8 +217,8 @@ class TokensHelper { * @returns {*} */ add(username, token) { - return this.adapter.add(username, token); - } + return storage.Tokens.add.call(this, username, token); + }, /** * Drop the token @@ -218,8 +227,8 @@ class TokensHelper { * @returns {*} */ drop(username, token = null) { - return this.adapter.drop(username, token); - } + return storage.Tokens.drop.call(this, username, token); + }, /** * Get last token score @@ -228,8 +237,8 @@ class TokensHelper { * @returns {integer} */ lastAccess(username, token) { - return this.adapter.count(username, token); - } + return storage.Tokens.lastAccess.call(this, username, token); + }, /** * Get special email throttle state @@ -238,8 +247,8 @@ class TokensHelper { * @returns {bool} state */ getEmailThrottleState(type, email) { - return this.adapter.getEmailThrottleState(type, email); - } + return storage.Tokens.getEmailThrottleState.call(this, type, email); + }, /** * Set special email throttle state @@ -248,8 +257,8 @@ class TokensHelper { * @returns {*} */ setEmailThrottleState(type, email) { - return this.adapter.setEmailThrottleState(type, email); - } + return storage.Tokens.setEmailThrottleState.call(this, type, email); + }, /** * Get special email throttle token @@ -258,8 +267,8 @@ class TokensHelper { * @returns {string} email */ getEmailThrottleToken(type, token) { - return this.adapter.getEmailThrottleToken(type, token); - } + return storage.Tokens.getEmailThrottleToken.call(this, type, token); + }, /** * Set special email throttle token @@ -269,8 +278,8 @@ class TokensHelper { * @returns {*} */ setEmailThrottleToken(type, email, token) { - return this.adapter.setEmailThrottleToken(type, email, token); - } + return storage.Tokens.setEmailThrottleToken.call(this, type, email, token); + }, /** * Drop special email throttle token @@ -279,26 +288,22 @@ class TokensHelper { * @returns {*} */ dropEmailThrottleToken(type, token) { - return this.adapter.dropEmailThrottleToken(type, token); - } -} + return storage.Tokens.dropEmailThrottleToken.call(this, type, token); + }, +}; /** * Adapter pattern class for util methods with IP */ -class Utils { - constructor(adapter) { - this.adapter = adapter; - } - +exports.Utils = { /** * Check IP limits for registration * @param ipaddress * @returns {*} */ checkIPLimits(ipaddress) { - return this.adapter.checkIPLimits(ipaddress); - } + return storage.Utils.checkIPLimits.call(this, ipaddress); + }, /** * Check captcha @@ -308,11 +313,6 @@ class Utils { * @returns {*} */ checkCaptcha(username, captcha, next = null) { - return this.adapter.checkCaptcha(username, captcha, next); - } -} - -exports.User = new UserModel(storage.User); -exports.Attempts = new AttemptsHelper(storage.Attempts); -exports.Tokens = new TokensHelper(storage.Tokens); -exports.Utils = new Utils(storage.Utils); + return storage.Utils.checkCaptcha.call(this, username, captcha, next); + }, +}; From e4c8b441f886447c986db890db25f55bbb0457d2 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 1 Jul 2016 04:38:31 +0300 Subject: [PATCH 19/38] fix: fixing bugs through the tests --- src/actions/activate.js | 4 ++-- src/actions/register.js | 2 +- src/model/modelError.js | 8 ++++++-- src/utils/send-email.js | 10 ++++++---- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/actions/activate.js b/src/actions/activate.js index cd0dba209..8d023b750 100644 --- a/src/actions/activate.js +++ b/src/actions/activate.js @@ -6,12 +6,12 @@ const { User } = require('../model/usermodel'); module.exports = function verifyChallenge(opts) { // TODO: add security logs // var remoteip = opts.remoteip; - const { token, namespace, username } = opts; + const { token, username } = opts; const { config } = this; const audience = opts.audience || config.defaultAudience; function verifyToken() { - return emailVerification.verify.call(this, token, namespace, config.validation.ttl > 0); + return emailVerification.verify.call(this, token, 'activate', config.validation.ttl > 0); } function hook(user) { diff --git a/src/actions/register.js b/src/actions/register.js index 87bbcb9bc..10e9d46d3 100644 --- a/src/actions/register.js +++ b/src/actions/register.js @@ -17,7 +17,7 @@ const { ModelError, ERR_ACCOUNT_MUST_BE_ACTIVATED, ERR_USERNAME_ALREADY_EXISTS } * @return {Promise} */ module.exports = function registerUser(message) { - const { config: registrationLimits } = this; + const { config: { registrationLimits } } = this; // message const { username, alias, password, audience, ipaddress, skipChallenge, activate } = message; diff --git a/src/model/modelError.js b/src/model/modelError.js index a47312ef2..f6543197f 100644 --- a/src/model/modelError.js +++ b/src/model/modelError.js @@ -69,7 +69,7 @@ const ErrorTypes = { ERR_EMAIL_ALREADY_SENT: genErr(152, 429, 'We\'ve already sent you an email, if it doesn\'t come - please try again in a little while or send us an email'), // eslint-disable-line ERR_TOKEN_INVALID: - genErr(160, 403, 'Invalid Token'), + genErr(160, 403, 'invalid token'), ERR_TOKEN_AUDIENCE_MISMATCH: genErr(161, 403, 'audience mismatch'), ERR_TOKEN_MISS_EMAIL: @@ -141,8 +141,12 @@ const httpErrorMapper = function _HttpErrorMapper(e = null) { return e; } + if (e instanceof Errors.ValidationError) { + return e; + } + return new Errors.HttpStatusError(ERR_DEFAULT.http, ERR_DEFAULT.msg); }; -module.exports = { ...ErrorCodes, ModelError, httpErrorMapper }; + module.exports = { ...ErrorCodes, ModelError, httpErrorMapper }; diff --git a/src/utils/send-email.js b/src/utils/send-email.js index 599de549e..b686d588c 100644 --- a/src/utils/send-email.js +++ b/src/utils/send-email.js @@ -190,8 +190,7 @@ exports.send = function sendEmail(email, type = MAIL_ACTIVATE, wait = false) { * @return {Promise} */ exports.verify = function verifyToken(string, namespace = MAIL_ACTIVATE, expires) { - const { config: validation } = this; - const { secret: validationSecret, algorithm } = validation; + const { config: { validation: { secret: validationSecret, algorithm } } } = this; return exports .safeDecode @@ -204,8 +203,11 @@ exports.verify = function verifyToken(string, namespace = MAIL_ACTIVATE, expires } return Promise - .bind(this) - .then(() => Tokens.getEmailThrottleToken(namespace, token)) +// .bind(this) +// .then(() => Tokens.getEmailThrottleToken(namespace, token)) +// .then(function inspectAssociatedData(associatedEmail) { + .bind(that, [namespace, token]) + .spread(Tokens.getEmailThrottleToken) .then(function inspectAssociatedData(associatedEmail) { if (!associatedEmail) { throw new ModelError(ERR_TOKEN_EXPIRED); From ac180640224dcfb96f47f0cd0bc142edce9ab854 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 1 Jul 2016 04:43:11 +0300 Subject: [PATCH 20/38] fix: linting! --- src/model/modelError.js | 2 +- src/utils/send-email.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/model/modelError.js b/src/model/modelError.js index f6543197f..8d50807bd 100644 --- a/src/model/modelError.js +++ b/src/model/modelError.js @@ -149,4 +149,4 @@ const httpErrorMapper = function _HttpErrorMapper(e = null) { }; - module.exports = { ...ErrorCodes, ModelError, httpErrorMapper }; +module.exports = { ...ErrorCodes, ModelError, httpErrorMapper }; diff --git a/src/utils/send-email.js b/src/utils/send-email.js index b686d588c..34fa76594 100644 --- a/src/utils/send-email.js +++ b/src/utils/send-email.js @@ -191,6 +191,7 @@ exports.send = function sendEmail(email, type = MAIL_ACTIVATE, wait = false) { */ exports.verify = function verifyToken(string, namespace = MAIL_ACTIVATE, expires) { const { config: { validation: { secret: validationSecret, algorithm } } } = this; + const that = this; return exports .safeDecode @@ -218,7 +219,7 @@ exports.verify = function verifyToken(string, namespace = MAIL_ACTIVATE, expires } if (expires) { - return Tokens.dropEmailThrottleToken.call(this, namespace, token); + return Tokens.dropEmailThrottleToken.call(that, namespace, token); } return null; From 71aa9828671249a0136ede1a6444ae53285350b4 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 1 Jul 2016 06:46:52 +0300 Subject: [PATCH 21/38] fix: fixing bugs through tests --- src/actions/ban.js | 2 +- src/actions/updateMetadata.js | 2 +- src/custom/cappasity-users-activate.js | 2 +- src/model/storages/redisstorage.js | 26 +++++++++++++++----------- src/model/usermodel.js | 19 ++++--------------- src/utils/send-email.js | 10 +++++----- 6 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/actions/ban.js b/src/actions/ban.js index 1a0dbff2e..0a479ca19 100644 --- a/src/actions/ban.js +++ b/src/actions/ban.js @@ -10,6 +10,6 @@ module.exports = function banUser(opts) { return Promise .bind(this, opts.username) .then(User.getUsername) - .then(username => ({ username, opts })) + .then(username => ({ ...opts, username })) .then(opts.ban ? User.lock : User.unlock); }; diff --git a/src/actions/updateMetadata.js b/src/actions/updateMetadata.js index 56fb9d739..4057da911 100644 --- a/src/actions/updateMetadata.js +++ b/src/actions/updateMetadata.js @@ -6,5 +6,5 @@ module.exports = function updateMetadataAction(message) { .bind(this, message.username) .then(User.getUsername) .then(username => ({ ...message, username })) - .then(message.script ? User.executeUpdateMetaScript : User.setMeta); + .then(User.setMeta); }; diff --git a/src/custom/cappasity-users-activate.js b/src/custom/cappasity-users-activate.js index 3ad187e18..88b271723 100644 --- a/src/custom/cappasity-users-activate.js +++ b/src/custom/cappasity-users-activate.js @@ -31,6 +31,6 @@ module.exports = function mixPlan(username, audience) { }, }; - return User.setMeta.call(this, username, audience, metadata); + return User.setMeta.call(this, { username, audience, metadata }); }); }; diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index fd67a6d92..c2a649c43 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -67,7 +67,7 @@ exports.User = { .exec() .spread((aliasToUsername, exists, data) => { if (aliasToUsername[1]) { - return this.getUser(aliasToUsername[1]); + return exports.User.getUser(aliasToUsername[1]); } if (!exists[1]) { @@ -283,15 +283,20 @@ exports.User = { * @param metadata * @returns {Object} */ - setMeta(username, audience, metadata) { + setMeta(opts) { const { redis } = this; + const { username, audience, metadata, script } = opts; const audiences = is.array(audience) ? audience : [audience]; const keys = audiences.map(aud => generateKey(username, USERS_METADATA, aud)); - const pipe = redis.pipeline(); - const metaOps = is.array(metadata) ? metadata : [metadata]; - const operations = metaOps.map((meta, idx) => this._handleAudience(pipe, keys[idx], meta)); - return pipe.exec().then(res => mapMetaResponse(operations, res)); + if(metadata) { + const pipe = redis.pipeline(); + const metaOps = is.array(metadata) ? metadata : [metadata]; + const operations = metaOps.map((meta, idx) => exports.User._handleAudience(pipe, keys[idx], metadata)); + return pipe.exec().then(res => mapMetaResponse(operations, res)); + } + + return exports.User.executeUpdateMetaScript(username, audience, script); }, /** @@ -301,9 +306,8 @@ exports.User = { * @param script * @returns {Object} */ - executeUpdateMetaScript(username, audience, script) { + executeUpdateMetaScript(username, audiences, script) { const { redis } = this; - const audiences = is.array(audience) ? audience : [audience]; const keys = audiences.map(aud => generateKey(username, USERS_METADATA, aud)); // dynamic scripts @@ -371,7 +375,7 @@ exports.User = { return null; }) // setting alias, if we can - .tap(activate && alias ? () => { this.setAlias(username, alias); } : noop); + .tap(activate && alias ? () => { exports.User.setAlias(username, alias); } : noop); }, /** @@ -438,11 +442,11 @@ exports.User = { * @param opts * @returns {*} */ - lock(username, opts) { + lock(opts) { const { redis, config } = this; const { jwt: { defaultAudience } } = config; - const { reason, whom, remoteip } = opts; // to guarantee writing only those three variables to metadata from opts + const { username, reason, whom, remoteip } = opts; // to guarantee writing only those three variables to metadata from opts const data = { banned: true, [USERS_BANNED_DATA]: { diff --git a/src/model/usermodel.js b/src/model/usermodel.js index 6f6781992..61c8409b2 100644 --- a/src/model/usermodel.js +++ b/src/model/usermodel.js @@ -85,19 +85,8 @@ exports.User = { * @param metadata * @returns {Object} */ - setMeta(username, audience, metadata) { - return storage.User.setMeta.call(this, username, audience, metadata); - }, - - /** - * Update meta of user by using direct script - * @param username - * @param audience - * @param script - * @returns {Object} - */ - executeUpdateMetaScript(username, audience, script) { - return storage.User.executeUpdateMetaScript.call(this, username, audience, script); + setMeta(opts) { + return storage.User.setMeta.call(this, opts); }, /** @@ -137,8 +126,8 @@ exports.User = { * @param opts * @returns {*} */ - lock(username, opts) { - return storage.User.lock.call(this, username, opts); + lock(opts) { + return storage.User.lock.call(this, opts); }, /** diff --git a/src/utils/send-email.js b/src/utils/send-email.js index 34fa76594..1e14abe73 100644 --- a/src/utils/send-email.js +++ b/src/utils/send-email.js @@ -108,8 +108,8 @@ exports.send = function sendEmail(email, type = MAIL_ACTIVATE, wait = false) { const logger = this.log.child({ action: 'sendEmail', email }); return Promise - .bind(this) - .then(() => Tokens.getEmailThrottleState(type, email)) + .bind(this, [type, email]) + .spread(Tokens.getEmailThrottleState) .then(isThrottled(true)) .then(function generateContent() { // generate context @@ -150,10 +150,10 @@ exports.send = function sendEmail(email, type = MAIL_ACTIVATE, wait = false) { } return Promise - .bind(this) - .then(() => Tokens.setEmailThrottleState(type, email)) + .bind(this, [type, email, activationSecret]) + .spread(Tokens.setEmailThrottleState) .then(isThrottled(false)) - .then(() => Tokens.setEmailThrottleToken(type, email, activationSecret)); + .then(Tokens.setEmailThrottleToken); }) .then(function definedSubjectAndSend({ context, emailTemplate }) { const mail = { From 2ee69931de81d370c47148a162c76be064504b74 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 1 Jul 2016 06:50:40 +0300 Subject: [PATCH 22/38] fix: linting! --- src/model/storages/redisstorage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index c2a649c43..9017374bf 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -289,7 +289,7 @@ exports.User = { const audiences = is.array(audience) ? audience : [audience]; const keys = audiences.map(aud => generateKey(username, USERS_METADATA, aud)); - if(metadata) { + if (metadata) { const pipe = redis.pipeline(); const metaOps = is.array(metadata) ? metadata : [metadata]; const operations = metaOps.map((meta, idx) => exports.User._handleAudience(pipe, keys[idx], metadata)); From 275a7bb726e71f85fc49f7c8c613fc69bef7c72c Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 1 Jul 2016 06:56:52 +0300 Subject: [PATCH 23/38] fix: fixing register bug with catchReturn wrong error code --- src/actions/register.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/actions/register.js b/src/actions/register.js index 10e9d46d3..340273f6c 100644 --- a/src/actions/register.js +++ b/src/actions/register.js @@ -9,7 +9,7 @@ const mxExists = require('../utils/mxExists.js'); const noop = require('lodash/noop'); const { User, Utils } = require('../model/usermodel'); -const { ModelError, ERR_ACCOUNT_MUST_BE_ACTIVATED, ERR_USERNAME_ALREADY_EXISTS } = require('../model/modelError'); +const { ModelError, ERR_USERNAME_NOT_EXISTS, ERR_ACCOUNT_MUST_BE_ACTIVATED, ERR_USERNAME_ALREADY_EXISTS } = require('../model/modelError'); /** * Registration handler @@ -59,7 +59,7 @@ module.exports = function registerUser(message) { // verify user does not exist at this point .tap(User.getUsername) .throw(new ModelError(ERR_USERNAME_ALREADY_EXISTS, username)) - .catchReturn({ statusCode: 404 }, username) + .catchReturn({ code: ERR_USERNAME_NOT_EXISTS }, username) .tap(alias ? () => User.checkAlias.call(this, alias) : noop) // step 3 - encrypt password .then(() => { From 7d5a1f2c27fbe428cb2ef001553a6253429f73d5 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Sun, 3 Jul 2016 00:53:39 +0300 Subject: [PATCH 24/38] fix: fixing bugs through the tests --- src/actions/register.js | 2 +- src/defaults.js | 11 +++------- src/messageResolver.js | 16 ++++++++++++++ src/model/modelError.js | 11 ++-------- src/model/storages/redisstorage.js | 4 ++-- test/suites/reg.js | 35 ++++++++++++++++++++++++++++++ test/suites/register.js | 2 +- 7 files changed, 60 insertions(+), 21 deletions(-) create mode 100644 src/messageResolver.js create mode 100644 test/suites/reg.js diff --git a/src/actions/register.js b/src/actions/register.js index 340273f6c..f0df2b0f8 100644 --- a/src/actions/register.js +++ b/src/actions/register.js @@ -76,7 +76,7 @@ module.exports = function registerUser(message) { }) .then(scrypt.hash) // step 4 - create user if it wasn't created by some1 else trying to use race-conditions - .then((hash) => { User.create.call(this, username, alias, hash, activate); }) + .then((hash) => User.create.call(this, username, alias, hash, activate)) // step 5 - save metadata if present .return({ username, diff --git a/src/defaults.js b/src/defaults.js index 09c1844a1..9091cfe5d 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -1,6 +1,5 @@ const path = require('path'); -const { httpErrorMapper } = require('./model/modelError'); - +const resolveMessage = require('./messageResolver.js'); /** * Contains default options for users microservice * @type {Object} @@ -20,12 +19,8 @@ module.exports = { initRoutes: true, // automatically init router initRouter: true, - // error wrapping to http state - onComplete(err) { - if (err) { - throw httpErrorMapper(err); - } - }, + // onComplete handler with error wrapping + onComplete: resolveMessage, }, captcha: { secret: 'put-your-real-gcaptcha-secret-here', diff --git a/src/messageResolver.js b/src/messageResolver.js new file mode 100644 index 000000000..9ae552e10 --- /dev/null +++ b/src/messageResolver.js @@ -0,0 +1,16 @@ +/** + * Created by Stainwoortsel on 02.07.2016. + */ +const { httpErrorMapper } = require('./model/modelError'); + +module.exports = function resolveMessage(err, data) { + if (err) { + if (err.name === 'ModelError') { + throw httpErrorMapper(err); + } + + throw err; + } + + return data; +}; diff --git a/src/model/modelError.js b/src/model/modelError.js index 8d50807bd..296f2f176 100644 --- a/src/model/modelError.js +++ b/src/model/modelError.js @@ -41,7 +41,7 @@ const ErrorTypes = { ERR_USERNAME_ALREADY_ACTIVE: genErr(110, 417, (username) => (`${username} is already active`)), ERR_USERNAME_ALREADY_EXISTS: - genErr(111, 409, (username) => (`${username} is already exists`)), + genErr(111, 409, (username) => (`"${username}" already exists`)), ERR_USERNAME_NOT_EXISTS: genErr(112, 404, (username) => (`${username} does not exists`)), ERR_USERNAME_NOT_FOUND: @@ -108,6 +108,7 @@ const ErrorCodes = mapValues(ErrorTypes, mapErr); */ const ModelError = Errors.helpers.generateClass('ModelError', { extends: Errors.Error, + name: 'ModelError', args: ['code', 'data'], generateMessage: function generateMessage() { const key = findKey(ErrorTypes, { code: this.code }); @@ -137,14 +138,6 @@ const httpErrorMapper = function _HttpErrorMapper(e = null) { return e; } - if (e instanceof Errors.InvalidOperationError) { - return e; - } - - if (e instanceof Errors.ValidationError) { - return e; - } - return new Errors.HttpStatusError(ERR_DEFAULT.http, ERR_DEFAULT.msg); }; diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index 9017374bf..24d762227 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -224,7 +224,7 @@ exports.User = { .pipeline() .sadd(USERS_PUBLIC_INDEX, username) .hset(generateKey(username, USERS_DATA), USERS_ALIAS_FIELD, alias) - .hset(generateKey(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, JSONStringify) + .hset(generateKey(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, JSON.stringify(alias)) .exec(); }, @@ -375,7 +375,7 @@ exports.User = { return null; }) // setting alias, if we can - .tap(activate && alias ? () => { exports.User.setAlias(username, alias); } : noop); + .tap(activate && alias ? () => exports.User.setAlias.call(this, username, alias) : noop); }, /** diff --git a/test/suites/reg.js b/test/suites/reg.js new file mode 100644 index 000000000..e76da681f --- /dev/null +++ b/test/suites/reg.js @@ -0,0 +1,35 @@ +/* global inspectPromise */ +const { expect } = require('chai'); +const times = require('lodash/times'); + +describe('#reg', function registerSuite() { + const headers = { routingKey: 'users.register' }; + + beforeEach(global.startService); + afterEach(global.clearRedis); + + it('must be able to create user without validations and return user object and jwt token', function test() { + const opts = { + username: 'v@makeomatic.ru', + password: 'mynicepassword', + audience: 'matic.ninja', + }; + + return this.users + .router(opts, headers) + .reflect() + .then(inspectPromise(true)) + .then(registered => { +console.log(registered); + expect(registered).to.have.ownProperty('jwt'); + expect(registered).to.have.ownProperty('user'); + expect(registered.user.username).to.be.eq(opts.username); + expect(registered.user).to.have.ownProperty('metadata'); + expect(registered.user.metadata).to.have.ownProperty('matic.ninja'); + expect(registered.user.metadata).to.have.ownProperty('*.localhost'); + expect(registered.user).to.not.have.ownProperty('password'); + expect(registered.user).to.not.have.ownProperty('audience'); + }); + }); + +}); diff --git a/test/suites/register.js b/test/suites/register.js index 157d049e8..e30910ef1 100644 --- a/test/suites/register.js +++ b/test/suites/register.js @@ -47,7 +47,7 @@ describe('#register', function registerSuite() { audience: 'matic.ninja', alias: 'bondthebest', }; - + return this.users .router(opts, headers) .reflect() From 8b7263d226d76bbdfd811d60fb8ac5705c3b662b Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Sun, 3 Jul 2016 05:19:47 +0300 Subject: [PATCH 25/38] fix: fixing bugs through the tests --- src/actions/challenge.js | 4 ++-- src/actions/getMetadata.js | 12 +++++++--- src/actions/login.js | 4 ++-- src/model/storages/redisstorage.js | 37 +++++++++++++++++++----------- src/model/usermodel.js | 13 +++++------ src/utils/isPublic.js | 24 +++++++++++++++++++ test/suites/reg.js | 35 ---------------------------- 7 files changed, 66 insertions(+), 63 deletions(-) create mode 100644 src/utils/isPublic.js delete mode 100644 test/suites/reg.js diff --git a/src/actions/challenge.js b/src/actions/challenge.js index b3ae36fe0..63ebe4b1a 100644 --- a/src/actions/challenge.js +++ b/src/actions/challenge.js @@ -2,7 +2,7 @@ const Promise = require('bluebird'); const emailChallenge = require('../utils/send-email.js'); const isActive = require('../utils/isActive'); const { User } = require('../model/usermodel'); -const { ModelError, ERR_USERNAME_ALREADY_ACTIVE } = require('../model/modelError'); +const { ModelError, ERR_ACCOUNT_NOT_ACTIVATED, ERR_USERNAME_ALREADY_ACTIVE } = require('../model/modelError'); module.exports = function sendChallenge(message) { const { username } = message; @@ -15,6 +15,6 @@ module.exports = function sendChallenge(message) { .then(User.getOne) .tap(isActive) .throw(new ModelError(ERR_USERNAME_ALREADY_ACTIVE, username)) - .catchReturn({ statusCode: 412 }, username) + .catchReturn({ code: ERR_ACCOUNT_NOT_ACTIVATED }, username) .then(emailChallenge.send); }; diff --git a/src/actions/getMetadata.js b/src/actions/getMetadata.js index 4bd4cc55f..ab186bd9d 100644 --- a/src/actions/getMetadata.js +++ b/src/actions/getMetadata.js @@ -1,12 +1,18 @@ const Promise = require('bluebird'); +const isPublic = require('../utils/isPublic'); +const noop = require('lodash/noop'); const { User } = require('../model/usermodel'); +const isArray = Array.isArray; + module.exports = function getMetadataAction(message) { - const { audience, username, fields } = message; + const { audience: _audience, username, fields } = message; + const audience = isArray(_audience) ? _audience : [_audience]; return Promise .bind(this, username) .then(User.getUsername) - .then(realUsername => [realUsername, audience, fields, message.public]) - .spread(User.getMeta); + .then(realUsername => [realUsername, audience, fields]) + .spread(User.getMeta) + .tap(message.public ? isPublic(username, audience) : noop); }; diff --git a/src/actions/login.js b/src/actions/login.js index 37007a481..6e765dd7a 100644 --- a/src/actions/login.js +++ b/src/actions/login.js @@ -35,10 +35,10 @@ module.exports = function login(opts) { return Promise .bind(this, opts.username) .then(User.getOne) - .then(data => [data, remoteip]) + .then(data => ({ ...data, remoteip })) .tap(verifyIp ? ({ username, ip }) => theAttempts.check(username, ip) : noop) .tap(verifyHash) - .tap(verifyIp ? (username, ip) => theAttempts.drop(username, ip) : noop) + .tap(verifyIp ? ({ username, ip }) => theAttempts.drop(username, ip) : noop) .tap(isActive) .tap(isBanned) .then(getUserInfo) diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index 24d762227..80b459a14 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -67,7 +67,7 @@ exports.User = { .exec() .spread((aliasToUsername, exists, data) => { if (aliasToUsername[1]) { - return exports.User.getUser(aliasToUsername[1]); + return exports.User.getOne(aliasToUsername[1]); } if (!exists[1]) { @@ -207,25 +207,34 @@ exports.User = { }); }, - setAlias(username, alias, data = null) { + /** + * Set up alias of user + * @param opts + * @returns {*} + */ + setAlias(opts) { const { redis, config } = this; const { jwt: { defaultAudience } } = config; + const { username, alias, data } = opts; if (data && data[USERS_ALIAS_FIELD]) { throw new ModelError(ERR_ALIAS_ALREADY_ASSIGNED); } - const assigned = redis.hsetnx(USERS_ALIAS_TO_LOGIN, alias, username); - if (assigned === 0) { - throw new ModelError(ERR_ALIAS_ALREADY_TAKEN); - } - return redis - .pipeline() - .sadd(USERS_PUBLIC_INDEX, username) - .hset(generateKey(username, USERS_DATA), USERS_ALIAS_FIELD, alias) - .hset(generateKey(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, JSON.stringify(alias)) - .exec(); + .hsetnx(USERS_ALIAS_TO_LOGIN, alias, username) + .then(assigned => { + if (assigned === 0) { + throw new ModelError(ERR_ALIAS_ALREADY_TAKEN); + } + + return redis + .pipeline() + .sadd(USERS_PUBLIC_INDEX, username) + .hset(generateKey(username, USERS_DATA), USERS_ALIAS_FIELD, alias) + .hset(generateKey(username, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, JSON.stringify(alias)) + .exec(); + }); }, /** @@ -375,7 +384,7 @@ exports.User = { return null; }) // setting alias, if we can - .tap(activate && alias ? () => exports.User.setAlias.call(this, username, alias) : noop); + .tap(activate && alias ? () => exports.User.setAlias.call(this, { username, alias }) : noop); }, /** @@ -470,7 +479,7 @@ exports.User = { * @param username * @returns {*} */ - unlock(username) { + unlock({ username }) { const { redis, config } = this; const { jwt: { defaultAudience } } = config; diff --git a/src/model/usermodel.js b/src/model/usermodel.js index 61c8409b2..f35b1cfb7 100644 --- a/src/model/usermodel.js +++ b/src/model/usermodel.js @@ -60,12 +60,11 @@ exports.User = { /** * Sets alias to the user by username - * @param username - * @param alias + * @param opts * @returns {*} */ - setAlias(username, alias) { - return storage.User.setAlias.call(this, username, alias); + setAlias(opts) { + return storage.User.setAlias.call(this, opts); }, /** @@ -132,11 +131,11 @@ exports.User = { /** * Unlock banned user - * @param username + * @param opts * @returns {*} */ - unlock(username) { - return storage.User.unlock.call(this, username); + unlock(opts) { + return storage.User.unlock.call(this, opts); }, }; diff --git a/src/utils/isPublic.js b/src/utils/isPublic.js new file mode 100644 index 000000000..486928ebc --- /dev/null +++ b/src/utils/isPublic.js @@ -0,0 +1,24 @@ +/** + * Created by Stainwoortsel on 03.07.2016. + */ +const get = require('lodash/get'); +const { ModelError, ERR_USERNAME_NOT_FOUND } = require('../model/modelError'); +const { USERS_ALIAS_FIELD } = require('../constants.js'); + +module.exports = function isPublic(username, audiences) { + return metadata => { + let notFound = true; + + // iterate over passed audiences + audiences.forEach(audience => { + if (notFound && get(metadata, [audience, USERS_ALIAS_FIELD]) === username) { + notFound = false; + } + }); + + if (notFound) { + throw new ModelError(ERR_USERNAME_NOT_FOUND); + } + }; +}; + diff --git a/test/suites/reg.js b/test/suites/reg.js deleted file mode 100644 index e76da681f..000000000 --- a/test/suites/reg.js +++ /dev/null @@ -1,35 +0,0 @@ -/* global inspectPromise */ -const { expect } = require('chai'); -const times = require('lodash/times'); - -describe('#reg', function registerSuite() { - const headers = { routingKey: 'users.register' }; - - beforeEach(global.startService); - afterEach(global.clearRedis); - - it('must be able to create user without validations and return user object and jwt token', function test() { - const opts = { - username: 'v@makeomatic.ru', - password: 'mynicepassword', - audience: 'matic.ninja', - }; - - return this.users - .router(opts, headers) - .reflect() - .then(inspectPromise(true)) - .then(registered => { -console.log(registered); - expect(registered).to.have.ownProperty('jwt'); - expect(registered).to.have.ownProperty('user'); - expect(registered.user.username).to.be.eq(opts.username); - expect(registered.user).to.have.ownProperty('metadata'); - expect(registered.user.metadata).to.have.ownProperty('matic.ninja'); - expect(registered.user.metadata).to.have.ownProperty('*.localhost'); - expect(registered.user).to.not.have.ownProperty('password'); - expect(registered.user).to.not.have.ownProperty('audience'); - }); - }); - -}); From 0860363439d4e8fa0dfd1da9b36886d227dd713a Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Mon, 4 Jul 2016 03:12:36 +0300 Subject: [PATCH 26/38] fix: fixing bugs through the tests: usermodel, login, updateMeta, tokens, attempts --- src/actions/login.js | 11 +---------- src/actions/updatePassword.js | 5 ++--- src/model/storages/redisstorage.js | 2 +- src/model/usermodel.js | 11 +++++++++-- src/utils/jwt.js | 3 ++- test/suites/updateMetadata.js | 1 - 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/actions/login.js b/src/actions/login.js index 6e765dd7a..512029b04 100644 --- a/src/actions/login.js +++ b/src/actions/login.js @@ -24,14 +24,6 @@ module.exports = function login(opts) { return jwt.login.call(this, username, audience); } - function enrichError(err) { - if (remoteip) { - err.loginAttempts = theAttempts.count(); - } - - return err; - } - return Promise .bind(this, opts.username) .then(User.getOne) @@ -41,6 +33,5 @@ module.exports = function login(opts) { .tap(verifyIp ? ({ username, ip }) => theAttempts.drop(username, ip) : noop) .tap(isActive) .tap(isBanned) - .then(getUserInfo) - .catch(verifyIp ? enrichError : e => { throw e; }); + .then(getUserInfo); }; diff --git a/src/actions/updatePassword.js b/src/actions/updatePassword.js index b66e33476..84742c0aa 100644 --- a/src/actions/updatePassword.js +++ b/src/actions/updatePassword.js @@ -67,9 +67,8 @@ module.exports = exports = function updatePassword(opts) { } if (remoteip) { - promise = promise.tap(function resetLock(username) { - const theAttempts = new Attempts(this); - return theAttempts.drop(username, remoteip); + promise = promise.tap(username => { + return (new Attempts(this)).drop(username, remoteip); }); } diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index 80b459a14..5f7fef83e 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -67,7 +67,7 @@ exports.User = { .exec() .spread((aliasToUsername, exists, data) => { if (aliasToUsername[1]) { - return exports.User.getOne(aliasToUsername[1]); + return exports.User.getOne.call(this, aliasToUsername[1]); } if (!exists[1]) { diff --git a/src/model/usermodel.js b/src/model/usermodel.js index f35b1cfb7..6d26d954d 100644 --- a/src/model/usermodel.js +++ b/src/model/usermodel.js @@ -168,7 +168,14 @@ class AttemptsClass { this.loginAttempts = attempts; if (this.loginAttempts > lockAfterAttempts) { const duration = moment().add(keepLoginAttempts, 'seconds').toNow(true); - throw new ModelError(ERR_ATTEMPTS_LOCKED, duration); + const verifyIp = ip && lockAfterAttempts > 0; + + const err = new ModelError(ERR_ATTEMPTS_LOCKED, duration); + if (verifyIp) { + err.loginAttempts = this.loginAttempts; + } + + throw err; } }); } @@ -181,7 +188,7 @@ class AttemptsClass { */ drop(username, ip) { this.loginAttempts = 0; - return storage.Attempts.drop.call(this, username, ip); + return storage.Attempts.drop.call(this.context, username, ip); } /** diff --git a/src/utils/jwt.js b/src/utils/jwt.js index ae2004f77..f4071afb7 100644 --- a/src/utils/jwt.js +++ b/src/utils/jwt.js @@ -60,6 +60,7 @@ exports.logout = function logout(token, audience) { const { config } = this; const { jwt: jwtConfig } = config; const { hashingFunction: algorithm, secret, issuer } = jwtConfig; + const that = this; return jwt .verifyAsync(token, secret, { issuer, audience, algorithms: [algorithm] }) @@ -68,7 +69,7 @@ exports.logout = function logout(token, audience) { throw new ModelError(ERR_TOKEN_INVALID); }) .then(function decodedToken(decoded) { - return Tokens.drop(decoded.username, token); + return Tokens.drop.call(that, decoded.username, token); }) .return({ success: true }); }; diff --git a/test/suites/updateMetadata.js b/test/suites/updateMetadata.js index edf66ae32..b667696ca 100644 --- a/test/suites/updateMetadata.js +++ b/test/suites/updateMetadata.js @@ -78,7 +78,6 @@ describe('#updateMetadata', function getMetadataSuite() { .then(inspectPromise()) .then(data => { const [mainData, extraData] = data; - expect(mainData.$set).to.be.eq('OK'); expect(mainData.$incr.b).to.be.eq(2); expect(extraData.$incr.b).to.be.eq(3); From 12c5df107435e9b1f6edbb55f8666a730d0f99ae Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Mon, 4 Jul 2016 03:21:28 +0300 Subject: [PATCH 27/38] fix: jwt logout decodedTokens, arrow-function better --- src/utils/jwt.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils/jwt.js b/src/utils/jwt.js index f4071afb7..9de0b3557 100644 --- a/src/utils/jwt.js +++ b/src/utils/jwt.js @@ -60,7 +60,6 @@ exports.logout = function logout(token, audience) { const { config } = this; const { jwt: jwtConfig } = config; const { hashingFunction: algorithm, secret, issuer } = jwtConfig; - const that = this; return jwt .verifyAsync(token, secret, { issuer, audience, algorithms: [algorithm] }) @@ -68,8 +67,8 @@ exports.logout = function logout(token, audience) { this.log.debug('error decoding token', err); throw new ModelError(ERR_TOKEN_INVALID); }) - .then(function decodedToken(decoded) { - return Tokens.drop.call(that, decoded.username, token); + .then(decoded => { + return Tokens.drop.call(this, decoded.username, token); }) .return({ success: true }); }; From 7d7bfb47e036c86bc70b5a76c8237e957b4b6b50 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Tue, 5 Jul 2016 03:18:04 +0300 Subject: [PATCH 28/38] fix: fixing bugs through the tests: jwt/send-mail --- src/model/storages/redisstorage.js | 6 +++--- src/utils/jwt.js | 2 ++ src/utils/send-email.js | 12 +++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index 5f7fef83e..56be4256d 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -259,7 +259,7 @@ exports.User = { * @param {Object} metadata * @returns {object} */ - _handleAudience(pipeline, key, metadata) { + handleAudience(pipeline, key, metadata) { const $remove = metadata.$remove; const $removeOps = $remove && $remove.length || 0; if ($removeOps > 0) { @@ -301,11 +301,11 @@ exports.User = { if (metadata) { const pipe = redis.pipeline(); const metaOps = is.array(metadata) ? metadata : [metadata]; - const operations = metaOps.map((meta, idx) => exports.User._handleAudience(pipe, keys[idx], metadata)); + const operations = metaOps.map((meta, idx) => exports.User.handleAudience(pipe, keys[idx], meta)); return pipe.exec().then(res => mapMetaResponse(operations, res)); } - return exports.User.executeUpdateMetaScript(username, audience, script); + return exports.User.executeUpdateMetaScript.call(this, username, audience, script); }, /** diff --git a/src/utils/jwt.js b/src/utils/jwt.js index 9de0b3557..67d52bac3 100644 --- a/src/utils/jwt.js +++ b/src/utils/jwt.js @@ -6,6 +6,8 @@ const flakeIdGen = new FlakeId(); const { User, Tokens } = require('../model/usermodel'); const { ModelError, ERR_TOKEN_INVALID, ERR_TOKEN_AUDIENCE_MISMATCH } = require('../model/modelError'); +// TODO: merge this code with master!!! + /** * Logs user in and returns JWT and User Object * @param {String} username diff --git a/src/utils/send-email.js b/src/utils/send-email.js index 1e14abe73..90edc6493 100644 --- a/src/utils/send-email.js +++ b/src/utils/send-email.js @@ -10,6 +10,9 @@ const { MAIL_ACTIVATE, MAIL_RESET, MAIL_PASSWORD, MAIL_REGISTER } = require('../ const { Tokens } = require('../model/usermodel'); const { ModelError, ERR_TOKEN_CANT_DECODE, ERR_EMAIL_ALREADY_SENT, ERR_TOKEN_BAD_EMAIL, ERR_TOKEN_EXPIRED, ERR_TOKEN_MISS_EMAIL } = require('../model/modelError'); + +// TODO: merge this code with master!!! + /** * Throttled error * @param {Mixed} reply @@ -191,12 +194,11 @@ exports.send = function sendEmail(email, type = MAIL_ACTIVATE, wait = false) { */ exports.verify = function verifyToken(string, namespace = MAIL_ACTIVATE, expires) { const { config: { validation: { secret: validationSecret, algorithm } } } = this; - const that = this; return exports .safeDecode .call(this, algorithm, validationSecret, string) - .then(function inspectResult(message) { + .then(message => { const { email, token } = message; if (!email || !token) { @@ -207,9 +209,9 @@ exports.verify = function verifyToken(string, namespace = MAIL_ACTIVATE, expires // .bind(this) // .then(() => Tokens.getEmailThrottleToken(namespace, token)) // .then(function inspectAssociatedData(associatedEmail) { - .bind(that, [namespace, token]) + .bind(this, [namespace, token]) .spread(Tokens.getEmailThrottleToken) - .then(function inspectAssociatedData(associatedEmail) { + .then(associatedEmail => { if (!associatedEmail) { throw new ModelError(ERR_TOKEN_EXPIRED); } @@ -219,7 +221,7 @@ exports.verify = function verifyToken(string, namespace = MAIL_ACTIVATE, expires } if (expires) { - return Tokens.dropEmailThrottleToken.call(that, namespace, token); + return Tokens.dropEmailThrottleToken.call(this, namespace, token); } return null; From 785ccfdfdfc7680f5b16225a9c9f39e42be0a551 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Tue, 5 Jul 2016 19:23:15 +0300 Subject: [PATCH 29/38] fix: fixing bugs through the test (hope, in last time): jwt, send-mail --- src/model/storages/redisstorage.js | 4 ++-- src/utils/jwt.js | 34 +++++++++++++++--------------- src/utils/send-email.js | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index 56be4256d..745a5f494 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -569,7 +569,7 @@ exports.Tokens = { const { redis, config } = this; const { jwt: { ttl } } = config; const tokensHolder = generateKey(username, USERS_TOKENS); - return redis.zscoreBuffer(tokensHolder, token).then(function getLastAccess(_score) { + return redis.zscoreBuffer(tokensHolder, token).then(_score => { // parseResponse const score = parseInt(_score, 10); @@ -603,7 +603,7 @@ exports.Tokens = { setEmailThrottleState(type, email) { const { redis, config } = this; const throttleEmailsKey = generateKey(`vthrottle-${type}`, email); - const { validation: throttle } = config; + const { validation: { throttle } } = config; const throttleArgs = [throttleEmailsKey, 1, 'NX']; if (throttle > 0) { diff --git a/src/utils/jwt.js b/src/utils/jwt.js index 67d52bac3..2aa52fe5a 100644 --- a/src/utils/jwt.js +++ b/src/utils/jwt.js @@ -1,13 +1,10 @@ const Promise = require('bluebird'); const jwt = Promise.promisifyAll(require('jsonwebtoken')); const FlakeId = require('flake-idgen'); -const noop = require('lodash/noop'); const flakeIdGen = new FlakeId(); const { User, Tokens } = require('../model/usermodel'); const { ModelError, ERR_TOKEN_INVALID, ERR_TOKEN_AUDIENCE_MISMATCH } = require('../model/modelError'); -// TODO: merge this code with master!!! - /** * Logs user in and returns JWT and User Object * @param {String} username @@ -41,15 +38,15 @@ exports.login = function login(username, _audience) { username, metadata: User.getMeta.call(this, username, audience), }) - .then(function remap(props) { - return { - jwt: props.jwt, - user: { - username: props.username, - metadata: props.metadata, - }, - }; - }); + .then(function remap(props) { + return { + jwt: props.jwt, + user: { + username: props.username, + metadata: props.metadata, + }, + }; + }); }; /** @@ -80,9 +77,10 @@ exports.logout = function logout(token, audience) { * @param {String} username */ exports.reset = function reset(username) { - return Tokens.drop(username); + return Tokens.drop.call(this, username); }; + /** * Verifies token and returns decoded version of it * @param {String} token @@ -101,15 +99,17 @@ exports.verify = function verifyToken(token, audience, peek) { this.log.debug('invalid token passed: %s', token, err); throw new ModelError(ERR_TOKEN_INVALID); }) - .then(function decodedToken(decoded) { + .then(decoded => { if (audience.indexOf(decoded.aud) === -1) { throw new ModelError(ERR_TOKEN_AUDIENCE_MISMATCH); } const { username } = decoded; - const lastAccess = Tokens - .lastAccess(username, token) - .then(peek ? () => Tokens.add(username, token) : noop); + let lastAccess = Tokens.lastAccess.call(this, username, token); + + if (!peek) { + lastAccess = lastAccess.then(() => Tokens.add.call(this, username, token)); + } return lastAccess.return(decoded); }); diff --git a/src/utils/send-email.js b/src/utils/send-email.js index 90edc6493..74839b4d1 100644 --- a/src/utils/send-email.js +++ b/src/utils/send-email.js @@ -156,7 +156,7 @@ exports.send = function sendEmail(email, type = MAIL_ACTIVATE, wait = false) { .bind(this, [type, email, activationSecret]) .spread(Tokens.setEmailThrottleState) .then(isThrottled(false)) - .then(Tokens.setEmailThrottleToken); + .then(() => Tokens.setEmailThrottleToken.call(this, type, email, activationSecret)); }) .then(function definedSubjectAndSend({ context, emailTemplate }) { const mail = { From 8ea411f7c6d629dcd4d0c6c93023d9491109fcb0 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 8 Jul 2016 02:13:04 +0300 Subject: [PATCH 30/38] fix: some cosmetic fixes --- src/actions/activate.js | 8 +++++++- src/actions/alias.js | 6 +++++- src/actions/challenge.js | 4 +++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/actions/activate.js b/src/actions/activate.js index 8d023b750..bcc92a72a 100644 --- a/src/actions/activate.js +++ b/src/actions/activate.js @@ -2,7 +2,13 @@ const Promise = require('bluebird'); const emailVerification = require('../utils/send-email.js'); const jwt = require('../utils/jwt.js'); const { User } = require('../model/usermodel'); +const { MAIL_ACTIVATE } = require('../constants'); +/** + * Activate existing users + * @param opts + * @return {Promise} + */ module.exports = function verifyChallenge(opts) { // TODO: add security logs // var remoteip = opts.remoteip; @@ -11,7 +17,7 @@ module.exports = function verifyChallenge(opts) { const audience = opts.audience || config.defaultAudience; function verifyToken() { - return emailVerification.verify.call(this, token, 'activate', config.validation.ttl > 0); + return emailVerification.verify.call(this, token, MAIL_ACTIVATE, config.validation.ttl > 0); } function hook(user) { diff --git a/src/actions/alias.js b/src/actions/alias.js index 2aa6664cc..e1f8daa0e 100644 --- a/src/actions/alias.js +++ b/src/actions/alias.js @@ -3,7 +3,11 @@ const isActive = require('../utils/isActive'); const isBanned = require('../utils/isBanned'); const { User } = require('../model/usermodel'); - +/** + * Assign alias to user + * @param opts + * @return {Promise} + */ module.exports = function assignAlias(opts) { const { username, alias } = opts; diff --git a/src/actions/challenge.js b/src/actions/challenge.js index 63ebe4b1a..a6048eadb 100644 --- a/src/actions/challenge.js +++ b/src/actions/challenge.js @@ -2,7 +2,9 @@ const Promise = require('bluebird'); const emailChallenge = require('../utils/send-email.js'); const isActive = require('../utils/isActive'); const { User } = require('../model/usermodel'); -const { ModelError, ERR_ACCOUNT_NOT_ACTIVATED, ERR_USERNAME_ALREADY_ACTIVE } = require('../model/modelError'); +const { ModelError, + ERR_ACCOUNT_NOT_ACTIVATED, + ERR_USERNAME_ALREADY_ACTIVE } = require('../model/modelError'); module.exports = function sendChallenge(message) { const { username } = message; From d6cff19a67e06816978c95d34694b98d5e94361b Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 8 Jul 2016 02:17:25 +0300 Subject: [PATCH 31/38] fix: some another cosmetic fixes --- src/actions/challenge.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/actions/challenge.js b/src/actions/challenge.js index a6048eadb..63ebe4b1a 100644 --- a/src/actions/challenge.js +++ b/src/actions/challenge.js @@ -2,9 +2,7 @@ const Promise = require('bluebird'); const emailChallenge = require('../utils/send-email.js'); const isActive = require('../utils/isActive'); const { User } = require('../model/usermodel'); -const { ModelError, - ERR_ACCOUNT_NOT_ACTIVATED, - ERR_USERNAME_ALREADY_ACTIVE } = require('../model/modelError'); +const { ModelError, ERR_ACCOUNT_NOT_ACTIVATED, ERR_USERNAME_ALREADY_ACTIVE } = require('../model/modelError'); module.exports = function sendChallenge(message) { const { username } = message; From 1cabd9cf49a6867d644161a599abf780a8174a4d Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Fri, 8 Jul 2016 23:54:21 +0300 Subject: [PATCH 32/38] fix: UNIX-style end of strings in scripts (to avoid bash errors) --- src/model/storages/redisstorage.js | 4 ++-- test/docker.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index 745a5f494..e09f6de83 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -132,7 +132,7 @@ exports.User = { return { users, cursor: offset + limit, - page: Math.floor(offset / limit + 1), + page: Math.floor((offset / limit) + 1), pages: Math.ceil(length / limit), }; }); @@ -261,7 +261,7 @@ exports.User = { */ handleAudience(pipeline, key, metadata) { const $remove = metadata.$remove; - const $removeOps = $remove && $remove.length || 0; + const $removeOps = ($remove && $remove.length || 0); if ($removeOps > 0) { pipeline.hdel(key, $remove); } diff --git a/test/docker.sh b/test/docker.sh index 3739d0f51..569dfb788 100755 --- a/test/docker.sh +++ b/test/docker.sh @@ -16,7 +16,7 @@ COMPOSE="docker-compose -f $DC" if ! [ -x "$(which docker-compose)" ]; then mkdir $DIR/.bin - curl -L https://github.com/docker/compose/releases/download/1.7.1/docker-compose-`uname -s`-`uname -m` > $DIR/.bin/docker-compose + curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VER}/docker-compose-`uname -s`-`uname -m` > $DIR/.bin/docker-compose chmod +x $DIR/.bin/docker-compose fi From 71c46a53fb3786b29fe6d69cf9de34476ab37f71 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Sat, 9 Jul 2016 00:02:41 +0300 Subject: [PATCH 33/38] fix: some lint sugar --- src/model/storages/redisstorage.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index e09f6de83..934bf88b8 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -261,21 +261,21 @@ exports.User = { */ handleAudience(pipeline, key, metadata) { const $remove = metadata.$remove; - const $removeOps = ($remove && $remove.length || 0); + const $removeOps = ($remove && $remove.length) || 0; if ($removeOps > 0) { pipeline.hdel(key, $remove); } const $set = metadata.$set; const $setKeys = $set && Object.keys($set); - const $setLength = $setKeys && $setKeys.length || 0; + const $setLength = ($setKeys && $setKeys.length) || 0; if ($setLength > 0) { pipeline.hmset(key, mapValues($set, JSONStringify)); } const $incr = metadata.$incr; const $incrFields = $incr && Object.keys($incr); - const $incrLength = $incrFields && $incrFields.length || 0; + const $incrLength = ($incrFields && $incrFields.length) || 0; if ($incrLength > 0) { $incrFields.forEach(fieldName => { pipeline.hincrby(key, fieldName, $incr[fieldName]); From db222e4ae90b8b5777173476aea7c5937ae7147a Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Sat, 9 Jul 2016 00:05:51 +0300 Subject: [PATCH 34/38] fix: and little fix for math operation with page of list in redisstorage:getList --- src/model/storages/redisstorage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index 934bf88b8..f3e19de77 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -132,7 +132,7 @@ exports.User = { return { users, cursor: offset + limit, - page: Math.floor((offset / limit) + 1), + page: Math.floor(offset / limit) + 1, pages: Math.ceil(length / limit), }; }); From aaad1a40af45b9f1ba72b2d5fbb635375a79bdf1 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Sat, 9 Jul 2016 00:07:25 +0300 Subject: [PATCH 35/38] fix: please linter, please... --- src/users.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/users.js b/src/users.js index 665e7071f..7cba05ee5 100644 --- a/src/users.js +++ b/src/users.js @@ -4,7 +4,7 @@ const Errors = require('common-errors'); const merge = require('lodash/merge'); const fsort = require('redis-filtered-sort'); const { NotImplementedError } = Errors; -const defaultOpts = require('./defaults.js'); +const _defaultOpts = require('./defaults.js'); /** * @namespace Users @@ -15,7 +15,7 @@ module.exports = class Users extends Mservice { * Configuration options for the service * @type {Object} */ - static defaultOpts = defaultOpts; + static defaultOpts = _defaultOpts; /** * @namespace Users From 98b5770e8aca97ce88ae8219e19688dcb3de3850 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Tue, 19 Jul 2016 01:46:15 +0300 Subject: [PATCH 36/38] feat: mongo docker-compose --- test/docker-compose-mongo.yml | 64 +++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 test/docker-compose-mongo.yml diff --git a/test/docker-compose-mongo.yml b/test/docker-compose-mongo.yml new file mode 100644 index 000000000..f995db618 --- /dev/null +++ b/test/docker-compose-mongo.yml @@ -0,0 +1,64 @@ +version: '2' + +services: + mongo-conf: + container_name: mongo-conf + hostname: mongo-conf + expose: + - "27010" + - "28017" + image: stainwoortsel/mongo-config + + mongo-sh-01: + container_name: mongo-sh-01 + hostname: mongo-sh-01 + expose: + - "27020" + - "28017" + image: stainwoortsel/mongo-shard + + mongo-sh-02: + container_name: mongo-sh-02 + hostname: mongo-sh-02 + links: + - mongo-sh-01 + expose: + - "27020" + - "28017" + image: stainwoortsel/mongo-shard + + mongos: + container_name: mongos + hostname: mongos + links: + - mongo-conf + - mongo-sh-01 + - mongo-sh-02 + command: mongos --configdb mongo-conf:27010 --port 27100 + expose: + - "27010" + - "27020" + - "27017" + - "28017" + - "27100" + ports: + - "27100:27100" + image: stainwoortsel/mongo-mongos + + tester: + container_name: tester + image: makeomatic/node:6.2.2-tester + links: + - mongo-conf + - mongo-sh-01 + - mongo-sh-02 + - rabbitmq + - mongos + working_dir: /src + volumes: + - ${PWD}:/src + environment: + NODE_ENV: "test" + DEBUG: ${DEBUG} + SKIP_REBUILD: ${SKIP_REBUILD} + command: tail -f /dev/null From 20fa21df4b59c64c13eec5888fad5cd02ecf612c Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Mon, 1 Aug 2016 21:59:02 +0300 Subject: [PATCH 37/38] feat: making test suites a bit more independed from storages course, the key is in an abstraction. the user model abstraction now allows to make such operation --- package.json | 4 +++- src/model/modelError.js | 2 ++ src/model/storages/mongostorage.js | 3 +++ src/model/storages/redisstorage.js | 24 ++++++++++++++++++++++++ src/model/usermodel.js | 9 +++++++++ test/config.js | 8 ++++++++ test/docker-compose.yml | 9 +++++++++ test/suites/activate.js | 8 +++----- test/suites/list.js | 27 ++++++++++++--------------- test/suites/login.js | 4 ++-- test/suites/requestPassword.js | 6 +++--- test/suites/updatePassword.js | 6 +++--- 12 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 src/model/storages/mongostorage.js diff --git a/package.json b/package.json index 791c79f93..88c329695 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "ms-users", + "version": "0.0.1", "description": "Core of the microservice for handling users", "main": "./lib/index.js", "scripts": { @@ -33,12 +34,13 @@ "jsonwebtoken": "^7.0.1", "lodash": "^4.5.0", "moment": "^2.14.1", + "mongodb": "^2.2.4", "ms-amqp-transport": "^3.1.0", "ms-conf": "^2.1.0", "ms-mailer-client": "^3.0.0", "ms-mailer-templates": "^0.7.0", "ms-validation": "^2.0.0", - "mservice": "^2.6.0", + "mservice": "file:///C:\\dev\\nodejs\\ms\\mservice", "node-uuid": "^1.4.7", "password-generator": "^2.0.2", "redis-filtered-sort": "^1.2.0", diff --git a/src/model/modelError.js b/src/model/modelError.js index 296f2f176..05fd81545 100644 --- a/src/model/modelError.js +++ b/src/model/modelError.js @@ -52,6 +52,8 @@ const ErrorTypes = { genErr(121, 412, 'Account hasn\'t been activated'), ERR_ACCOUNT_ALREADY_ACTIVATED: genErr(122, 417, (user) => (`Account ${user} was already activated`)), + ERR_ACCOUNT_ALREADY_DISACTIVATED: + genErr(122, 417, (user) => (`Account ${user} was already disactivated`)), ERR_ACCOUNT_IS_LOCKED: genErr(123, 423, 'Account has been locked'), ERR_ACCOUNT_IS_ALREADY_EXISTS: diff --git a/src/model/storages/mongostorage.js b/src/model/storages/mongostorage.js new file mode 100644 index 000000000..109f0be13 --- /dev/null +++ b/src/model/storages/mongostorage.js @@ -0,0 +1,3 @@ +/** + * Created by Stainwoortsel on 01.08.2016. + */ diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index f3e19de77..ed72d1eb8 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -15,6 +15,7 @@ const { ERR_ALIAS_ALREADY_ASSIGNED, ERR_ALIAS_ALREADY_TAKEN, ERR_USERNAME_NOT_EXISTS, ERR_USERNAME_NOT_FOUND, ERR_TOKEN_FORGED, ERR_CAPTCHA_WRONG_USERNAME, ERR_ATTEMPTS_TO_MUCH_REGISTERED, ERR_ALIAS_ALREADY_EXISTS, ERR_ACCOUNT_IS_ALREADY_EXISTS, ERR_ACCOUNT_ALREADY_ACTIVATED, + ERR_ACCOUNT_ALREADY_DISACTIVATED, } = require('../modelError'); /* ERR_USERNAME_ALREADY_ACTIVE, @@ -445,6 +446,28 @@ exports.User = { }); }, + /** + * Disactivate user + * @param username + * @returns {*} + */ + disactivate(username) { + const { redis } = this; + const userKey = generateKey(username, USERS_DATA); + + return redis + .pipeline() + .hget(userKey, USERS_ACTIVE_FLAG) + .hset(userKey, USERS_ACTIVE_FLAG, 'false') + .exec() + .spread(function pipeResponse(isActive) { + const status = isActive[1]; + if (status === 'false') { + throw new ModelError(ERR_ACCOUNT_ALREADY_DISACTIVATED, username); + } + }); + }, + /** * Ban user * @param username @@ -721,3 +744,4 @@ exports.Utils = { }; }, }; + diff --git a/src/model/usermodel.js b/src/model/usermodel.js index 6d26d954d..3ae0f257d 100644 --- a/src/model/usermodel.js +++ b/src/model/usermodel.js @@ -119,6 +119,15 @@ exports.User = { return storage.User.activate.call(this, username); }, + /** + * Disactivate user + * @param username + * @returns {*} + */ + disactivate(username) { + return storage.User.disactivate.call(this, username); + }, + /** * Ban user * @param username diff --git a/test/config.js b/test/config.js index 789ab96a5..8f4bc2015 100644 --- a/test/config.js +++ b/test/config.js @@ -3,6 +3,13 @@ const { expect } = require('chai'); global.Promise = require('bluebird'); +global.MONGO = { + standalone: { + connectionString: 'mongodb://mongo/msusers', + options: {}, + } +}; + global.AMQP = { connection: { host: 'rabbitmq', @@ -20,6 +27,7 @@ global.REDIS = { const config = { amqp: global.AMQP, redis: global.REDIS, + mongo: global.MONGO, logger: true, debug: true, validation: { diff --git a/test/docker-compose.yml b/test/docker-compose.yml index ae0d37533..68b5409b7 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -1,6 +1,14 @@ version: '2' services: + mongo: + image: mvertes/alpine-mongo + container_name: mongo + hostname: mongo + expose: + - "27017" + - "28017" + redis-1: container_name: redis-1 hostname: redis-1 @@ -57,6 +65,7 @@ services: - redis-1 - redis-2 - redis-3 + - mongo - rabbitmq - redis-client working_dir: /src diff --git a/test/suites/activate.js b/test/suites/activate.js index b664ffd71..8705cd638 100644 --- a/test/suites/activate.js +++ b/test/suites/activate.js @@ -3,7 +3,7 @@ const { expect } = require('chai'); const URLSafeBase64 = require('urlsafe-base64'); describe('#activate', function activateSuite() { - const redisKey = require('../../src/utils/key.js'); + const { Tokens } = require('../../src/model/usermodel'); const headers = { routingKey: 'users.activate' }; const emailValidation = require('../../src/utils/send-email.js'); const email = 'v@aminev.me'; @@ -44,8 +44,7 @@ describe('#activate', function activateSuite() { }); beforeEach(function pretest() { - const secretKey = redisKey('vsecret-activate', this.uuid); - return this.users.redis.set(secretKey, email); + return Tokens.setEmailThrottleToken.call(this.users, 'activate', email, this.uuid); }); it('must reject activation when account is already activated', function test() { @@ -66,8 +65,7 @@ describe('#activate', function activateSuite() { }); beforeEach('insert token', function pretest() { - const secretKey = redisKey('vsecret-activate', this.uuid); - return this.users.redis.set(secretKey, email); + return Tokens.setEmailThrottleToken.call(this.users, 'activate', email, this.uuid); }); it('must activate account when challenge token is correct and not expired', function test() { diff --git a/test/suites/list.js b/test/suites/list.js index 2a91b926d..ba1cedff4 100644 --- a/test/suites/list.js +++ b/test/suites/list.js @@ -1,39 +1,35 @@ /* global inspectPromise */ const { expect } = require('chai'); +const { User } = require('../../src/model/usermodel'); const ld = require('lodash'); describe('#list', function listSuite() { this.timeout(10000); - const redisKey = require('../../src/utils/key.js'); const faker = require('faker'); const headers = { routingKey: 'users.list' }; beforeEach(global.startService); afterEach(global.clearRedis); - beforeEach(function populateRedis() { + beforeEach(function populateUsers() { const audience = this.users._config.jwt.defaultAudience; const promises = []; const { USERS_INDEX, USERS_METADATA } = require('../../src/constants.js'); ld.times(105, () => { - const user = { - id: faker.internet.email(), + const userData = { + username: faker.internet.email(), + audience, metadata: { - firstName: faker.name.firstName(), - lastName: faker.name.lastName(), + $set: { + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + }, }, }; - - promises.push(this.users._redis - .pipeline() - .sadd(USERS_INDEX, user.id) - .hmset( - redisKey(user.id, USERS_METADATA, audience), - ld.mapValues(user.metadata, JSON.stringify.bind(JSON)) - ) - .exec() + promises.push( + User.create.call(this.users, userData.username, '', '123', true).then(() => User.setMeta.call(this.users, userData)) ); }); @@ -52,6 +48,7 @@ describe('#list', function listSuite() { }, headers) .reflect() .then(result => { + try { expect(result.isFulfilled()).to.be.eq(true); expect(result.value().page).to.be.eq(6); diff --git a/test/suites/login.js b/test/suites/login.js index d3dff1bec..9acd78f09 100644 --- a/test/suites/login.js +++ b/test/suites/login.js @@ -3,7 +3,7 @@ const { expect } = require('chai'); const ld = require('lodash'); describe('#login', function loginSuite() { - const redisKey = require('../../src/utils/key.js'); + const { User } = require('../../src/model/usermodel'); const headers = { routingKey: 'users.login' }; const user = { username: 'v@makeomatic.ru', password: 'nicepassword', audience: '*.localhost' }; const userWithValidPassword = { username: 'v@makeomatic.ru', password: 'nicepassword1', audience: '*.localhost' }; @@ -79,7 +79,7 @@ describe('#login', function loginSuite() { describe('account: banned', function suite() { beforeEach(function pretest() { - return this.users.redis.hset(redisKey(user.username, USERS_DATA), USERS_BANNED_FLAG, 'true'); + return User.lock.call(this.users, { username: user.username }); }); it('must reject login', function test() { diff --git a/test/suites/requestPassword.js b/test/suites/requestPassword.js index c7b9f075c..ad5f6bb8f 100644 --- a/test/suites/requestPassword.js +++ b/test/suites/requestPassword.js @@ -2,7 +2,7 @@ const { expect } = require('chai'); describe('#requestPassword', function requestPasswordSuite() { - const redisKey = require('../../src/utils/key.js'); + const { User } = require('../../src/model/usermodel'); const headers = { routingKey: 'users.requestPassword' }; const username = 'v@makeomatic.ru'; const audience = 'requestPassword'; @@ -27,7 +27,7 @@ describe('#requestPassword', function requestPasswordSuite() { describe('account: inactive', function suite() { beforeEach(function pretest() { - return this.users.redis.hset(redisKey(username, USERS_DATA), USERS_ACTIVE_FLAG, 'false'); + return User.disactivate.call(this.users, username); }); it('must fail when account is inactive', function test() { @@ -43,7 +43,7 @@ describe('#requestPassword', function requestPasswordSuite() { describe('account: banned', function suite() { beforeEach(function pretest() { - return this.users.redis.hset(redisKey(username, USERS_DATA), USERS_BANNED_FLAG, 'true'); + return User.lock.call(this.users, { username }); }); it('must fail when account is banned', function test() { diff --git a/test/suites/updatePassword.js b/test/suites/updatePassword.js index 9bf07321b..906ab9237 100644 --- a/test/suites/updatePassword.js +++ b/test/suites/updatePassword.js @@ -2,7 +2,7 @@ const { expect } = require('chai'); describe('#updatePassword', function updatePasswordSuite() { - const redisKey = require('../../src/utils/key.js'); + const { User } = require('../../src/model/usermodel'); const headers = { routingKey: 'users.updatePassword' }; const username = 'v@makeomatic.ru'; const password = '123'; @@ -29,7 +29,7 @@ describe('#updatePassword', function updatePasswordSuite() { describe('user: inactive', function suite() { beforeEach(function pretest() { - return this.users.redis.hset(redisKey(username, USERS_DATA), USERS_ACTIVE_FLAG, 'false'); + return User.disactivate.call(this.users, username); }); it('must reject updating password for an inactive account on username+password update', function test() { @@ -45,7 +45,7 @@ describe('#updatePassword', function updatePasswordSuite() { describe('user: banned', function suite() { beforeEach(function pretest() { - return this.users.redis.hset(redisKey(username, USERS_DATA), USERS_BANNED_FLAG, 'true'); + return User.lock.call(this.users, { username }); }); it('must reject updating password for an inactive account on username+password update', function test() { From 24e36742b0d3e48c217cf6542cc557db893de1d8 Mon Sep 17 00:00:00 2001 From: Vadim Egorov Date: Wed, 10 Aug 2016 11:13:37 +0300 Subject: [PATCH 38/38] feat: methods in mongo adapter, before HUGE refactoring commit --- package.json | 1 + src/model/storages/mongostorage.js | 488 +++++++++++++++++++++++++++++ src/model/storages/redisstorage.js | 7 + src/model/usermodel.js | 8 + 4 files changed, 504 insertions(+) diff --git a/package.json b/package.json index 88c329695..fe456cd82 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "lodash": "^4.5.0", "moment": "^2.14.1", "mongodb": "^2.2.4", + "mongoose-validator": "^1.2.5", "ms-amqp-transport": "^3.1.0", "ms-conf": "^2.1.0", "ms-mailer-client": "^3.0.0", diff --git a/src/model/storages/mongostorage.js b/src/model/storages/mongostorage.js index 109f0be13..bb1184db9 100644 --- a/src/model/storages/mongostorage.js +++ b/src/model/storages/mongostorage.js @@ -1,3 +1,491 @@ /** * Created by Stainwoortsel on 01.08.2016. */ +const Promise = require('bluebird'); +const storage = require('./storages/redisstorage'); +const moment = require('moment'); +const mongoose = require('mongoose'); //для самопроверки ) +const { Schema } = mongoose; + + + +const { + ModelError, + ERR_ALIAS_ALREADY_ASSIGNED, ERR_ALIAS_ALREADY_TAKEN, ERR_USERNAME_NOT_EXISTS, ERR_USERNAME_NOT_FOUND, + ERR_TOKEN_FORGED, ERR_CAPTCHA_WRONG_USERNAME, ERR_ATTEMPTS_TO_MUCH_REGISTERED, + ERR_ALIAS_ALREADY_EXISTS, ERR_ACCOUNT_IS_ALREADY_EXISTS, ERR_ACCOUNT_ALREADY_ACTIVATED, +} = require('../modelError'); +/* + ERR_USERNAME_ALREADY_ACTIVE, + ERR_USERNAME_ALREADY_EXISTS, ERR_ACCOUNT_MUST_BE_ACTIVATED, + ERR_ACCOUNT_NOT_ACTIVATED, ERR_ACCOUNT_IS_LOCKED + ERR_ADMIN_IS_UNTOUCHABLE, ERR_CAPTCHA_ERROR_RESPONSE, ERR_EMAIL_DISPOSABLE, + ERR_EMAIL_NO_MX, ERR_EMAIL_ALREADY_SENT, ERR_TOKEN_INVALID, ERR_TOKEN_AUDIENCE_MISMATCH, ERR_TOKEN_MISS_EMAIL, + ERR_TOKEN_EXPIRED, ERR_TOKEN_BAD_EMAIL, ERR_TOKEN_CANT_DECODE, ERR_PASSWORD_INVALID, + ERR_PASSWORD_INVALID_HASH, ERR_PASSWORD_INCORRECT, ERR_PASSWORD_SCRYPT_ERROR, + + */ + +const { + USERS_DATA, USERS_METADATA, USERS_ALIAS_TO_LOGIN, + USERS_ACTIVE_FLAG, USERS_INDEX, USERS_PUBLIC_INDEX, + USERS_ALIAS_FIELD, USERS_TOKENS, USERS_BANNED_FLAG, USERS_BANNED_DATA, +} = require('../../constants'); + +/** + * Adapter pattern class with user model methods + */ +exports.User = { + UserModel: null, + + /** + * Initialize the storage + */ + init() { + exports.User.TokenModel = new Schema({ + 'key': { type: 'String', required: true }, + 'token': { type: 'String', required: true }, + 'expires': { type: 'Date', required: true } + }); + exports.User.UserModel = this.mongo.model('User', new Schema({ + 'username': { type: 'String', required: true, unique: true }, + 'alias': { type: 'String' }, + 'password': { type: 'String', required: true }, + 'isbanned': { type: 'Boolean', default: false }, + 'isactive': { type: 'Boolean', default: false }, + 'ispublic': { type: 'Boolean', default: false }, + 'registered': { type: 'Date' }, + 'meta': { type: 'Mixed' }, + 'tokens': { type: 'Mixed' } // вместо массива проще сделать K->V чтобы было легче работать + })); + // поиск по внутренней структуре докумнета тоже может работать, если использовать не могус, а нативный монго + // почитать про это + // мета: сохранять как { audience: { key: value } } и не надо будет извращаться с поиском + // по метаданным, потому что не будет стрингифай + // + + }, + + + /** + * Get user by username + * @param username + * @returns {Object} + */ + getOne(username) { + //return exports.User.UserModel.findOne({ 'username' : username }); + return exports.User.UserModel + .find({ $or:[ { 'username': username }, { 'alias': username } ]}) + .limit(1) + .exec() + .then((data) => { + if(data.length === 0) throw new ModelError(ERR_USERNAME_NOT_EXISTS); + + return data; + }); + }, + + /** + * TODO: needed + * Get list of users by params + * @param opts + * @returns {Array} + */ + getList(opts) { + // TODO: check list of options + // criteria поле для поиска -- название, и поле для сортировки + // если что, смотреть LUA + + // filter -- это переменная redis filter format, находится в redis-filtered-sort пакете + const { criteria, audience, filter } = opts; + const strFilter = typeof filter === 'string' ? filter : fsort.filter(filter || {}); + const order = opts.order || 'ASC'; + const offset = opts.offset || 0; + const limit = opts.limit || 10; + const index = opts.public ? USERS_PUBLIC_INDEX : USERS_INDEX; + + const metaKey = generateKey('*', USERS_METADATA, audience); + + return exports.User.UserModel.find(opts).exec(); + }, + + /** + * TODO: needed + * Get metadata of user + * @param username + * @param audiences + * @param fields + * @param _public + * @returns {Object} + */ + getMeta(username, audiences, fields = {}, _public = null) { + //TODO: check this method + return mongoose.connection.users.find({ 'username': username, 'meta..': '' }); + }, + + /** + * Get ~real~ username by username or alias + * @param username + * @returns {String} username + */ + getUsername(username) { + return exports.User.UserModel.find({ $or:[ { 'username': username }, { 'alias': username } ]}) + .limit(1) + .exec() + .then((data) => { + return data.username; + }); + }, + + /** + * Check alias existence + * @param alias + * @returns {*} + */ + checkAlias(alias) { + return exports.User.UserModel + .find({ alias }) + .limit(1) + .exec() + .then((user) => { + if(user.alias) { + throw new ModelError(ERR_ALIAS_ALREADY_EXISTS, user.alias); + } + + return user.username; + }); + }, + + /** + * Sets alias to the user by username + * @param opts + * @returns {*} + */ + setAlias(opts) { + const { config } = this; + const { jwt: { defaultAudience } } = config; + const { username, alias, data } = opts; + + // TODO: same logic as in redis, but I feel something wrong... + if (data && data[USERS_ALIAS_FIELD]) { + throw new ModelError(ERR_ALIAS_ALREADY_ASSIGNED); + } + + return exports.User.UserModel + .find({ username, alias: { $exists: true } }) + .limit(1) + .exec() + .then((item) => { + if(item.length > 0) { + throw new ModelError(ERR_ALIAS_ALREADY_TAKEN); + } + + const data = { alias }; + data[defaultAudience] = {}; + data[defaultAudience][USERS_ALIAS_FIELD] = alias; + + return exports.User.UserModel.findOneAndUpdate({ username }, data); + }); + }, + + /** + * Set user password + * @param username + * @param hash + * @returns {String} username + */ + setPassword(username, hash) { + return exports.User.UserModel.findOneAndUpdate({ username }, { password: hash }); + }, + + /** + * Updates metadata of user by username and audience + * @param username + * @param audience + * @param metadata + * @returns {Object} + */ + setMeta(opts) { + // ... + }, + + /** + * Create user account with alias and password + * @param username + * @param alias + * @param hash + * @param activate + * @returns {*} + */ + create(username, alias, hash, activate) { + const { config } = this; + const { deleteInactiveAccounts } = config; + const userDataKey = generateKey(username, USERS_DATA); + const pipeline = redis.pipeline(); + + const data = { + username, + alias, + password: hash, + isactive: activare + }; + +//TODO: refactor it! by using UNIQUE in schema and error catching + return exports.User.UserModel + .find({ username }) + .limit(1) + .exec() + .then((data) => { + if(data.length) { + throw new ModelError(ERR_ACCOUNT_IS_ALREADY_EXISTS, username); + } + + const usr = new exports.User.UserModel(data); + return usr + .save() + .then(() => { + if (!activate && deleteInactiveAccounts >= 0) { +// TODO: expires logic! + } + + return null; + + }) + // setting alias, if we can + .tap(activate && alias ? () => exports.User.setAlias.call(this, { username, alias }) : noop); + }); + }, + + /** + * Remove user + * @param username + * @param data + * @returns {*} + */ + remove(username, data) { + return exports.User.UserModel.findOneAndDelete({ username }); + }, + + /** + * Activate user + * @param username + * @returns {*} + */ + activate(username) { + // TODO: error on already activated + return exports.User.UserModel.findOneAndUpdate({ username }, { isactive: true }); + }, + + /** + * Disactivate user + * @param username + * @returns {*} + */ + disactivate(username) { + // TODO: error on already disactivated + return exports.User.UserModel.findOneAndUpdate({ username }, { isactive: false }); + }, + + /** + * Ban user + * @param username + * @param opts + * @returns {*} + */ + lock(opts) { + const { username, reason, whom, remoteip } = opts; // to guarantee writing only those three variables to metadata from opts + const data = { + isbanned: true, + [USERS_BANNED_DATA]: { + reason: reason || '', + whom: whom || '', + remoteip: remoteip || '', + }, + tokens: [] + }; + + return exports.User.UserModel.findOneAndUpdate({ username }, data); + }, + + /** + * Unlock banned user + * @param { username } + * @returns {*} + */ + unlock({ username }) { + return exports.User.UserModel.findOneAndUpdate({ username }, { isbanned: false }); + }, +}; + +/** + * Adapter pattern class for user login attempts counting + */ +class AttemptsClass { + /** + * Attempts class constructor, _context parameter is the context of service + * @param _context + */ + constructor(_context) { + this.context = _context; + this.loginAttempts = 0; + } + + /** + * Check login attempts + * @param username + * @param ip + * @returns {*} + */ + check(username, ip) { + const { config: { jwt: { lockAfterAttempts }, keepLoginAttempts } } = this.context; + return Promise.bind(this.context, { username, ip }) + .then(storage.Attempts.check) + .then((attempts) => { + if (attempts === null) return; + + this.loginAttempts = attempts; + if (this.loginAttempts > lockAfterAttempts) { + const duration = moment().add(keepLoginAttempts, 'seconds').toNow(true); + const verifyIp = ip && lockAfterAttempts > 0; + + const err = new ModelError(ERR_ATTEMPTS_LOCKED, duration); + if (verifyIp) { + err.loginAttempts = this.loginAttempts; + } + + throw err; + } + }); + } + + /** + * Drop login attempts + * @param username + * @param ip + * @returns {*} + */ + drop(username, ip) { + this.loginAttempts = 0; + return storage.Attempts.drop.call(this.context, username, ip); + } + + /** + * Get attempts count + * @returns {integer} + */ + count() { + return this.loginAttempts; + } +} +exports.Attempts = AttemptsClass; + +/** + * Adapter pattern class for user tokens + */ +exports.Tokens = { + /** + * Add the token + * @param username + * @param token + * @returns {*} + */ + add(username, token) { + return storage.Tokens.add.call(this, username, token); + }, + + /** + * Drop the token + * @param username + * @param token + * @returns {*} + */ + drop(username, token = null) { + return storage.Tokens.drop.call(this, username, token); + }, + + /** + * Get last token score + * @param username + * @param token + * @returns {integer} + */ + lastAccess(username, token) { + return storage.Tokens.lastAccess.call(this, username, token); + }, + + /** + * Get special email throttle state + * @param type + * @param email + * @returns {bool} state + */ + getEmailThrottleState(type, email) { + return storage.Tokens.getEmailThrottleState.call(this, type, email); + }, + + /** + * Set special email throttle state + * @param type + * @param email + * @returns {*} + */ + setEmailThrottleState(type, email) { + return storage.Tokens.setEmailThrottleState.call(this, type, email); + }, + + /** + * Get special email throttle token + * @param type + * @param token + * @returns {string} email + */ + getEmailThrottleToken(type, token) { + return storage.Tokens.getEmailThrottleToken.call(this, type, token); + }, + + /** + * Set special email throttle token + * @param type + * @param email + * @param token + * @returns {*} + */ + setEmailThrottleToken(type, email, token) { + return storage.Tokens.setEmailThrottleToken.call(this, type, email, token); + }, + + /** + * Drop special email throttle token + * @param type + * @param token + * @returns {*} + */ + dropEmailThrottleToken(type, token) { + return storage.Tokens.dropEmailThrottleToken.call(this, type, token); + }, +}; + +/** + * Adapter pattern class for util methods with IP + */ +exports.Utils = { + /** + * Check IP limits for registration + * @param ipaddress + * @returns {*} + */ + checkIPLimits(ipaddress) { + return storage.Utils.checkIPLimits.call(this, ipaddress); + }, + + /** + * Check captcha + * @param username + * @param captcha + * @param next + * @returns {*} + */ + checkCaptcha(username, captcha, next = null) { + return storage.Utils.checkCaptcha.call(this, username, captcha, next); + }, +}; diff --git a/src/model/storages/redisstorage.js b/src/model/storages/redisstorage.js index ed72d1eb8..53584a04d 100644 --- a/src/model/storages/redisstorage.js +++ b/src/model/storages/redisstorage.js @@ -51,6 +51,13 @@ const generateKey = (...args) => { exports.User = { + /** + * Initialize the storage + */ + init() { + // ... + }, + /** * Get user by username * @param username diff --git a/src/model/usermodel.js b/src/model/usermodel.js index 3ae0f257d..7726efe8a 100644 --- a/src/model/usermodel.js +++ b/src/model/usermodel.js @@ -10,6 +10,14 @@ const moment = require('moment'); * Adapter pattern class with user model methods */ exports.User = { + + /** + * Initialize the model + */ + init() { + storage.User.init.call(this); + }, + /** * Get user by username * @param username