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: adds ability for temporary activation for user #498

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/actions/activate.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {
USERS_USERNAME_FIELD,
USERS_ACTION_ACTIVATE,
USERS_ACTIVATED_FIELD,
USERS_TEMP_ACTIVATED_TIME_FIELD,
} = require('../constants.js');

// cache error
Expand Down Expand Up @@ -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);

Expand Down
10 changes: 5 additions & 5 deletions src/actions/alias.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/actions/challenge.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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];
Expand Down
4 changes: 2 additions & 2 deletions src/actions/disposable-password.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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, {
Expand Down
4 changes: 2 additions & 2 deletions src/actions/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down
91 changes: 41 additions & 50 deletions src/actions/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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());
Expand All @@ -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);
Expand All @@ -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,
};
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/actions/requestPassword.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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])
Expand Down
4 changes: 2 additions & 2 deletions src/actions/updatePassword.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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);

Expand Down
28 changes: 11 additions & 17 deletions src/actions/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/configs/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,8 @@ exports.mfa = {
window: 10,
},
};

exports.temporaryActivation = {
enabled: false,
validTimeMs: 10 * 24 * 60 * 60 * 1000,
};
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading