Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: abstract DB Adapter #112

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
757b931
feat(Adapter Class): The realisation of adapter class and Redis data …
stainwoortsel Jun 3, 2016
76cc264
feat(actions, adapter, redisstorage): Updating few actions with new s…
stainwoortsel Jun 3, 2016
fd2a23f
feat(adapter, redisstorage, actions): Making abstraction layer betwee…
stainwoortsel Jun 3, 2016
c4ebf71
style(gitignore):
stainwoortsel Jun 3, 2016
4b7a5c6
style(delete compiled and map files):
stainwoortsel Jun 3, 2016
a334384
feat: actions: login, alias, list
stainwoortsel Jun 4, 2016
98cf306
feat: register action
stainwoortsel Jun 5, 2016
17ba3bf
feat: add new utils: verifyGoogleCaptcha and mapMetaResponse
stainwoortsel Jun 5, 2016
e04d2c7
feat: remove action
stainwoortsel Jun 5, 2016
e113733
feat: actions: requestPassword, updateMetadata
stainwoortsel Jun 5, 2016
0b51598
feat: actions: updatePassword, verify
stainwoortsel Jun 6, 2016
55c47af
style: actions, adapter, redisstorage, utils, docker.sh
stainwoortsel Jun 8, 2016
b18ac68
feat: new UserModer class abstraction, redisstorage, new modelError m…
stainwoortsel Jun 18, 2016
821deb6
feat: renewed modelError methods, usermodal, redisstodage; utils: jwt…
stainwoortsel Jun 20, 2016
8a5de75
feat: usermodel & redisstorage: throttle state and token methods, uti…
stainwoortsel Jun 21, 2016
3829f5e
style: some linter fixes: util/scrypt, util/send-emal, model/redissto…
stainwoortsel Jun 21, 2016
60746e9
fix: fixing docker.sh
stainwoortsel Jun 23, 2016
6ba80a6
Merge branch 'master' into adapter
AVVS Jun 26, 2016
0dfc8c9
fix: fixing 'this'-context trouble, fixing loginAttempts variable
stainwoortsel Jun 26, 2016
e4c8b44
fix: fixing bugs through the tests
stainwoortsel Jul 1, 2016
ac18064
fix: linting!
stainwoortsel Jul 1, 2016
71aa982
fix: fixing bugs through tests
stainwoortsel Jul 1, 2016
2ee6993
fix: linting!
stainwoortsel Jul 1, 2016
275a7bb
fix: fixing register bug with catchReturn wrong error code
stainwoortsel Jul 1, 2016
7d5a1f2
fix: fixing bugs through the tests
stainwoortsel Jul 2, 2016
8b7263d
fix: fixing bugs through the tests
stainwoortsel Jul 3, 2016
0860363
fix: fixing bugs through the tests: usermodel, login, updateMeta, tok…
stainwoortsel Jul 4, 2016
12c5df1
fix: jwt logout decodedTokens, arrow-function better
stainwoortsel Jul 4, 2016
7d7bfb4
fix: fixing bugs through the tests: jwt/send-mail
stainwoortsel Jul 5, 2016
785ccfd
fix: fixing bugs through the test (hope, in last time): jwt, send-mail
stainwoortsel Jul 5, 2016
8ea411f
fix: some cosmetic fixes
stainwoortsel Jul 7, 2016
d6cff19
fix: some another cosmetic fixes
stainwoortsel Jul 7, 2016
7cc68c8
refactor: merging with master
stainwoortsel Jul 8, 2016
1cabd9c
fix: UNIX-style end of strings in scripts (to avoid bash errors)
stainwoortsel Jul 8, 2016
71c46a5
fix: some lint sugar
stainwoortsel Jul 8, 2016
db222e4
fix: and little fix for math operation with page of list in redisstor…
stainwoortsel Jul 8, 2016
aaad1a4
fix: please linter, please...
stainwoortsel Jul 8, 2016
98b5770
feat: mongo docker-compose
stainwoortsel Jul 18, 2016
20fa21d
feat: making test suites a bit more independed from storages
stainwoortsel Aug 1, 2016
24e3674
feat: methods in mongo adapter, before HUGE refactoring commit
stainwoortsel Aug 10, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"plugins": [
"transform-strict-mode",
"transform-es2015-spread",
"transform-object-rest-spread",
"transform-class-properties"
]
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ node_modules
# compiled source code
lib
.env

src/db/sandbox/
.idea
*-compiled.js
*-compiled.js.map
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"name": "ms-users",
"version": "0.0.1",
"description": "Core of the microservice for handling users",
"main": "./lib/index.js",
"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",
"doc": "apidoc -i ./src/actions -v -o ./docs",
Expand Down Expand Up @@ -33,12 +34,14 @@
"jsonwebtoken": "^7.0.1",
"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",
"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",
Expand All @@ -49,9 +52,8 @@
"devDependencies": {
"apidoc": "^0.16.1",
"babel-cli": "^6.10.1",
"babel-eslint": "^6.0.5",
"babel-eslint": "^6.1.0",
"babel-plugin-transform-class-properties": "^6.10.2",
"babel-plugin-transform-es2015-spread": "^6.0.0",
"babel-plugin-transform-object-rest-spread": "^6.0.0",
"babel-plugin-transform-strict-mode": "^6.0.0",
"babel-register": "^6.5.2",
Expand All @@ -64,7 +66,7 @@
"eslint-plugin-mocha": "^4.0.0",
"faker": "^3.0.1",
"isparta": "^4.0.0",
"istanbul": "^0.4.2",
"istanbul": "^0.4.4",
"json": "^9.0.4",
"latest-version-cli": "^1.0.0",
"mocha": "^2.4.5",
Expand Down
1 change: 1 addition & 0 deletions rm.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test/docker.sh customizing testable files
52 changes: 9 additions & 43 deletions src/actions/activate.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,23 @@
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 { User } = require('../model/usermodel');
const { MAIL_ACTIVATE } = require('../constants');

/**
* @api {amqp} <prefix>.activate Activate User
* @apiVersion 1.0.0
* @apiName ActivateUser
* @apiGroup Users
*
* @apiDescription This method allows one to activate user by 2 means: providing a username or encoded verification token.
* When only username is provided, no verifications will be performed and user will be set to active. In contrary, `token`
* would be verified. This allows for 2 scenarios: admin activating a user, or user completing verification challenge. In
* case of success output would contain user object
*
* @apiParam (Payload) {String} username - currently email of the user
* @apiParam (Payload) {String} token - if present, would be used against test challenge
* @apiParam (Payload) {String} [remoteip] - not used, but is reserved for security log in the future
* @apiParam (Payload) {String} [audience] - additional metadata will be pushed there from custom hooks
*
* Activate existing users
* @param opts
* @return {Promise}
*/
module.exports = function verifyChallenge(opts) {
// TODO: add security logs
// var remoteip = opts.remoteip;
const { token, username } = opts;
const { redis, config } = this;
const { config } = this;
const audience = opts.audience || config.defaultAudience;

function verifyToken() {
return emailVerification.verify.call(this, token, 'activate', 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`);
}
});
return emailVerification.verify.call(this, token, MAIL_ACTIVATE, config.validation.ttl > 0);
}

function hook(user) {
Expand All @@ -60,8 +26,8 @@ module.exports = function verifyChallenge(opts) {

return Promise
.bind(this, username)
.then(username ? userExists : verifyToken)
.tap(activateAccount)
.then(username ? User.getUsername : verifyToken)
.tap(User.activate)
.tap(hook)
.then(user => [user, audience])
.spread(jwt.login);
Expand Down
48 changes: 9 additions & 39 deletions src/actions/alias.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,21 @@
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 isActive = require('../utils/isActive');
const isBanned = require('../utils/isBanned');
const { User } = require('../model/usermodel');

/**
* @api {amqp} <prefix>.alias Add alias to user
* @apiVersion 1.0.0
* @apiName AddAlias
* @apiGroup Users
*
* @apiDescription Adds alias to existing username. This alias must be unique across system, as
* well as obide strict restrictions - ascii chars only, include numbers and dot. It's used to obfuscate
* username in public interfaces
*
* @apiParam (Payload) {String} username - currently email of the user
* @apiParam (Payload) {String{3..15}} alias - chosen alias
*
* Assign alias to user
* @param opts
* @return {Promise}
*/
module.exports = function assignAlias(opts) {
const { redis, config: { jwt: { defaultAudience } } } = this;
const { username, alias } = opts;

return Promise
.bind(this, username)
.then(getInternalData)
.then(User.getOne)
.tap(isActive)
.tap(isBanned)
.then(data => {
if (data[USERS_ALIAS_FIELD]) {
throw new Errors.HttpStatusError(417, 'alias is already assigned');
}

return redis.hsetnx(USERS_ALIAS_TO_LOGIN, alias, username);
})
.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();
});
.then(data => ({ username, alias, data }))
.then(User.setAlias);
};
47 changes: 3 additions & 44 deletions src/actions/ban.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,5 @@
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');

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();
}

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();
}
const { User } = require('../model/usermodel');

/**
* @api {amqp} <prefix>.ban Lock or Unlock user
Expand All @@ -61,7 +20,7 @@ function unlockUser({ username }) {
module.exports = function banUser(opts) {
return Promise
.bind(this, opts.username)
.then(userExists)
.then(User.getUsername)
.then(username => ({ ...opts, username }))
.then(opts.ban ? lockUser : unlockUser);
.then(opts.ban ? User.lock : User.unlock);
};
12 changes: 6 additions & 6 deletions src/actions/challenge.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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 isActive = require('../utils/isActive');
const { User } = require('../model/usermodel');
const { ModelError, ERR_ACCOUNT_NOT_ACTIVATED, ERR_USERNAME_ALREADY_ACTIVE } = require('../model/modelError');

/**
* @api {amqp} <prefix>.challenge Creates user challenges
Expand All @@ -28,9 +28,9 @@ module.exports = function sendChallenge(message) {

return Promise
.bind(this, username)
.then(getInternalData)
.then(User.getOne)
.tap(isActive)
.throw(new Errors.HttpStatusError(417, `${username} is already active`))
.catchReturn({ statusCode: 412 }, username)
.throw(new ModelError(ERR_USERNAME_ALREADY_ACTIVE, username))
.catchReturn({ code: ERR_ACCOUNT_NOT_ACTIVATED }, username)
.then(emailChallenge.send);
};
4 changes: 2 additions & 2 deletions src/actions/getInternalData.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const Promise = require('bluebird');
const getInternalData = require('../utils/getInternalData.js');
const pick = require('lodash/pick');
const { User } = require('../model/usermodel');

/**
* @api {amqp} <prefix>.getInternalData Retrieve Internal Data
Expand All @@ -21,7 +21,7 @@ module.exports = function internalData(message) {

return Promise
.bind(this, message.username)
.then(getInternalData)
.then(User.getOne)
.then(data => {
return fields ? pick(data, fields) : data;
});
Expand Down
46 changes: 5 additions & 41 deletions src/actions/getMetadata.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,18 @@
const Promise = require('bluebird');
const Errors = require('common-errors');
const getMetadata = require('../utils/getMetadata.js');
const userExists = require('../utils/userExists.js');
const isPublic = require('../utils/isPublic');
const noop = require('lodash/noop');
const get = require('lodash/get');
const isArray = Array.isArray;
const { USERS_ALIAS_FIELD } = require('../constants.js');

function isPublic(username, audiences) {
return metadata => {
let notFound = true;
const { User } = require('../model/usermodel');

// iterate over passed audiences
audiences.forEach(audience => {
if (notFound && get(metadata, [audience, USERS_ALIAS_FIELD]) === username) {
notFound = false;
}
});

if (notFound) {
throw new Errors.HttpStatusError(404, 'username was not found');
}
};
}
const isArray = Array.isArray;

/**
* @api {amqp} <prefix>.getMetadata Retrieve Public Data
* @apiVersion 1.0.0
* @apiName getMetadata
* @apiGroup Users
*
* @apiDescription This should be used to retrieve user's publicly available data. It contains 2 modes:
* data that is available when the user requests data about him or herself and when someone else tries
* to get data about a given user on the system. For instance, if you want to view someone's public profile
*
* @apiParam (Payload) {String} username - user's username, can be `alias` or real `username`.
* If it's a real username - then all the data is returned.
* @apiParam (Payload) {String[]} audience - which namespace of metadata should be used, can be string or array of strings
* @apiParam (Payload) {Object} fields - must contain an object of `[audience]: String[]` mapping
* @apiParam (Payload) {String[]} fields.* - fields to return from a passed audience
*
*/
module.exports = function getMetadataAction(message) {
const { audience: _audience, username, fields } = message;
const audience = isArray(_audience) ? _audience : [_audience];

return Promise
.bind(this, username)
.then(userExists)
.then(User.getUsername)
.then(realUsername => [realUsername, audience, fields])
.spread(getMetadata)
.spread(User.getMeta)
.tap(message.public ? isPublic(username, audience) : noop);
};
Loading