Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
90 changes: 41 additions & 49 deletions src/actions/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ 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 +37,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 +153,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 +189,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 +210,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 +252,10 @@ async function performRegistration({ service, params }) {

// assign alias
if (alias) {
await assignAlias.call(service, {
params: {
username,
alias,
internal: true,
},
});
await assignAlias.call(service, { params: { username, alias, internal: true } });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe its better to use service.dispatch so that validation & other middleware is triggered during this actiion

}

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 +267,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