Skip to content
This repository was archived by the owner on Jan 23, 2025. It is now read-only.

Reset Password API #192

Merged
merged 3 commits into from
Apr 11, 2014
Merged
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
104 changes: 83 additions & 21 deletions actions/resetPassword.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
/*
* Copyright (C) 2014 TopCoder Inc., All Rights Reserved.
*
* @version 1.1
* @author LazyChild, isv
* @version 1.2
* @author LazyChild, isv, Ghost_141
*
* changes in 1.1
* - implemented generateResetToken function
* Changes in 1.2:
* - Implement the Reset Password API
*/
"use strict";

var async = require('async');
var stringUtils = require("../common/stringUtils.js");
var moment = require('moment-timezone');
var _ = require('underscore');

var NotFoundError = require('../errors/NotFoundError');
var BadRequestError = require('../errors/BadRequestError');
var IllegalArgumentError = require('../errors/IllegalArgumentError');
var UnauthorizedError = require('../errors/UnauthorizedError');
var ForbiddenError = require('../errors/ForbiddenError');
var TOKEN_ALPHABET = stringUtils.ALPHABET_ALPHA_EN + stringUtils.ALPHABET_DIGITS_EN;
Expand Down Expand Up @@ -46,30 +50,82 @@ var resolveUserByHandleOrEmail = function (handle, email, api, dbConnectionMap,
};

/**
* This is the function that stub reset password
* Reset Password.
*
* @param {Object} api - The api object that is used to access the global infrastructure
* @param {Object} connection - The connection object for the current request
* @param {Function<connection, render>} next - The callback to be called after this function is done
*/
function resetPassword(api, connection, next) {
var result, helper = api.helper;
var result, helper = api.helper, sqlParams, userId, ldapEntryParams, oldPassword,
dbConnectionMap = connection.dbConnectionMap,
token = connection.params.token,
handle = decodeURI(connection.params.handle).toLowerCase(),
newPassword = connection.params.password,
tokenKey = api.config.general.resetTokenPrefix + handle + api.config.general.resetTokenSuffix;

async.waterfall([
function (cb) {
if (connection.params.handle === "nonValid") {
cb(new BadRequestError("The handle you entered is not valid"));
} else if (connection.params.handle === "badLuck") {
cb(new Error("Unknown server error. Please contact support."));
} else if (connection.params.token === "unauthorized_token") {
cb(new UnauthorizedError("Authentication credentials were missing or incorrect."));
} else if (connection.params.token === "forbidden_token") {
cb(new ForbiddenError("The request is understood, but it has been refused or access is not allowed."));
} else {
result = {
"description": "Your password has been reset!"
};
cb();
var error = helper.checkStringPopulated(token, 'token') ||
helper.checkStringPopulated(handle, 'handle') ||
helper.validatePassword(newPassword);
if (error) {
cb(error);
return;
}
sqlParams = {
handle: handle
};
api.dataAccess.executeQuery('get_user_information', sqlParams, dbConnectionMap, cb);
},
function (result, cb) {
if (result.length === 0) {
cb(new NotFoundError('The user is not exist.'));
return;
}
userId = result[0].user_id;
oldPassword = helper.decodePassword(result[0].old_password, helper.PASSWORD_HASH_KEY);
sqlParams.handle = result[0].handle;
helper.getCachedValue(tokenKey, cb);
},
function (cache, cb) {
if (!_.isDefined(cache)) {
// The token is either not assigned or is expired.
cb(new BadRequestError('The token is expired or not existed. Please apply a new one.'));
return;
}
if (cache !== token) {
// The token don't match
cb(new IllegalArgumentError('The token is incorrect.'));
return;
}
sqlParams.password = helper.encodePassword(newPassword, helper.PASSWORD_HASH_KEY);
api.dataAccess.executeQuery('update_password', sqlParams, dbConnectionMap, cb);
},
function (count, cb) {
if (count !== 1) {
cb(new Error('password is not updated successfully'));
return;
}
ldapEntryParams = {
userId: userId,
handle: sqlParams.handle,
oldPassword: oldPassword,
newPassword: newPassword
};
api.ldapHelper.updateMemberPasswordLDAPEntry(ldapEntryParams, cb);
},
function (cb) {
// Delete the token from cache system.
api.cache.destroy(tokenKey, function (err) {
cb(err);
});
},
function (cb) {
result = {
description: 'Your password has been reset!'
};
cb();
}
], function (err) {
if (err) {
Expand All @@ -93,7 +149,7 @@ function resetPassword(api, connection, next) {
* @param {Function<err>} callback - the callback function.
*/
var generateResetToken = function (userHandle, userEmailAddress, api, callback) {
var tokenCacheKey = 'tokens-' + userHandle + '-reset-token',
var tokenCacheKey = api.config.general.resetTokenPrefix + userHandle + api.config.general.resetTokenSuffix,
current,
expireDate,
expireDateString,
Expand Down Expand Up @@ -144,10 +200,16 @@ exports.resetPassword = {
blockedConnectionTypes: [],
outputExample: {},
version: 'v2',
cacheEnabled: false,
transaction: 'write',
cacheEnabled: false,
databases: ["common_oltp"],
run: function (api, connection, next) {
api.log("Execute resetPassword#run", 'debug');
resetPassword(api, connection, next);
if (connection.dbConnectionMap) {
api.log("Execute resetPassword#run", 'debug');
resetPassword(api, connection, next);
} else {
api.helper.handleNoConnection(api, connection, next);
}
}
};

Expand Down
27 changes: 21 additions & 6 deletions common/stringUtils.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/*
* Copyright (C) 2013-2014 TopCoder Inc., All Rights Reserved.
* Copyright (C) 2013 - 2014 TopCoder Inc., All Rights Reserved.
*
* Version: 1.1
* Author: isv
*
* changes in 1.1:
* Version: 1.2
* Author: TCSASSEMBLER, Ghost_141, isv
* Changes in 1.1:
* - add PUNCTUATION and PASSWORD_ALPHABET.
* Changes in 1.2:
* - add generateRandomString function.
*/

Expand All @@ -21,6 +22,18 @@ var ALPHABET_ALPHA_EN = ALPHABET_ALPHA_LOWER_EN + ALPHABET_ALPHA_UPPER_EN;

var ALPHABET_DIGITS_EN = "0123456789";

/**
* The valid characters for punctuation.
* @since 1.1
*/
var PUNCTUATION = "-_.{}[]()";

/**
* The valid characters for password.
* @since 1.1
*/
var PASSWORD_ALPHABET = ALPHABET_ALPHA_EN + ALPHABET_DIGITS_EN + PUNCTUATION;

/**
* Checks if string has all its characters in alphabet given.
*
Expand Down Expand Up @@ -62,4 +75,6 @@ exports.generateRandomString = function (alphabet, length) {
exports.ALPHABET_ALPHA_UPPER_EN = ALPHABET_ALPHA_UPPER_EN;
exports.ALPHABET_ALPHA_LOWER_EN = ALPHABET_ALPHA_LOWER_EN;
exports.ALPHABET_ALPHA_EN = ALPHABET_ALPHA_EN;
exports.ALPHABET_DIGITS_EN = ALPHABET_DIGITS_EN;
exports.ALPHABET_DIGITS_EN = ALPHABET_DIGITS_EN;
exports.PUNCTUATION = PUNCTUATION;
exports.PASSWORD_ALPHABET = PASSWORD_ALPHABET;
9 changes: 8 additions & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (C) 2013 - 2014 TopCoder Inc., All Rights Reserved.
*
* @author vangavroche, Ghost_141, kurtrips, Sky_, isv, bugbuka
* @version 1.20
* @version 1.21
* changes in 1.1:
* - add defaultCacheLifetime parameter
* changes in 1.2:
Expand Down Expand Up @@ -48,6 +48,9 @@
* changes in 1.20:
* - add tcForumsServer property.
* - add studioForumsServer property.
* Changes in 1.21:
* - add minPasswordLength and maxPasswordLength
* - add resetTokenSuffix
*/
"use strict";

Expand Down Expand Up @@ -89,6 +92,10 @@ config.general = {
defaultCacheLifetime : process.env.CACHE_EXPIRY || 1000 * 60 * 10, //10 min default
defaultAuthMiddlewareCacheLifetime : process.env.AUTH_MIDDLEWARE_CACHE_EXPIRY || 1000 * 60 * 10, //10 min default
defaultUserCacheLifetime: process.env.USER_CACHE_EXPIRY || 1000 * 60 * 60 * 24, //24 hours default
resetTokenPrefix: 'tokens-',
resetTokenSuffix: '-reset-token',
minPasswordLength: 8,
maxPasswordLength: 30,
defaultResetPasswordTokenCacheLifetime: process.env.RESET_PASSWORD_TOKEN_CACHE_EXPIRY ? parseInt(process.env.RESET_PASSWORD_TOKEN_CACHE_EXPIRY, 10) : 1000 * 60 * 30, //30 min
resetPasswordTokenEmailSubject: process.env.RESET_PASSWORD_TOKEN_EMAIL_SUBJECT || "TopCoder Account Password Reset",
cachePrefix: '',
Expand Down
3 changes: 1 addition & 2 deletions deploy/development.bat
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ REM - added RESET_PASSWORD_TOKEN_EMAIL_SUBJECT environment variable
REM - added REDIS_HOST environment variable
REM - added REDIS_PORT environment variable


REM tests rely on caching being off. But set this to a real value (or remove) while coding.

set VM_IP=%TC_VM_IP%
Expand Down Expand Up @@ -78,8 +77,8 @@ set DEV_FORUM_JNDI=jnp://env.topcoder.com:1199
set ACTIONHERO_CONFIG=./config.js

REM The period for expiring the generated tokens for password resetting (in milliseconds)
set RESET_PASSWORD_TOKEN_CACHE_EXPIRY=15000
set RESET_PASSWORD_TOKEN_EMAIL_SUBJECT=TopCoder Account Password Reset
set RESET_PASSWORD_TOKEN_CACHE_EXPIRY=180000

rem set REDIS_HOST=localhost
rem set REDIS_PORT=6379
3 changes: 2 additions & 1 deletion deploy/development.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ export DEV_FORUM_JNDI=jnp://env.topcoder.com:1199
export ACTIONHERO_CONFIG=./config.js

## The period for expiring the generated tokens for password resetting
export RESET_PASSWORD_TOKEN_CACHE_EXPIRY=1800000
export RESET_PASSWORD_TOKEN_EMAIL_SUBJECT=TopCoder Account Password Reset
# Set this to 180000 which is 3 mins. This will help saving time for test.
export RESET_PASSWORD_TOKEN_CACHE_EXPIRY=180000

export REDIS_HOST=localhost
export REDIS_PORT=6379
46 changes: 44 additions & 2 deletions initializers/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/**
* This module contains helper functions.
* @author Sky_, Ghost_141, muzehyun, kurtrips, isv, LazyChild, hesibo
* @version 1.22
* @version 1.23
* changes in 1.1:
* - add mapProperties
* changes in 1.2:
Expand Down Expand Up @@ -62,6 +62,10 @@
* - add LIST_TYPE_REGISTRATION_STATUS_MAP and VALID_LIST_TYPE.
* Changes in 1.22:
* - add allTermsAgreed method.
* Changes in 1.23:
* - add validatePassword method.
* - introduce the stringUtils in this file.
* - add PASSWORD_HASH_KEY.
*/
"use strict";

Expand All @@ -79,6 +83,7 @@ if (typeof String.prototype.startsWith !== 'function') {
var async = require('async');
var _ = require('underscore');
var moment = require('moment');
var stringUtils = require('../common/stringUtils');
var IllegalArgumentError = require('../errors/IllegalArgumentError');
var NotFoundError = require('../errors/NotFoundError');
var BadRequestError = require('../errors/BadRequestError');
Expand Down Expand Up @@ -120,6 +125,13 @@ helper.both = {
*/
helper.MAX_INT = 2147483647;

/**
* HASH KEY For Password
*
* @since 1.23
*/
helper.PASSWORD_HASH_KEY = process.env.PASSWORD_HASH_KEY || 'default';

/**
* The name in api response to database name map.
*/
Expand Down Expand Up @@ -1197,12 +1209,42 @@ helper.checkUserExists = function (handle, api, dbConnectionMap, callback) {
});
};

/**
* Validate the given password value.
* @param {String} password - the password value.
* @returns {Object} - Return error if the given password is invalid.
* @since 1.23
*/
helper.validatePassword = function (password) {
var value = password.trim(),
configGeneral = helper.api.config.general,
i,
error;
error = helper.checkStringPopulated(password, 'password');
if (error) {
return error;
}
if (value.length > configGeneral.maxPasswordLength) {
return new IllegalArgumentError('password may contain at most ' + configGeneral.maxPasswordLength + ' characters.');
}
if (value.length < configGeneral.minPasswordLength) {
return new IllegalArgumentError('password must be at least ' + configGeneral.minPasswordLength + ' characters in length.');
}
for (i = 0; i < password.length; i += 1) {
if (!_.contains(stringUtils.PASSWORD_ALPHABET, password.charAt(i))) {
return new IllegalArgumentError('Your password may contain only letters, numbers and ' + stringUtils.PUNCTUATION);
}
}

return null;
};

/**
* check if the every terms has been agreed
*
* @param {Array} terms - The terms.
* @returns {Boolean} true if all terms agreed otherwise false.
* @since 1.16
* @since 1.22
*/
helper.allTermsAgreed = function (terms) {
return _.every(terms, function (term) {
Expand Down
Loading