diff --git a/README.md b/README.md index 0fc0b3ef5..97389b2e6 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,11 @@ that contains description of routes and their capabilities. Aims to provide a co TODO +### Temporary activation + +`config.temporaryActivation.enabled` - Enable/disable temporary activation, default `false`. +`config.temporaryActivation.validTimeMs` - Temporary activation time, default 10 days. + ## Endpoint description Currently available on github pages diff --git a/src/actions/activate.js b/src/actions/activate.js index 26faa80a0..786b87f1a 100644 --- a/src/actions/activate.js +++ b/src/actions/activate.js @@ -19,6 +19,7 @@ const { USERS_USERNAME_FIELD, USERS_ACTION_ACTIVATE, USERS_ACTIVATED_FIELD, + USERS_TEMP_ACTIVATED_TIME_FIELD, } = require('../constants.js'); // cache error @@ -140,6 +141,8 @@ async function activateAccount(data, metadata) { .pipeline() .hget(userKey, USERS_ACTIVE_FLAG) .hset(userKey, USERS_ACTIVE_FLAG, 'true') + // unsets USERS_TEMP_ACTIVATED_TIME_FIELD used for temporary activation + .hdel(userKey, USERS_TEMP_ACTIVATED_TIME_FIELD) .persist(userKey) .sadd(USERS_INDEX, userId); diff --git a/src/actions/alias.js b/src/actions/alias.js index e8defd012..4f87510ce 100644 --- a/src/actions/alias.js +++ b/src/actions/alias.js @@ -2,9 +2,8 @@ const Promise = require('bluebird'); const Errors = require('common-errors'); const { ActionTransport } = require('@microfleet/core'); const { getInternalData } = require('../utils/userData'); -const isActive = require('../utils/is-active'); +const { isActive, makeNotActiveError } = require('../utils/is-active'); const isBanned = require('../utils/is-banned'); -const DetailedHttpStatusError = require('../utils/detailed-error'); const key = require('../utils/key'); const handlePipeline = require('../utils/pipeline-error'); const { @@ -33,7 +32,8 @@ const { * */ async function assignAlias({ params }) { - const { redis, config: { jwt: { defaultAudience } } } = this; + const { redis, config } = this; + const { jwt: { defaultAudience } } = config; const { username, internal } = params; // lowercase alias @@ -50,10 +50,10 @@ async function assignAlias({ params }) { // determine if user is active const userId = data[USERS_ID_FIELD]; - const activeUser = isActive(data, true); + const activeUser = isActive(config, data); if (!activeUser && !internal) { - return Promise.reject(DetailedHttpStatusError(412, 'Account hasn\'t been activated', { username: data[USERS_USERNAME_FIELD] })); + throw makeNotActiveError(data[USERS_USERNAME_FIELD]); } let lock; diff --git a/src/actions/challenge.js b/src/actions/challenge.js index 8022e14cc..2940faa00 100644 --- a/src/actions/challenge.js +++ b/src/actions/challenge.js @@ -1,7 +1,7 @@ const { ActionTransport } = require('@microfleet/core'); const { getInternalData } = require('../utils/userData'); const getMetadata = require('../utils/get-metadata'); -const isActive = require('../utils/is-active'); +const { isActive } = require('../utils/is-active'); const challenge = require('../utils/challenges/challenge'); const { USERS_ACTION_ACTIVATE, @@ -38,7 +38,7 @@ module.exports = async function sendChallenge({ params }) { const internalData = await getInternalData.call(service, username); - if (isActive(internalData, true)) throw USER_ALREADY_ACTIVE; + if (isActive(config, internalData)) throw USER_ALREADY_ACTIVE; const userId = internalData[USERS_ID_FIELD]; const resolvedUsername = internalData[USERS_USERNAME_FIELD]; diff --git a/src/actions/disposable-password.js b/src/actions/disposable-password.js index 156da0e2c..545c319b1 100644 --- a/src/actions/disposable-password.js +++ b/src/actions/disposable-password.js @@ -2,7 +2,7 @@ const Promise = require('bluebird'); const { ActionTransport } = require('@microfleet/core'); const challenge = require('../utils/challenges/challenge'); const { getInternalData } = require('../utils/userData'); -const isActive = require('../utils/is-active'); +const { isActiveTap } = require('../utils/is-active'); const isBanned = require('../utils/is-banned'); const hasNotPassword = require('../utils/has-no-password'); const { USERS_ACTION_DISPOSABLE_PASSWORD, USERS_USERNAME_FIELD } = require('../constants'); @@ -24,7 +24,7 @@ module.exports = function disposablePassword(request) { return Promise .bind(this, id) .then(getInternalData) - .tap(isActive) + .tap(isActiveTap) .tap(isBanned) .tap(hasNotPassword) .then((data) => ([challengeType, { diff --git a/src/actions/login.js b/src/actions/login.js index d8b640e31..e3815c553 100644 --- a/src/actions/login.js +++ b/src/actions/login.js @@ -5,7 +5,7 @@ const moment = require('moment'); const is = require('is'); const scrypt = require('../utils/scrypt'); const jwt = require('../utils/jwt'); -const isActive = require('../utils/is-active'); +const { assertIsActive } = require('../utils/is-active'); const isBanned = require('../utils/is-banned'); const { checkMFA } = require('../utils/mfa'); @@ -211,7 +211,7 @@ async function login({ params, locals }) { await cleanupRateLimits(ctx, internalData); // verifies that the user is active, rejects by default - await isActive(internalData); + assertIsActive(config, internalData); // verifies that user is not banned, sync action - throws isBanned(internalData); diff --git a/src/actions/register.js b/src/actions/register.js index 090e6b610..e25cd2a2e 100644 --- a/src/actions/register.js +++ b/src/actions/register.js @@ -17,9 +17,8 @@ const mxExists = require('../utils/mx-exists'); const checkCaptcha = require('../utils/check-captcha'); const { getUserId } = require('../utils/userData'); const aliasExists = require('../utils/alias-exists'); -const assignAlias = require('./alias'); const checkLimits = require('../utils/check-ip-limits'); -const challenge = require('../utils/challenges/challenge'); +const generateChallenge = require('../utils/challenges/challenge'); const handlePipeline = require('../utils/pipeline-error'); const hashPassword = require('../utils/register/password/hash'); const { @@ -37,6 +36,7 @@ const { USERS_REFERRAL_FIELD, USERS_REFERRAL_META_FIELD, USERS_ACTIVATED_FIELD, + USERS_TEMP_ACTIVATED_TIME_FIELD, lockAlias, lockRegister, USERS_ACTION_INVITE, @@ -152,11 +152,9 @@ async function performRegistration({ service, params }) { metadata, challengeType, } = params; - - const { - config, - redis, - } = service; + const { config, redis } = service; + const { deleteInactiveAccounts, temporaryActivation, token: tokenConfig } = config; + const { enabled: temporaryActivationEnabled } = temporaryActivation; // do verifications of DB state await Promise.bind(service, username) @@ -190,6 +188,14 @@ async function performRegistration({ service, params }) { [USERS_USERNAME_FIELD]: username, [USERS_ACTIVE_FLAG]: activate, }; + const challengeParams = [ + { + id: username, + action: USERS_ACTION_ACTIVATE, + ...tokenConfig[challengeType], + }, + { ...metadata[creatorAudience] }, + ]; if (params.skipPassword === false) { // this will be passed as context if we need to send an email @@ -203,20 +209,24 @@ async function performRegistration({ service, params }) { if (sso) { const { provider, uid, credentials } = sso; - // inject sensitive provider info to internal data basicInfo[provider] = JSON.stringify(credentials.internals); - // link uid to username pipeline.hset(USERS_SSO_TO_ID, uid, userId); } + // this field will be unset when activate user + if (temporaryActivationEnabled === true && activate === false) { + basicInfo[USERS_TEMP_ACTIVATED_TIME_FIELD] = Date.now(); + } + const userDataKey = redisKey(userId, USERS_DATA); pipeline.hmset(userDataKey, basicInfo); pipeline.hset(USERS_USERNAME_TO_ID, username, userId); - if (activate === false && config.deleteInactiveAccounts >= 0) { - pipeline.expire(userDataKey, config.deleteInactiveAccounts); + // do not expire if temporaryActivationEnabled === true because user will be added to USERS_INDEX + if (activate === false && temporaryActivationEnabled === false && deleteInactiveAccounts >= 0) { + pipeline.expire(userDataKey, deleteInactiveAccounts); } handlePipeline(await pipeline.exec()); @@ -241,16 +251,10 @@ async function performRegistration({ service, params }) { // assign alias if (alias) { - await assignAlias.call(service, { - params: { - username, - alias, - internal: true, - }, - }); + await service.dispatch('alias', { params: { username, alias, internal: true } }); } - if (activate === true) { + if (activate === true || temporaryActivationEnabled === true) { // perform instant activation // internal username index const regPipeline = redis.pipeline().sadd(USERS_INDEX, userId); @@ -262,42 +266,29 @@ async function performRegistration({ service, params }) { regPipeline.sadd(`${USERS_REFERRAL_INDEX}:${ref}`, userId); } - return regPipeline - .exec() - .then(handlePipeline) - // custom actions - .bind(service) - .return(['users:activate', userId, params, metadata]) - .spread(service.hook) - // login & return JWT - .return([userId, creatorAudience]) - .spread(jwt.login); + await regPipeline.exec().then(handlePipeline); + await service.hook.call(service, 'users:activate', userId, params, metadata); + + if (temporaryActivationEnabled === true && challengeType === CHALLENGE_TYPE_EMAIL) { + await generateChallenge.call(service, challengeType, ...challengeParams); + } + + return jwt.login.call(service, userId, creatorAudience); } - const challengeOpts = { - id: username, - action: USERS_ACTION_ACTIVATE, - ...config.token[challengeType], - }; + const response = { id: userId, requiresActivation: true }; - const metaCopy = { - ...metadata[creatorAudience], - }; + // don't create challenge + if (params.skipChallenge === true) { + return response; + } - const challengeResponse = params.skipChallenge - ? null - : await challenge.call(service, challengeType, challengeOpts, metaCopy); + const challenge = await generateChallenge.call(service, challengeType, ...challengeParams); - return challengeResponse - ? { - id: userId, - requiresActivation: true, - uid: challengeResponse.context.token.uid, - } - : { - id: userId, - requiresActivation: true, - }; + return { + ...response, + uid: challenge.context.token.uid, + }; } /** diff --git a/src/actions/requestPassword.js b/src/actions/requestPassword.js index 9f10b89e1..85b080f46 100644 --- a/src/actions/requestPassword.js +++ b/src/actions/requestPassword.js @@ -1,6 +1,6 @@ const Promise = require('bluebird'); const { getInternalData } = require('../utils/userData'); -const isActive = require('../utils/is-active'); +const { isActiveTap } = require('../utils/is-active'); const isBanned = require('../utils/is-banned'); const hasPassword = require('../utils/has-password'); const getMetadata = require('../utils/get-metadata'); @@ -38,7 +38,7 @@ module.exports = function requestPassword(request) { return Promise .bind(this, usernameOrAlias) .then(getInternalData) - .tap(isActive) + .tap(isActiveTap) .tap(isBanned) .tap(hasPassword) .then((data) => [data[USERS_ID_FIELD], defaultAudience]) diff --git a/src/actions/updatePassword.js b/src/actions/updatePassword.js index 3a97fb5c2..144339f09 100644 --- a/src/actions/updatePassword.js +++ b/src/actions/updatePassword.js @@ -4,7 +4,7 @@ const scrypt = require('../utils/scrypt'); const redisKey = require('../utils/key'); const jwt = require('../utils/jwt'); const { getInternalData } = require('../utils/userData'); -const isActive = require('../utils/is-active'); +const { assertIsActive } = require('../utils/is-active'); const isBanned = require('../utils/is-banned'); const hasPassword = require('../utils/has-password'); const { getUserId } = require('../utils/userData'); @@ -28,7 +28,7 @@ const Forbidden = new HttpStatusError(403, 'invalid token'); async function usernamePasswordReset(service, username, password) { const internalData = await getInternalData.call(service, username); - await isActive(internalData); + assertIsActive(service.config, internalData); await isBanned(internalData); await hasPassword(internalData); diff --git a/src/actions/verify.js b/src/actions/verify.js index 250e0262a..aa7c7e187 100644 --- a/src/actions/verify.js +++ b/src/actions/verify.js @@ -4,7 +4,8 @@ const { ActionTransport } = require('@microfleet/core'); const jwt = require('../utils/jwt'); const getMetadata = require('../utils/get-metadata'); const { getInternalData } = require('../utils/userData'); -const { USERS_MFA_FLAG } = require('../constants'); +const { USERS_MFA_FLAG, USERS_ID_FIELD } = require('../constants'); +const { assertIsActive } = require('../utils/is-active'); /** * Internal functions @@ -21,31 +22,24 @@ async function decodedToken({ username, userId }) { } const { audience, defaultAudience, service } = this; + const internalData = await getInternalData.call(service, userId || username); + const resolvedUserId = userId || internalData[USERS_ID_FIELD]; + + // needs for checking temporary activation + assertIsActive(service.config, internalData); // push extra audiences if (audience.indexOf(defaultAudience) === -1) { audience.push(defaultAudience); } - let resolveduserId = userId; - let hasMFA; - if (resolveduserId == null) { - const internalData = await getInternalData.call(service, username); - resolveduserId = internalData.id; - hasMFA = !!internalData[USERS_MFA_FLAG]; - } + const metadata = await getMetadata.call(service, resolvedUserId, audience); - const metadata = await getMetadata.call(service, resolveduserId, audience); - const response = { - id: resolveduserId, + return { metadata, + id: resolvedUserId, + mfa: !!internalData[USERS_MFA_FLAG], }; - - if (hasMFA !== undefined) { - response.mfa = hasMFA; - } - - return response; } /** diff --git a/src/configs/core.js b/src/configs/core.js index c0d9fc7aa..5962d1afa 100644 --- a/src/configs/core.js +++ b/src/configs/core.js @@ -122,3 +122,8 @@ exports.mfa = { window: 10, }, }; + +exports.temporaryActivation = { + enabled: false, + validTimeMs: 10 * 24 * 60 * 60 * 1000, +}; diff --git a/src/constants.js b/src/constants.js index d496be820..93a5234aa 100644 --- a/src/constants.js +++ b/src/constants.js @@ -43,6 +43,7 @@ module.exports = exports = { USERS_BANNED_DATA: 'bannedData', USERS_CREATED_FIELD: 'created', USERS_ACTIVATED_FIELD: 'aa', + USERS_TEMP_ACTIVATED_TIME_FIELD: 'tempActivatedTime', USERS_USERNAME_FIELD: 'username', USERS_IS_ORG_FIELD: 'org', USERS_PASSWORD_FIELD: 'password', diff --git a/src/utils/is-active.js b/src/utils/is-active.js index 70bc422fb..dff0a2a74 100644 --- a/src/utils/is-active.js +++ b/src/utils/is-active.js @@ -1,11 +1,69 @@ const Promise = require('bluebird'); + const DetailedHttpStatusError = require('./detailed-error'); -const { USERS_ACTIVE_FLAG, USERS_USERNAME_FIELD } = require('../constants.js'); +const { + USERS_ACTIVE_FLAG, + USERS_USERNAME_FIELD, + USERS_TEMP_ACTIVATED_TIME_FIELD, +} = require('../constants.js'); + +function makeNotActiveError(username) { + return DetailedHttpStatusError(412, 'Account hasn\'t been activated', { username }); +} + +/** + * Used if you need a boolean + * @param {Object} config - ms-users config + * @param {Object} userData - user internal data + * @returns {boolean} + */ +function isActive(config, userData) { + const { temporaryActivation } = config; + const temporaryActivatedTime = userData[USERS_TEMP_ACTIVATED_TIME_FIELD]; -module.exports = function isActive(data, sync) { - if (String(data[USERS_ACTIVE_FLAG]) !== 'true') { - return sync ? false : Promise.reject(DetailedHttpStatusError(412, 'Account hasn\'t been activated', { username: data[USERS_USERNAME_FIELD] })); + if (temporaryActivatedTime !== undefined) { + if ((parseInt(temporaryActivatedTime, 10) + temporaryActivation.validTimeMs) >= Date.now()) { + return true; + } + + return false; } - return sync ? true : Promise.resolve(data); + return String(userData[USERS_ACTIVE_FLAG]) === 'true'; +} + +/** + * Used if you need to throw an error + * @param {Object} config - ms-users config + * @param {Object} userData - user internal data + * @returns {Promise} - user internal data + */ +function assertIsActive(config, userData) { + if (isActive(config, userData) === false) { + throw makeNotActiveError(userData[USERS_USERNAME_FIELD]); + } +} + +/** + * Helper for using with bluebird promise chain + * @param {Object} userData - user internal data + * @this {Microfleet} - instance of Microfleet + * @throws {DetailedHttpStatusError} + * @returns {Object} - user internal data + */ +function isActiveTap(userData) { + const { config } = this; + + if (isActive(config, userData) === false) { + return Promise.reject(makeNotActiveError(userData[USERS_USERNAME_FIELD])); + } + + return Promise.resolve(userData); +} + +module.exports = { + isActive, + assertIsActive, + isActiveTap, + makeNotActiveError, }; diff --git a/test/suites/actions/register.temp-activation.js b/test/suites/actions/register.temp-activation.js new file mode 100644 index 000000000..418d4b143 --- /dev/null +++ b/test/suites/actions/register.temp-activation.js @@ -0,0 +1,241 @@ +const { delay } = require('bluebird'); +const { rejects, strict: assert } = require('assert'); + +const Users = require('../../../src'); + +describe('register: temporary activated users', function suite() { + const service = new Users({ + temporaryActivation: { + enabled: true, + validTimeMs: 3 * 1000, + }, + token: { + email: { + throttle: 1, // for creating token for activation without error in test + }, + }, + }); + + before(() => service.connect()); + after(async () => { + if (service.redisType === 'redisCluster') { + await Promise.all( + service.redis.nodes('master').map((node) => node.flushdb()) + ); + } else { + await service.redis.flushdb(); + } + }); + after(() => service.close()); + + it('should be able to login after register and send activation email', async () => { + const { amqp, redis } = service; + + const data = await amqp.publishAndWait('users.register', { + username: 'perchik@gmail.com', + password: 'perchikisnotfatcat', + alias: 'perchik2000', + activate: false, + audience: 'tikkothouse', + }); + const redisData = await redis.hgetall(`${data.user.id}!data`); + const redisMeta = await redis.hgetall(`${data.user.id}!metadata!*.localhost`); + + assert(data.jwt.length !== undefined); + assert(data.user.id !== undefined); + assert(data.user.metadata.tikkothouse !== undefined); + assert(data.user.metadata['*.localhost'].id !== undefined); + assert(data.user.metadata['*.localhost'].username === 'perchik@gmail.com'); + assert(data.user.metadata['*.localhost'].created !== undefined); + assert(data.user.metadata['*.localhost'].alias === 'perchik2000'); + assert(data.user.metadata['*.localhost'].aa === undefined); + + assert(redisData.tempActivatedTime !== undefined); + assert(redisData.username === 'perchik@gmail.com'); + assert(redisData.active === 'false'); + assert(redisData.password !== undefined); + assert(redisData.alias === 'perchik2000'); + assert(redisData.created !== undefined); + + assert(redisMeta.id !== undefined); + assert(redisMeta.username === '"perchik@gmail.com"'); + assert(redisMeta.created !== undefined); + assert(redisMeta.alias === '"perchik2000"'); + assert(redisMeta.aa === undefined); + + assert(await redis.ttl(`${data.user.id}!data`) === -1); + + assert(await redis.hget('users-alias', 'perchik2000') === data.user.id); + + assert(await redis.sismember('user-iterator-set', data.user.id) === 1); + + assert(await redis.exists('tmanager!1.0.0activate!perchik@gmail.com') === 1); + + // @todo assert that email has been sent + }); + + // depends on previous test + it('should be able to login and verify', async () => { + const { amqp } = service; + + const data0 = await amqp.publishAndWait('users.login', { + username: 'perchik@gmail.com', + password: 'perchikisnotfatcat', + audience: '*.localhost', + }); + + assert(data0.jwt.length !== undefined); + assert(data0.user.id !== undefined); + assert(data0.user.metadata['*.localhost'].id !== undefined); + assert(data0.user.metadata['*.localhost'].username === 'perchik@gmail.com'); + assert(data0.user.metadata['*.localhost'].created !== undefined); + assert(data0.user.metadata['*.localhost'].alias === 'perchik2000'); + assert(data0.user.metadata['*.localhost'].aa === undefined); + assert(data0.mfa === false); + + const data1 = await amqp.publishAndWait('users.verify', { + token: data0.jwt, + audience: '*.localhost', + }); + + assert(data1.id !== undefined); + assert(data1.metadata['*.localhost'].id !== undefined); + assert(data1.metadata['*.localhost'].username === 'perchik@gmail.com'); + assert(data1.metadata['*.localhost'].created !== undefined); + assert(data1.metadata['*.localhost'].alias === 'perchik2000'); + assert(data1.metadata['*.localhost'].aa === undefined); + assert(data1.mfa === false); + + this.jwt = data0.jwt; + }); + + // depends on previous test + it('should be able to login or verify if temporary activation time is over', async () => { + const { amqp } = service; + + await delay(3 * 1000); // time from config + + await rejects( + amqp.publishAndWait('users.login', { + username: 'perchik@gmail.com', + password: 'perchikisnotfatcat', + audience: '*.localhost', + }), + /Account hasn't been activated/ + ); + + await rejects( + amqp.publishAndWait('users.verify', { + token: this.jwt, + audience: '*.localhost', + }), + /Account hasn't been activated/ + ); + }); + + // depends on previous test + it('should be able to activate temporary activated account', async () => { + const { amqp, redis } = service; + // @todo get token from email + const { secret } = await service.tokenManager.create({ + id: 'perchik@gmail.com', + action: 'activate', + }); + + const data = await amqp.publishAndWait('users.activate', { token: secret }); + const redisData = await redis.hgetall(`${data.user.id}!data`); + const redisMeta = await redis.hgetall(`${data.user.id}!metadata!*.localhost`); + + assert(data.jwt.length !== undefined); + assert(data.user.id !== undefined); + assert(data.user.metadata['*.localhost'].id !== undefined); + assert(data.user.metadata['*.localhost'].username === 'perchik@gmail.com'); + assert(data.user.metadata['*.localhost'].created !== undefined); + assert(data.user.metadata['*.localhost'].alias === 'perchik2000'); + assert(data.user.metadata['*.localhost'].aa !== undefined); + + assert(redisData.tempActivatedTime === undefined); + assert(redisData.username === 'perchik@gmail.com'); + assert(redisData.active === 'true'); + assert(redisData.password !== undefined); + assert(redisData.alias === 'perchik2000'); + assert(redisData.created !== undefined); + + assert(redisMeta.id !== undefined); + assert(redisMeta.username === '"perchik@gmail.com"'); + assert(redisMeta.created !== undefined); + assert(redisMeta.alias === '"perchik2000"'); + assert(redisMeta.aa !== undefined); + }); + + // depends on previous test + it('should be able to login and verify (includes old token)', async () => { + const { amqp } = service; + + const data0 = await amqp.publishAndWait('users.login', { + username: 'perchik@gmail.com', + password: 'perchikisnotfatcat', + audience: '*.localhost', + }); + + assert(data0.jwt.length !== undefined); + assert(data0.user.id !== undefined); + assert(data0.user.metadata['*.localhost'].id !== undefined); + assert(data0.user.metadata['*.localhost'].username === 'perchik@gmail.com'); + assert(data0.user.metadata['*.localhost'].created !== undefined); + assert(data0.user.metadata['*.localhost'].alias === 'perchik2000'); + assert(data0.user.metadata['*.localhost'].aa !== undefined); + assert(data0.mfa === false); + + const data1 = await amqp.publishAndWait('users.verify', { + token: data0.jwt, + audience: '*.localhost', + }); + + assert(data1.id !== undefined); + assert(data1.metadata['*.localhost'].id !== undefined); + assert(data1.metadata['*.localhost'].username === 'perchik@gmail.com'); + assert(data1.metadata['*.localhost'].created !== undefined); + assert(data1.metadata['*.localhost'].alias === 'perchik2000'); + assert(data1.metadata['*.localhost'].aa !== undefined); + assert(data1.mfa === false); + + const data2 = await amqp.publishAndWait('users.verify', { + token: this.jwt, + audience: '*.localhost', + }); + + assert(data2.id !== undefined); + assert(data2.metadata['*.localhost'].id !== undefined); + assert(data2.metadata['*.localhost'].username === 'perchik@gmail.com'); + assert(data2.metadata['*.localhost'].created !== undefined); + assert(data2.metadata['*.localhost'].alias === 'perchik2000'); + assert(data2.metadata['*.localhost'].aa !== undefined); + assert(data2.mfa === false); + }); + + it('should be able to activate temporary activated user immediately', async () => { + const { amqp } = service; + + const data0 = await amqp.publishAndWait('users.register', { + username: 'perchik3000@gmail.com', + password: 'perchikisnotfatcat', + alias: 'perchik3000', + activate: false, + audience: 'tikkothouse', + }); + + assert(data0.user.metadata['*.localhost'].aa === undefined); + + // for creating token for activation without error + await delay(1000); + // @todo get token from email + const { secret } = await service.tokenManager.create({ + id: 'perchik3000@gmail.com', + action: 'activate', + }); + const data1 = await amqp.publishAndWait('users.activate', { token: secret }); + + assert(data1.user.metadata['*.localhost'].aa !== undefined); + }); +});