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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy