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

Commit fc155e5

Browse files
committed
Merge pull request #179 from isvisv/master
Generate Reset Token
2 parents 775c144 + 4e1a827 commit fc155e5

17 files changed

+770
-66
lines changed

actions/resetPassword.js

Lines changed: 130 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,49 @@
11
/*
22
* Copyright (C) 2014 TopCoder Inc., All Rights Reserved.
33
*
4-
* @version 1.0
5-
* @author LazyChild
4+
* @version 1.1
5+
* @author LazyChild, isv
6+
*
7+
* changes in 1.1
8+
* - implemented generateResetToken function
69
*/
710
"use strict";
811

912
var async = require('async');
13+
var stringUtils = require("../common/stringUtils.js");
14+
var moment = require('moment-timezone');
15+
16+
var NotFoundError = require('../errors/NotFoundError');
1017
var BadRequestError = require('../errors/BadRequestError');
1118
var UnauthorizedError = require('../errors/UnauthorizedError');
1219
var ForbiddenError = require('../errors/ForbiddenError');
20+
var TOKEN_ALPHABET = stringUtils.ALPHABET_ALPHA_EN + stringUtils.ALPHABET_DIGITS_EN;
21+
22+
/**
23+
* Looks up for the user account matching specified handle (either TopCoder handle or social login username) or email.
24+
* If user account is not found then NotFoundError is returned to callback; otherwise ID for found user account is
25+
* passed to callback.
26+
*
27+
* @param {String} handle - the handle to check.
28+
* @param {String} email - the email to check.
29+
* @param {Object} api - the action hero api object.
30+
* @param {Object} dbConnectionMap - the database connection map.
31+
* @param {Function<err, row>} callback - the callback function.
32+
*/
33+
var resolveUserByHandleOrEmail = function (handle, email, api, dbConnectionMap, callback) {
34+
api.dataAccess.executeQuery("find_user_by_handle_or_email", { handle: handle, email: email }, dbConnectionMap,
35+
function (err, result) {
36+
if (err) {
37+
callback(err);
38+
return;
39+
}
40+
if (result && result[0]) {
41+
callback(null, result[0]);
42+
} else {
43+
callback(new NotFoundError("User does not exist"));
44+
}
45+
});
46+
};
1347

1448
/**
1549
* This is the function that stub reset password
@@ -21,24 +55,21 @@ var ForbiddenError = require('../errors/ForbiddenError');
2155
function resetPassword(api, connection, next) {
2256
var result, helper = api.helper;
2357
async.waterfall([
24-
function(cb) {
25-
if (connection.params.handle == "nonValid") {
58+
function (cb) {
59+
if (connection.params.handle === "nonValid") {
2660
cb(new BadRequestError("The handle you entered is not valid"));
27-
return;
28-
} else if (connection.params.handle == "badLuck") {
61+
} else if (connection.params.handle === "badLuck") {
2962
cb(new Error("Unknown server error. Please contact support."));
30-
return;
31-
} else if (connection.params.token == "unauthorized_token") {
63+
} else if (connection.params.token === "unauthorized_token") {
3264
cb(new UnauthorizedError("Authentication credentials were missing or incorrect."));
33-
return;
34-
} else if (connection.params.token == "forbidden_token") {
65+
} else if (connection.params.token === "forbidden_token") {
3566
cb(new ForbiddenError("The request is understood, but it has been refused or access is not allowed."));
36-
return;
67+
} else {
68+
result = {
69+
"description": "Your password has been reset!"
70+
};
71+
cb();
3772
}
38-
result = {
39-
"description": "Your password has been reset!"
40-
};
41-
cb();
4273
}
4374
], function (err) {
4475
if (err) {
@@ -50,45 +81,55 @@ function resetPassword(api, connection, next) {
5081
});
5182
}
5283

84+
5385
/**
54-
* This is the function that stub reset token
86+
* Generates the token for resetting the password for specified user account. First checks if non-expired token already
87+
* exists for the user. If so then BadRequestError is passed to callback. Otherwise a new token is generated and saved
88+
* to cache and returned to callback.
5589
*
90+
* @param {Number} userHandle - handle of user to generate token for.
91+
* @param {String} userEmailAddress - email address of user to email generated token to.
5692
* @param {Object} api - The api object that is used to access the global infrastructure
57-
* @param {Object} connection - The connection object for the current request
58-
* @param {Function<connection, render>} next - The callback to be called after this function is done
93+
* @param {Function<err>} callback - the callback function.
5994
*/
60-
function generateResetToken(api, connection, next) {
61-
var result, helper = api.helper;
62-
async.waterfall([
63-
function(cb) {
64-
if (connection.params.handle == "nonValid" || connection.params.email == "nonValid@test.com") {
65-
cb(new BadRequestError("The handle you entered is not valid"));
66-
return;
67-
} else if (connection.params.handle == "badLuck" || connection.params.email == "badLuck@test.com") {
68-
cb(new Error("Unknown server error. Please contact support."));
69-
return;
70-
}
95+
var generateResetToken = function (userHandle, userEmailAddress, api, callback) {
96+
var tokenCacheKey = 'tokens-' + userHandle + '-reset-token',
97+
current,
98+
expireDate,
99+
expireDateString,
100+
emailParams;
71101

72-
if (connection.params.handle == "googleSocial" || connection.params.email == "googleSocial@test.com") {
73-
result = {
74-
"socialLogin": "Google"
75-
};
76-
} else {
77-
result = {
78-
"token": "a3cbG"
79-
};
80-
}
81-
cb();
82-
}
83-
], function (err) {
102+
api.helper.getCachedValue(tokenCacheKey, function (err, token) {
84103
if (err) {
85-
helper.handleError(api, connection, err);
104+
callback(err);
105+
} else if (token) {
106+
// Non-expired token already exists for this user - raise an error
107+
callback(new BadRequestError("You have already requested the reset token, please find it in your email inbox."
108+
+ " If it's not there. Please contact support@topcoder.com."));
86109
} else {
87-
connection.response = result;
110+
// There is no token - generate new one
111+
var newToken = stringUtils.generateRandomString(TOKEN_ALPHABET, 6),
112+
lifetime = api.config.general.defaultResetPasswordTokenCacheLifetime;
113+
api.cache.save(tokenCacheKey, newToken, lifetime);
114+
115+
// Send email with token to user
116+
current = new Date();
117+
expireDate = current.setSeconds(current.getSeconds() + lifetime / 1000);
118+
expireDateString = moment(expireDate).tz('America/New_York').format('YYYY-MM-DD HH:mm:ss z');
119+
emailParams = {
120+
handle: userHandle,
121+
token: newToken,
122+
expiry: expireDateString,
123+
template: 'reset_token_email',
124+
subject: api.config.general.resetPasswordTokenEmailSubject,
125+
toAddress: userEmailAddress
126+
};
127+
api.tasks.enqueue("sendEmail", emailParams, 'default');
128+
129+
callback(null, newToken);
88130
}
89-
next(connection, true);
90131
});
91-
}
132+
};
92133

93134
/**
94135
* Reset password API.
@@ -113,17 +154,58 @@ exports.resetPassword = {
113154
* Generate reset token API.
114155
*/
115156
exports.generateResetToken = {
116-
"name": "generateResetToken",
117-
"description": "generateResetToken",
157+
name: "generateResetToken",
158+
description: "generateResetToken",
118159
inputs: {
119160
required: [],
120161
optional: ["handle", "email"]
121162
},
122163
blockedConnectionTypes: [],
123164
outputExample: {},
124165
version: 'v2',
166+
cacheEnabled: false,
167+
transaction: 'read',
168+
databases: ["common_oltp"],
125169
run: function (api, connection, next) {
126170
api.log("Execute generateResetToken#run", 'debug');
127-
generateResetToken(api, connection, next);
171+
if (connection.dbConnectionMap) {
172+
async.waterfall([
173+
function (cb) { // Find the user either by handle or by email
174+
// Get handle, email from request parameters
175+
var handle = (connection.params.handle || '').trim(),
176+
email = (connection.params.email || '').trim(),
177+
byHandle = (handle !== ''),
178+
byEmail = (email !== '');
179+
180+
// Validate the input parameters, either handle or email but not both must be provided
181+
if (byHandle && byEmail) {
182+
cb(new BadRequestError("Both handle and email are specified"));
183+
} else if (!byHandle && !byEmail) {
184+
cb(new BadRequestError("Either handle or email must be specified"));
185+
} else {
186+
resolveUserByHandleOrEmail(handle, email, api, connection.dbConnectionMap, cb);
187+
}
188+
}, function (result, cb) {
189+
if (result.social_login_provider_name !== '') {
190+
// For social login accounts return the provider name
191+
cb(null, null, result.social_login_provider_name);
192+
} else {
193+
// Generate reset password token for user
194+
generateResetToken(result.handle, result.email_address, api, cb);
195+
}
196+
}
197+
], function (err, newToken, socialProviderName) {
198+
if (err) {
199+
api.helper.handleError(api, connection, err);
200+
} else if (newToken) {
201+
connection.response = {successful: true};
202+
} else if (socialProviderName) {
203+
connection.response = {socialProvider: socialProviderName};
204+
}
205+
next(connection, true);
206+
});
207+
} else {
208+
api.helper.handleNoConnection(api, connection, next);
209+
}
128210
}
129211
};

apiary.apib

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,17 +1220,17 @@ Register a new user.
12201220

12211221
## Generate Reset Token [/users/resetToken/?handle={handle}&email={email}]
12221222
### Generate Reset Token [GET]
1223-
- return token for topcoder user
1223+
- return "successful" flag set to true
12241224
- return social provider name for social login user
12251225

12261226
+ Parameters
1227-
+ handle (optional, string, `iRabbit`) ... Member Handle
1228-
+ email (optional, string, `test@test.com`) ... Email Address
1227+
+ handle (optional, string, `iRabbit`) ... Member Handle or Social Login Username
1228+
+ email (optional, string, `test@test.com`) ... Member Email (mutually exclusive with handle parameter)
12291229

12301230
+ Response 200 (application/json)
12311231

12321232
{
1233-
"token":"a3cbG"
1233+
"successful":"true"
12341234
}
12351235

12361236
+ Response 200 (application/json)
@@ -1244,7 +1244,31 @@ Register a new user.
12441244
{
12451245
"name":"Bad Request",
12461246
"value":"400",
1247-
"description":"The handle you entered is not valid"
1247+
"description":"Either handle or email must be specified"
1248+
}
1249+
1250+
+ Response 400 (application/json)
1251+
1252+
{
1253+
"name":"Bad Request",
1254+
"value":"400",
1255+
"description":"Both handle and email are specified"
1256+
}
1257+
1258+
+ Response 400 (application/json)
1259+
1260+
{
1261+
"name":"Bad Request",
1262+
"value":"400",
1263+
"description":"You have already requested the reset token, please find it in your email inbox. If it's not there. Please contact support@topcoder.com."
1264+
}
1265+
1266+
+ Response 404 (application/json)
1267+
1268+
{
1269+
"name":"Not Found",
1270+
"value":"404",
1271+
"description":"User does not exist"
12481272
}
12491273

12501274
+ Response 500 (application/json)
@@ -1263,6 +1287,7 @@ Register a new user.
12631287
"description":"Servers are up but overloaded. Try again later."
12641288
}
12651289

1290+
12661291
## Reset Password [/users/resetPassword/{handle}]
12671292
### Reset Password [POST]
12681293
+ Parameters

common/stringUtils.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
/*
2-
* Copyright (C) 2013 TopCoder Inc., All Rights Reserved.
2+
* Copyright (C) 2013-2014 TopCoder Inc., All Rights Reserved.
33
*
4-
* Version: 1.0
5-
* Author: TCSASSEMBLER
4+
* Version: 1.1
5+
* Author: isv
6+
*
7+
* changes in 1.1:
8+
* - add generateRandomString function.
69
*/
710

811
"use strict";
@@ -39,6 +42,23 @@ exports.containsOnly = function (string, alphabet) {
3942
return true;
4043
};
4144

45+
/**
46+
* Generates random string of specified length using the symbols from the specified alphabet.
47+
*
48+
* @param {String} alphabet - alphabet to use for string generation.
49+
* @param {Number} length - the length for the string to be generated.
50+
* @since 1.1
51+
*/
52+
exports.generateRandomString = function (alphabet, length) {
53+
var text = '', i, index;
54+
for (i = 0; i < length; i = i + 1) {
55+
index = Math.random() * alphabet.length;
56+
text += alphabet.charAt(index);
57+
}
58+
59+
return text;
60+
};
61+
4262
exports.ALPHABET_ALPHA_UPPER_EN = ALPHABET_ALPHA_UPPER_EN;
4363
exports.ALPHABET_ALPHA_LOWER_EN = ALPHABET_ALPHA_LOWER_EN;
4464
exports.ALPHABET_ALPHA_EN = ALPHABET_ALPHA_EN;

config.js

Lines changed: 6 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
5-
* @version 1.17
5+
* @version 1.19
66
* changes in 1.1:
77
* - add defaultCacheLifetime parameter
88
* changes in 1.2:
@@ -41,6 +41,9 @@
4141
* - add welcome email property.
4242
* Changes in 1.17:
4343
* - add maxRSSLength.
44+
* changes in 1.19:
45+
* - add defaultResetPasswordTokenCacheLifetime property.
46+
* - add resetPasswordTokenEmailSubject property.
4447
*/
4548
"use strict";
4649

@@ -82,6 +85,8 @@ config.general = {
8285
defaultCacheLifetime : process.env.CACHE_EXPIRY || 1000 * 60 * 10, //10 min default
8386
defaultAuthMiddlewareCacheLifetime : process.env.AUTH_MIDDLEWARE_CACHE_EXPIRY || 1000 * 60 * 10, //10 min default
8487
defaultUserCacheLifetime: process.env.USER_CACHE_EXPIRY || 1000 * 60 * 60 * 24, //24 hours default
88+
defaultResetPasswordTokenCacheLifetime: process.env.RESET_PASSWORD_TOKEN_CACHE_EXPIRY ? parseInt(process.env.RESET_PASSWORD_TOKEN_CACHE_EXPIRY, 10) : 1000 * 60 * 30, //30 min
89+
resetPasswordTokenEmailSubject: process.env.RESET_PASSWORD_TOKEN_EMAIL_SUBJECT || "TopCoder Account Password Reset",
8590
cachePrefix: '',
8691
oauthClientId: process.env.OAUTH_CLIENT_ID || "CMaBuwSnY0Vu68PLrWatvvu3iIiGPh7t",
8792
//auth0 secret is encoded in base64!

deploy/ci.sh

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
#!/bin/bash
22

33
#
4-
# Copyright (C) 2013 TopCoder Inc., All Rights Reserved.
4+
# Copyright (C) 2013-2014 TopCoder Inc., All Rights Reserved.
55
#
6-
# Version: 1.0
7-
# Author: vangavroche, delemach
6+
# Version: 1.1
7+
# Author: vangavroche, delemach, isv
8+
#
9+
# changes in 1.1:
10+
# - added RESET_PASSWORD_TOKEN_CACHE_EXPIRY environment variable
11+
# - added RESET_PASSWORD_TOKEN_EMAIL_SUBJECT environment variable
12+
# - added REDIS_HOST environment variable
13+
# - added REDIS_PORT environment variable
814
#
915
export CACHE_EXPIRY=-1
1016

@@ -71,3 +77,10 @@ export GRANT_FORUM_ACCESS=false
7177
export DEV_FORUM_JNDI=jnp://env.topcoder.com:1199
7278

7379
export ACTIONHERO_CONFIG=./config.js
80+
81+
## The period for expiring the generated tokens for password resetting
82+
export RESET_PASSWORD_TOKEN_CACHE_EXPIRY=1800000
83+
export RESET_PASSWORD_TOKEN_EMAIL_SUBJECT=TopCoder Account Password Reset
84+
85+
export REDIS_HOST=localhost
86+
export REDIS_PORT=6379

0 commit comments

Comments
 (0)