Skip to content

Commit

Permalink
feat: delete unconfirmed users code
Browse files Browse the repository at this point in the history
  • Loading branch information
pajgo committed Jul 31, 2019
1 parent 9e25f85 commit 0dc98f2
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 54 deletions.
5 changes: 5 additions & 0 deletions schemas/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"amqp",
"redis",
"deleteInactiveAccounts",
"deleteInactiveAccountsInterval",
"jwt",
"validation",
"server",
Expand All @@ -19,6 +20,10 @@
"type": "integer",
"minimum": 0
},
"deleteInactiveAccountsInterval": {
"type": "integer",
"minimum": 1
},
"admins": {
"type": "array",
"items": {
Expand Down
3 changes: 3 additions & 0 deletions src/actions/activate.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const jwt = require('../utils/jwt.js');
const { getInternalData } = require('../utils/userData');
const getMetadata = require('../utils/getMetadata');
const handlePipeline = require('../utils/pipelineError.js');
const { removeInactiveUser } = require('../utils/inactiveUsers');
const {
USERS_INDEX,
USERS_DATA,
Expand Down Expand Up @@ -126,6 +127,8 @@ function activateAccount(data, metadata) {
.persist(userKey)
.sadd(USERS_INDEX, userId);

removeInactiveUser(pipeline, userId);

if (alias) {
pipeline.sadd(USERS_PUBLIC_INDEX, userId);
}
Expand Down
3 changes: 2 additions & 1 deletion src/actions/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const checkLimits = require('../utils/checkIpLimits');
const challenge = require('../utils/challenges/challenge');
const handlePipeline = require('../utils/pipelineError');
const hashPassword = require('../utils/register/password/hash');
const { addInactiveUser } = require('../utils/inactiveUsers');
const {
USERS_REF,
USERS_INDEX,
Expand Down Expand Up @@ -208,7 +209,7 @@ async function performRegistration({ service, params }) {
pipeline.hset(USERS_USERNAME_TO_ID, username, userId);

if (activate === false && config.deleteInactiveAccounts >= 0) {
pipeline.expire(userDataKey, config.deleteInactiveAccounts);
addInactiveUser(pipeline, userId, audience);
}

await pipeline.exec().then(handlePipeline);
Expand Down
56 changes: 3 additions & 53 deletions src/actions/remove.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,14 @@ const { ActionTransport } = require('@microfleet/core');
const Promise = require('bluebird');
const Errors = require('common-errors');
const intersection = require('lodash/intersection');
const get = require('../utils/get-value');
const key = require('../utils/key');
const removeUserUtil = require('../utils/removeUser');
const { getInternalData } = require('../utils/userData');
const getMetadata = require('../utils/getMetadata');
const handlePipeline = require('../utils/pipelineError');
const {
USERS_INDEX,
USERS_PUBLIC_INDEX,
USERS_ALIAS_TO_ID,
USERS_SSO_TO_ID,
USERS_USERNAME_TO_ID,
USERS_USERNAME_FIELD,
USERS_DATA,
USERS_METADATA,
USERS_TOKENS,
USERS_ID_FIELD,
USERS_ALIAS_FIELD,
USERS_ADMIN_ROLE,
USERS_SUPER_ADMIN_ROLE,
USERS_ACTION_ACTIVATE,
USERS_ACTION_RESET,
USERS_ACTION_PASSWORD,
USERS_ACTION_REGISTER,
THROTTLE_PREFIX,
SSO_PROVIDERS,
} = require('../constants');

// intersection of priority users
Expand Down Expand Up @@ -69,41 +52,8 @@ async function removeUser({ params }) {
}

const transaction = redis.pipeline();
const alias = internal[USERS_ALIAS_FIELD];
const userId = internal[USERS_ID_FIELD];
const resolvedUsername = internal[USERS_USERNAME_FIELD];

if (alias) {
transaction.hdel(USERS_ALIAS_TO_ID, alias.toLowerCase(), alias);
}

transaction.hdel(USERS_USERNAME_TO_ID, resolvedUsername);

// remove refs to SSO account
for (const provider of SSO_PROVIDERS) {
const uid = get(internal, `${provider}.uid`, { default: false });

if (uid) {
transaction.hdel(USERS_SSO_TO_ID, uid);
}
}

// clean indices
transaction.srem(USERS_PUBLIC_INDEX, userId);
transaction.srem(USERS_INDEX, userId);

// remove metadata & internal data
transaction.del(key(userId, USERS_DATA));
transaction.del(key(userId, USERS_METADATA, audience));

// remove auth tokens
transaction.del(key(userId, USERS_TOKENS));

// remove throttling on actions
transaction.del(key(THROTTLE_PREFIX, USERS_ACTION_ACTIVATE, userId));
transaction.del(key(THROTTLE_PREFIX, USERS_ACTION_PASSWORD, userId));
transaction.del(key(THROTTLE_PREFIX, USERS_ACTION_REGISTER, userId));
transaction.del(key(THROTTLE_PREFIX, USERS_ACTION_RESET, userId));
// assign remove logic
removeUserUtil(transaction, internal, audience);

// complete it
return transaction
Expand Down
6 changes: 6 additions & 0 deletions src/configs/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ exports.name = 'ms-users';
*/
exports.deleteInactiveAccounts = 30 * 24 * 60 * 60;

/**
* Seconds to run inactive account cleanup process
* @type {Number} seconds - defaults to 1 day;
*/
exports.deleteInactiveAccountsInterval = 1 * 24 * 60 * 60;

/**
* Flake ids - sequential unique 64 bit ids
* NOTE: this is used for JWT issue id & should be used for the
Expand Down
5 changes: 5 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ module.exports = exports = {
USERS_PUBLIC_INDEX: 'users-public',
USERS_REFERRAL_INDEX: 'users-referral',
ORGANIZATIONS_INDEX: 'organization-iterator-set',
// inactive user id's list
USERS_ACTIVATE: 'users-activate',
// handles json list of audiences provided at registrations
USERS_ACTIVATE_AUDIENCE: 'users-activate-audiences',

// id mapping
USERS_ALIAS_TO_ID: 'users-alias',
USERS_SSO_TO_ID: 'users-sso-hash',
Expand Down
3 changes: 3 additions & 0 deletions src/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const RedisCluster = require('ioredis').Cluster;
const Flakeless = require('ms-flakeless');
const conf = require('./config');
const get = require('./utils/get-value');
const { getCleanerTask, stopCleanerTask } = require('./utils/inactiveUsers');

/**
* @namespace Users
Expand Down Expand Up @@ -76,6 +77,7 @@ module.exports = class Users extends Microfleet {
// init token manager
const tokenManagerOpts = { backend: { connection: redis } };
this.tokenManager = new TokenManager(merge({}, config.tokenManager, tokenManagerOpts));
this.userCleanTask = getCleanerTask(this);
});

this.on('plugin:start:http', (server) => {
Expand All @@ -96,6 +98,7 @@ module.exports = class Users extends Microfleet {
this.on(`plugin:close:${this.redisType}`, () => {
this.dlock = null;
this.tokenManager = null;
stopCleanerTask(this.userCleanTask);
});

// add migration connector
Expand Down
113 changes: 113 additions & 0 deletions src/utils/inactiveUsers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
const Promise = require('bluebird');
const { LockAcquisitionError } = require('ioredis-lock');

const { getInternalData } = require('./userData');
const removeUser = require('./removeUser');

const { USERS_ACTIVATE,
USERS_ACTIVATE_AUDIENCE,
} = require('../constants');

const lockKey = `${USERS_ACTIVATE}:data-lock`;

// Get user ids having score lt current time - interval
async function getInactiveUsers(redis, interval) {
const start = '-inf';
const end = Date.now() - (interval * 1000);
return redis.zrangebyscore(USERS_ACTIVATE, start, end);
}

async function getAudience(redis, userId) {
const audience = await redis.hget(USERS_ACTIVATE_AUDIENCE, userId);
return audience;
}

async function deleteUser(userId, audience) {
const { redis } = this;
const context = { redis, audience };

const internal = await Promise
.bind(context, userId)
.then(getInternalData);

const transaction = redis.multi();

removeUser(transaction, internal, audience);
return transaction.exec();
}

/**
* Add user id to inacive users list
* @param {ioredis} redis
* @param {userId} userId
* @param {audience[]} audience
*/
function addInactiveUser(redis, userId, audience) {
const created = Date.now();
redis.zadd(USERS_ACTIVATE, userId, created);
redis.hset(USERS_ACTIVATE_AUDIENCE, userId, JSON.stringify(audience));
}

/**
* Remove user id from inactive users list
* @param {ioredis} redis
* @param {userId} userId
*/
function removeInactiveUser(redis, userId) {
redis.zrem(USERS_ACTIVATE, userId);
redis.hdel(USERS_ACTIVATE_AUDIENCE, userId);
}

/**
* Clean all users, who did't pass activation
* Call in `service` context
*/
async function cleanInactiveUsers() {
const { redis } = this;
const { deleteInactiveAccounts } = this.config;

const lock = await this.dlock.once(lockKey);
const inactiveAccounts = await getInactiveUsers(redis, deleteInactiveAccounts);

inactiveAccounts.forEach(async (acc) => {
const accAudiences = getAudience(redis, acc);
await deleteUser.bind(this, acc, accAudiences);
await removeInactiveUser(redis, acc);
});

lock.release().reflect();
}

/**
* Returns inactive users cleanup task
* @param {Mfleet} service
* @returns {NodeJS.Timeout}
*/
function getCleanerTask(service) {
const { deleteInactiveAccountsInterval: interval } = service.config;

return setInterval(() => {
cleanInactiveUsers.call(service)
.catch(LockAcquisitionError, () => {})
.catch((error) => {
service.log.error('Inactive User Clean task error', error);
});
}, interval * 1000);
}

/**
* Stops currently running task
* @param {NodeJS.Timeout} task
*/
function stopCleanerTask(task) {
clearInterval(task);
}


module.exports = {
addInactiveUser,
removeInactiveUser,
cleanInactiveUsers,
getCleanerTask,
stopCleanerTask,
};
73 changes: 73 additions & 0 deletions src/utils/removeUser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const get = require('../utils/get-value');
const key = require('../utils/key');

const { USERS_ALIAS_FIELD,
USERS_ID_FIELD,
USERS_ALIAS_TO_ID,
USERS_USERNAME_TO_ID,
USERS_USERNAME_FIELD,
USERS_PUBLIC_INDEX,
USERS_INDEX,
USERS_DATA,
USERS_METADATA,
USERS_TOKENS,
SSO_PROVIDERS,
THROTTLE_PREFIX,
USERS_ACTION_ACTIVATE,
USERS_ACTION_REGISTER,
USERS_ACTION_PASSWORD,
USERS_ACTION_RESET,
USERS_SSO_TO_ID } = require('../constants');

/**
* Assigns common user remove logic into passed transaction
* @param {ioredis} transaction
* @param {ms-user data} internal
* @param {ms-user audience/[]} _audiences
*/
function removeUser(transaction, internal, _audiences) {
const audiences = Array.isArray(_audiences) ? _audiences : [_audiences];

const alias = internal[USERS_ALIAS_FIELD];
const userId = internal[USERS_ID_FIELD];
const resolvedUsername = internal[USERS_USERNAME_FIELD];

if (alias) {
transaction.hdel(USERS_ALIAS_TO_ID, alias.toLowerCase(), alias);
}

transaction.hdel(USERS_USERNAME_TO_ID, resolvedUsername);

// remove refs to SSO account
for (const provider of SSO_PROVIDERS) {
const uid = get(internal, `${provider}.uid`, { default: false });

if (uid) {
transaction.hdel(USERS_SSO_TO_ID, uid);
}
}

// clean indices
transaction.srem(USERS_PUBLIC_INDEX, userId);
transaction.srem(USERS_INDEX, userId);

// remove metadata & internal data
transaction.del(key(userId, USERS_DATA));

audiences.forEach((audience) => {
transaction.del(key(userId, USERS_METADATA, audience));
});

// remove auth tokens
transaction.del(key(userId, USERS_TOKENS));

// remove throttling on actions
transaction.del(key(THROTTLE_PREFIX, USERS_ACTION_ACTIVATE, userId));
transaction.del(key(THROTTLE_PREFIX, USERS_ACTION_PASSWORD, userId));
transaction.del(key(THROTTLE_PREFIX, USERS_ACTION_REGISTER, userId));
transaction.del(key(THROTTLE_PREFIX, USERS_ACTION_RESET, userId));

return transaction;
}

module.exports = removeUser;

0 comments on commit 0dc98f2

Please sign in to comment.