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

Generate Reset Token #179

Merged
merged 1 commit into from
Apr 7, 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
178 changes: 130 additions & 48 deletions actions/resetPassword.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,49 @@
/*
* Copyright (C) 2014 TopCoder Inc., All Rights Reserved.
*
* @version 1.0
* @author LazyChild
* @version 1.1
* @author LazyChild, isv
*
* changes in 1.1
* - implemented generateResetToken function
*/
"use strict";

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

var NotFoundError = require('../errors/NotFoundError');
var BadRequestError = require('../errors/BadRequestError');
var UnauthorizedError = require('../errors/UnauthorizedError');
var ForbiddenError = require('../errors/ForbiddenError');
var TOKEN_ALPHABET = stringUtils.ALPHABET_ALPHA_EN + stringUtils.ALPHABET_DIGITS_EN;

/**
* Looks up for the user account matching specified handle (either TopCoder handle or social login username) or email.
* If user account is not found then NotFoundError is returned to callback; otherwise ID for found user account is
* passed to callback.
*
* @param {String} handle - the handle to check.
* @param {String} email - the email to check.
* @param {Object} api - the action hero api object.
* @param {Object} dbConnectionMap - the database connection map.
* @param {Function<err, row>} callback - the callback function.
*/
var resolveUserByHandleOrEmail = function (handle, email, api, dbConnectionMap, callback) {
api.dataAccess.executeQuery("find_user_by_handle_or_email", { handle: handle, email: email }, dbConnectionMap,
function (err, result) {
if (err) {
callback(err);
return;
}
if (result && result[0]) {
callback(null, result[0]);
} else {
callback(new NotFoundError("User does not exist"));
}
});
};

/**
* This is the function that stub reset password
Expand All @@ -21,24 +55,21 @@ var ForbiddenError = require('../errors/ForbiddenError');
function resetPassword(api, connection, next) {
var result, helper = api.helper;
async.waterfall([
function(cb) {
if (connection.params.handle == "nonValid") {
function (cb) {
if (connection.params.handle === "nonValid") {
cb(new BadRequestError("The handle you entered is not valid"));
return;
} else if (connection.params.handle == "badLuck") {
} else if (connection.params.handle === "badLuck") {
cb(new Error("Unknown server error. Please contact support."));
return;
} else if (connection.params.token == "unauthorized_token") {
} else if (connection.params.token === "unauthorized_token") {
cb(new UnauthorizedError("Authentication credentials were missing or incorrect."));
return;
} else if (connection.params.token == "forbidden_token") {
} else if (connection.params.token === "forbidden_token") {
cb(new ForbiddenError("The request is understood, but it has been refused or access is not allowed."));
return;
} else {
result = {
"description": "Your password has been reset!"
};
cb();
}
result = {
"description": "Your password has been reset!"
};
cb();
}
], function (err) {
if (err) {
Expand All @@ -50,45 +81,55 @@ function resetPassword(api, connection, next) {
});
}


/**
* This is the function that stub reset token
* Generates the token for resetting the password for specified user account. First checks if non-expired token already
* exists for the user. If so then BadRequestError is passed to callback. Otherwise a new token is generated and saved
* to cache and returned to callback.
*
* @param {Number} userHandle - handle of user to generate token for.
* @param {String} userEmailAddress - email address of user to email generated token to.
* @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
* @param {Function<err>} callback - the callback function.
*/
function generateResetToken(api, connection, next) {
var result, helper = api.helper;
async.waterfall([
function(cb) {
if (connection.params.handle == "nonValid" || connection.params.email == "nonValid@test.com") {
cb(new BadRequestError("The handle you entered is not valid"));
return;
} else if (connection.params.handle == "badLuck" || connection.params.email == "badLuck@test.com") {
cb(new Error("Unknown server error. Please contact support."));
return;
}
var generateResetToken = function (userHandle, userEmailAddress, api, callback) {
var tokenCacheKey = 'tokens-' + userHandle + '-reset-token',
current,
expireDate,
expireDateString,
emailParams;

if (connection.params.handle == "googleSocial" || connection.params.email == "googleSocial@test.com") {
result = {
"socialLogin": "Google"
};
} else {
result = {
"token": "a3cbG"
};
}
cb();
}
], function (err) {
api.helper.getCachedValue(tokenCacheKey, function (err, token) {
if (err) {
helper.handleError(api, connection, err);
callback(err);
} else if (token) {
// Non-expired token already exists for this user - raise an error
callback(new BadRequestError("You have already requested the reset token, please find it in your email inbox."
+ " If it's not there. Please contact support@topcoder.com."));
} else {
connection.response = result;
// There is no token - generate new one
var newToken = stringUtils.generateRandomString(TOKEN_ALPHABET, 6),
lifetime = api.config.general.defaultResetPasswordTokenCacheLifetime;
api.cache.save(tokenCacheKey, newToken, lifetime);

// Send email with token to user
current = new Date();
expireDate = current.setSeconds(current.getSeconds() + lifetime / 1000);
expireDateString = moment(expireDate).tz('America/New_York').format('YYYY-MM-DD HH:mm:ss z');
emailParams = {
handle: userHandle,
token: newToken,
expiry: expireDateString,
template: 'reset_token_email',
subject: api.config.general.resetPasswordTokenEmailSubject,
toAddress: userEmailAddress
};
api.tasks.enqueue("sendEmail", emailParams, 'default');

callback(null, newToken);
}
next(connection, true);
});
}
};

/**
* Reset password API.
Expand All @@ -113,17 +154,58 @@ exports.resetPassword = {
* Generate reset token API.
*/
exports.generateResetToken = {
"name": "generateResetToken",
"description": "generateResetToken",
name: "generateResetToken",
description: "generateResetToken",
inputs: {
required: [],
optional: ["handle", "email"]
},
blockedConnectionTypes: [],
outputExample: {},
version: 'v2',
cacheEnabled: false,
transaction: 'read',
databases: ["common_oltp"],
run: function (api, connection, next) {
api.log("Execute generateResetToken#run", 'debug');
generateResetToken(api, connection, next);
if (connection.dbConnectionMap) {
async.waterfall([
function (cb) { // Find the user either by handle or by email
// Get handle, email from request parameters
var handle = (connection.params.handle || '').trim(),
email = (connection.params.email || '').trim(),
byHandle = (handle !== ''),
byEmail = (email !== '');

// Validate the input parameters, either handle or email but not both must be provided
if (byHandle && byEmail) {
cb(new BadRequestError("Both handle and email are specified"));
} else if (!byHandle && !byEmail) {
cb(new BadRequestError("Either handle or email must be specified"));
} else {
resolveUserByHandleOrEmail(handle, email, api, connection.dbConnectionMap, cb);
}
}, function (result, cb) {
if (result.social_login_provider_name !== '') {
// For social login accounts return the provider name
cb(null, null, result.social_login_provider_name);
} else {
// Generate reset password token for user
generateResetToken(result.handle, result.email_address, api, cb);
}
}
], function (err, newToken, socialProviderName) {
if (err) {
api.helper.handleError(api, connection, err);
} else if (newToken) {
connection.response = {successful: true};
} else if (socialProviderName) {
connection.response = {socialProvider: socialProviderName};
}
next(connection, true);
});
} else {
api.helper.handleNoConnection(api, connection, next);
}
}
};
35 changes: 30 additions & 5 deletions apiary.apib
Original file line number Diff line number Diff line change
Expand Up @@ -1220,17 +1220,17 @@ Register a new user.

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

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

+ Response 200 (application/json)

{
"token":"a3cbG"
"successful":"true"
}

+ Response 200 (application/json)
Expand All @@ -1244,7 +1244,31 @@ Register a new user.
{
"name":"Bad Request",
"value":"400",
"description":"The handle you entered is not valid"
"description":"Either handle or email must be specified"
}

+ Response 400 (application/json)

{
"name":"Bad Request",
"value":"400",
"description":"Both handle and email are specified"
}

+ Response 400 (application/json)

{
"name":"Bad Request",
"value":"400",
"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."
}

+ Response 404 (application/json)

{
"name":"Not Found",
"value":"404",
"description":"User does not exist"
}

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


## Reset Password [/users/resetPassword/{handle}]
### Reset Password [POST]
+ Parameters
Expand Down
26 changes: 23 additions & 3 deletions common/stringUtils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/*
* Copyright (C) 2013 TopCoder Inc., All Rights Reserved.
* Copyright (C) 2013-2014 TopCoder Inc., All Rights Reserved.
*
* Version: 1.0
* Author: TCSASSEMBLER
* Version: 1.1
* Author: isv
*
* changes in 1.1:
* - add generateRandomString function.
*/

"use strict";
Expand Down Expand Up @@ -39,6 +42,23 @@ exports.containsOnly = function (string, alphabet) {
return true;
};

/**
* Generates random string of specified length using the symbols from the specified alphabet.
*
* @param {String} alphabet - alphabet to use for string generation.
* @param {Number} length - the length for the string to be generated.
* @since 1.1
*/
exports.generateRandomString = function (alphabet, length) {
var text = '', i, index;
for (i = 0; i < length; i = i + 1) {
index = Math.random() * alphabet.length;
text += alphabet.charAt(index);
}

return text;
};

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;
Expand Down
7 changes: 6 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
* @version 1.17
* @version 1.19
* changes in 1.1:
* - add defaultCacheLifetime parameter
* changes in 1.2:
Expand Down Expand Up @@ -41,6 +41,9 @@
* - add welcome email property.
* Changes in 1.17:
* - add maxRSSLength.
* changes in 1.19:
* - add defaultResetPasswordTokenCacheLifetime property.
* - add resetPasswordTokenEmailSubject property.
*/
"use strict";

Expand Down Expand Up @@ -82,6 +85,8 @@ 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
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: '',
oauthClientId: process.env.OAUTH_CLIENT_ID || "CMaBuwSnY0Vu68PLrWatvvu3iIiGPh7t",
//auth0 secret is encoded in base64!
Expand Down
19 changes: 16 additions & 3 deletions deploy/ci.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
#!/bin/bash

#
# Copyright (C) 2013 TopCoder Inc., All Rights Reserved.
# Copyright (C) 2013-2014 TopCoder Inc., All Rights Reserved.
#
# Version: 1.0
# Author: vangavroche, delemach
# Version: 1.1
# Author: vangavroche, delemach, isv
#
# changes in 1.1:
# - added RESET_PASSWORD_TOKEN_CACHE_EXPIRY environment variable
# - added RESET_PASSWORD_TOKEN_EMAIL_SUBJECT environment variable
# - added REDIS_HOST environment variable
# - added REDIS_PORT environment variable
#
export CACHE_EXPIRY=-1

Expand Down Expand Up @@ -71,3 +77,10 @@ export GRANT_FORUM_ACCESS=false
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

export REDIS_HOST=localhost
export REDIS_PORT=6379
Loading