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

Commit 50fdb41

Browse files
committed
Merge pull request #192 from Ghost141/reset-password
Reset Password API
2 parents cb65149 + edbe07d commit 50fdb41

16 files changed

+773
-270
lines changed

actions/resetPassword.js

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
/*
22
* Copyright (C) 2014 TopCoder Inc., All Rights Reserved.
33
*
4-
* @version 1.1
5-
* @author LazyChild, isv
4+
* @version 1.2
5+
* @author LazyChild, isv, Ghost_141
66
*
77
* changes in 1.1
88
* - implemented generateResetToken function
9+
* Changes in 1.2:
10+
* - Implement the Reset Password API
911
*/
1012
"use strict";
1113

1214
var async = require('async');
1315
var stringUtils = require("../common/stringUtils.js");
1416
var moment = require('moment-timezone');
17+
var _ = require('underscore');
1518

1619
var NotFoundError = require('../errors/NotFoundError');
1720
var BadRequestError = require('../errors/BadRequestError');
21+
var IllegalArgumentError = require('../errors/IllegalArgumentError');
1822
var UnauthorizedError = require('../errors/UnauthorizedError');
1923
var ForbiddenError = require('../errors/ForbiddenError');
2024
var TOKEN_ALPHABET = stringUtils.ALPHABET_ALPHA_EN + stringUtils.ALPHABET_DIGITS_EN;
@@ -46,30 +50,82 @@ var resolveUserByHandleOrEmail = function (handle, email, api, dbConnectionMap,
4650
};
4751

4852
/**
49-
* This is the function that stub reset password
53+
* Reset Password.
5054
*
5155
* @param {Object} api - The api object that is used to access the global infrastructure
5256
* @param {Object} connection - The connection object for the current request
5357
* @param {Function<connection, render>} next - The callback to be called after this function is done
5458
*/
5559
function resetPassword(api, connection, next) {
56-
var result, helper = api.helper;
60+
var result, helper = api.helper, sqlParams, userId, ldapEntryParams, oldPassword,
61+
dbConnectionMap = connection.dbConnectionMap,
62+
token = connection.params.token,
63+
handle = decodeURI(connection.params.handle).toLowerCase(),
64+
newPassword = connection.params.password,
65+
tokenKey = api.config.general.resetTokenPrefix + handle + api.config.general.resetTokenSuffix;
66+
5767
async.waterfall([
5868
function (cb) {
59-
if (connection.params.handle === "nonValid") {
60-
cb(new BadRequestError("The handle you entered is not valid"));
61-
} else if (connection.params.handle === "badLuck") {
62-
cb(new Error("Unknown server error. Please contact support."));
63-
} else if (connection.params.token === "unauthorized_token") {
64-
cb(new UnauthorizedError("Authentication credentials were missing or incorrect."));
65-
} else if (connection.params.token === "forbidden_token") {
66-
cb(new ForbiddenError("The request is understood, but it has been refused or access is not allowed."));
67-
} else {
68-
result = {
69-
"description": "Your password has been reset!"
70-
};
71-
cb();
69+
var error = helper.checkStringPopulated(token, 'token') ||
70+
helper.checkStringPopulated(handle, 'handle') ||
71+
helper.validatePassword(newPassword);
72+
if (error) {
73+
cb(error);
74+
return;
75+
}
76+
sqlParams = {
77+
handle: handle
78+
};
79+
api.dataAccess.executeQuery('get_user_information', sqlParams, dbConnectionMap, cb);
80+
},
81+
function (result, cb) {
82+
if (result.length === 0) {
83+
cb(new NotFoundError('The user is not exist.'));
84+
return;
85+
}
86+
userId = result[0].user_id;
87+
oldPassword = helper.decodePassword(result[0].old_password, helper.PASSWORD_HASH_KEY);
88+
sqlParams.handle = result[0].handle;
89+
helper.getCachedValue(tokenKey, cb);
90+
},
91+
function (cache, cb) {
92+
if (!_.isDefined(cache)) {
93+
// The token is either not assigned or is expired.
94+
cb(new BadRequestError('The token is expired or not existed. Please apply a new one.'));
95+
return;
96+
}
97+
if (cache !== token) {
98+
// The token don't match
99+
cb(new IllegalArgumentError('The token is incorrect.'));
100+
return;
101+
}
102+
sqlParams.password = helper.encodePassword(newPassword, helper.PASSWORD_HASH_KEY);
103+
api.dataAccess.executeQuery('update_password', sqlParams, dbConnectionMap, cb);
104+
},
105+
function (count, cb) {
106+
if (count !== 1) {
107+
cb(new Error('password is not updated successfully'));
108+
return;
72109
}
110+
ldapEntryParams = {
111+
userId: userId,
112+
handle: sqlParams.handle,
113+
oldPassword: oldPassword,
114+
newPassword: newPassword
115+
};
116+
api.ldapHelper.updateMemberPasswordLDAPEntry(ldapEntryParams, cb);
117+
},
118+
function (cb) {
119+
// Delete the token from cache system.
120+
api.cache.destroy(tokenKey, function (err) {
121+
cb(err);
122+
});
123+
},
124+
function (cb) {
125+
result = {
126+
description: 'Your password has been reset!'
127+
};
128+
cb();
73129
}
74130
], function (err) {
75131
if (err) {
@@ -93,7 +149,7 @@ function resetPassword(api, connection, next) {
93149
* @param {Function<err>} callback - the callback function.
94150
*/
95151
var generateResetToken = function (userHandle, userEmailAddress, api, callback) {
96-
var tokenCacheKey = 'tokens-' + userHandle + '-reset-token',
152+
var tokenCacheKey = api.config.general.resetTokenPrefix + userHandle + api.config.general.resetTokenSuffix,
97153
current,
98154
expireDate,
99155
expireDateString,
@@ -144,10 +200,16 @@ exports.resetPassword = {
144200
blockedConnectionTypes: [],
145201
outputExample: {},
146202
version: 'v2',
147-
cacheEnabled: false,
203+
transaction: 'write',
204+
cacheEnabled: false,
205+
databases: ["common_oltp"],
148206
run: function (api, connection, next) {
149-
api.log("Execute resetPassword#run", 'debug');
150-
resetPassword(api, connection, next);
207+
if (connection.dbConnectionMap) {
208+
api.log("Execute resetPassword#run", 'debug');
209+
resetPassword(api, connection, next);
210+
} else {
211+
api.helper.handleNoConnection(api, connection, next);
212+
}
151213
}
152214
};
153215

common/stringUtils.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/*
2-
* Copyright (C) 2013-2014 TopCoder Inc., All Rights Reserved.
2+
* Copyright (C) 2013 - 2014 TopCoder Inc., All Rights Reserved.
33
*
4-
* Version: 1.1
5-
* Author: isv
6-
*
7-
* changes in 1.1:
4+
* Version: 1.2
5+
* Author: TCSASSEMBLER, Ghost_141, isv
6+
* Changes in 1.1:
7+
* - add PUNCTUATION and PASSWORD_ALPHABET.
8+
* Changes in 1.2:
89
* - add generateRandomString function.
910
*/
1011

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

2223
var ALPHABET_DIGITS_EN = "0123456789";
2324

25+
/**
26+
* The valid characters for punctuation.
27+
* @since 1.1
28+
*/
29+
var PUNCTUATION = "-_.{}[]()";
30+
31+
/**
32+
* The valid characters for password.
33+
* @since 1.1
34+
*/
35+
var PASSWORD_ALPHABET = ALPHABET_ALPHA_EN + ALPHABET_DIGITS_EN + PUNCTUATION;
36+
2437
/**
2538
* Checks if string has all its characters in alphabet given.
2639
*
@@ -62,4 +75,6 @@ exports.generateRandomString = function (alphabet, length) {
6275
exports.ALPHABET_ALPHA_UPPER_EN = ALPHABET_ALPHA_UPPER_EN;
6376
exports.ALPHABET_ALPHA_LOWER_EN = ALPHABET_ALPHA_LOWER_EN;
6477
exports.ALPHABET_ALPHA_EN = ALPHABET_ALPHA_EN;
65-
exports.ALPHABET_DIGITS_EN = ALPHABET_DIGITS_EN;
78+
exports.ALPHABET_DIGITS_EN = ALPHABET_DIGITS_EN;
79+
exports.PUNCTUATION = PUNCTUATION;
80+
exports.PASSWORD_ALPHABET = PASSWORD_ALPHABET;

config.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Copyright (C) 2013 - 2014 TopCoder Inc., All Rights Reserved.
33
*
44
* @author vangavroche, Ghost_141, kurtrips, Sky_, isv, bugbuka
5-
* @version 1.20
5+
* @version 1.21
66
* changes in 1.1:
77
* - add defaultCacheLifetime parameter
88
* changes in 1.2:
@@ -48,6 +48,9 @@
4848
* changes in 1.20:
4949
* - add tcForumsServer property.
5050
* - add studioForumsServer property.
51+
* Changes in 1.21:
52+
* - add minPasswordLength and maxPasswordLength
53+
* - add resetTokenSuffix
5154
*/
5255
"use strict";
5356

@@ -89,6 +92,10 @@ config.general = {
8992
defaultCacheLifetime : process.env.CACHE_EXPIRY || 1000 * 60 * 10, //10 min default
9093
defaultAuthMiddlewareCacheLifetime : process.env.AUTH_MIDDLEWARE_CACHE_EXPIRY || 1000 * 60 * 10, //10 min default
9194
defaultUserCacheLifetime: process.env.USER_CACHE_EXPIRY || 1000 * 60 * 60 * 24, //24 hours default
95+
resetTokenPrefix: 'tokens-',
96+
resetTokenSuffix: '-reset-token',
97+
minPasswordLength: 8,
98+
maxPasswordLength: 30,
9299
defaultResetPasswordTokenCacheLifetime: process.env.RESET_PASSWORD_TOKEN_CACHE_EXPIRY ? parseInt(process.env.RESET_PASSWORD_TOKEN_CACHE_EXPIRY, 10) : 1000 * 60 * 30, //30 min
93100
resetPasswordTokenEmailSubject: process.env.RESET_PASSWORD_TOKEN_EMAIL_SUBJECT || "TopCoder Account Password Reset",
94101
cachePrefix: '',

deploy/development.bat

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ REM - added RESET_PASSWORD_TOKEN_EMAIL_SUBJECT environment variable
1111
REM - added REDIS_HOST environment variable
1212
REM - added REDIS_PORT environment variable
1313

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

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

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

8483
rem set REDIS_HOST=localhost
8584
rem set REDIS_PORT=6379

deploy/development.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,9 @@ export DEV_FORUM_JNDI=jnp://env.topcoder.com:1199
8080
export ACTIONHERO_CONFIG=./config.js
8181

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

8687
export REDIS_HOST=localhost
8788
export REDIS_PORT=6379

initializers/helper.js

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
/**
77
* This module contains helper functions.
88
* @author Sky_, Ghost_141, muzehyun, kurtrips, isv, LazyChild, hesibo
9-
* @version 1.22
9+
* @version 1.23
1010
* changes in 1.1:
1111
* - add mapProperties
1212
* changes in 1.2:
@@ -62,6 +62,10 @@
6262
* - add LIST_TYPE_REGISTRATION_STATUS_MAP and VALID_LIST_TYPE.
6363
* Changes in 1.22:
6464
* - add allTermsAgreed method.
65+
* Changes in 1.23:
66+
* - add validatePassword method.
67+
* - introduce the stringUtils in this file.
68+
* - add PASSWORD_HASH_KEY.
6569
*/
6670
"use strict";
6771

@@ -79,6 +83,7 @@ if (typeof String.prototype.startsWith !== 'function') {
7983
var async = require('async');
8084
var _ = require('underscore');
8185
var moment = require('moment');
86+
var stringUtils = require('../common/stringUtils');
8287
var IllegalArgumentError = require('../errors/IllegalArgumentError');
8388
var NotFoundError = require('../errors/NotFoundError');
8489
var BadRequestError = require('../errors/BadRequestError');
@@ -120,6 +125,13 @@ helper.both = {
120125
*/
121126
helper.MAX_INT = 2147483647;
122127

128+
/**
129+
* HASH KEY For Password
130+
*
131+
* @since 1.23
132+
*/
133+
helper.PASSWORD_HASH_KEY = process.env.PASSWORD_HASH_KEY || 'default';
134+
123135
/**
124136
* The name in api response to database name map.
125137
*/
@@ -1197,12 +1209,42 @@ helper.checkUserExists = function (handle, api, dbConnectionMap, callback) {
11971209
});
11981210
};
11991211

1212+
/**
1213+
* Validate the given password value.
1214+
* @param {String} password - the password value.
1215+
* @returns {Object} - Return error if the given password is invalid.
1216+
* @since 1.23
1217+
*/
1218+
helper.validatePassword = function (password) {
1219+
var value = password.trim(),
1220+
configGeneral = helper.api.config.general,
1221+
i,
1222+
error;
1223+
error = helper.checkStringPopulated(password, 'password');
1224+
if (error) {
1225+
return error;
1226+
}
1227+
if (value.length > configGeneral.maxPasswordLength) {
1228+
return new IllegalArgumentError('password may contain at most ' + configGeneral.maxPasswordLength + ' characters.');
1229+
}
1230+
if (value.length < configGeneral.minPasswordLength) {
1231+
return new IllegalArgumentError('password must be at least ' + configGeneral.minPasswordLength + ' characters in length.');
1232+
}
1233+
for (i = 0; i < password.length; i += 1) {
1234+
if (!_.contains(stringUtils.PASSWORD_ALPHABET, password.charAt(i))) {
1235+
return new IllegalArgumentError('Your password may contain only letters, numbers and ' + stringUtils.PUNCTUATION);
1236+
}
1237+
}
1238+
1239+
return null;
1240+
};
1241+
12001242
/**
12011243
* check if the every terms has been agreed
12021244
*
12031245
* @param {Array} terms - The terms.
12041246
* @returns {Boolean} true if all terms agreed otherwise false.
1205-
* @since 1.16
1247+
* @since 1.22
12061248
*/
12071249
helper.allTermsAgreed = function (terms) {
12081250
return _.every(terms, function (term) {

0 commit comments

Comments
 (0)