Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented email sending logic including forgot password, and email verification #187

Closed
Closed
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
1 change: 1 addition & 0 deletions Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function Config(applicationId, mount) {
this.fileKey = cacheInfo.fileKey;
this.facebookAppIds = cacheInfo.facebookAppIds;
this.mount = mount;
this.emailSender = cacheInfo.emailSender;
}


Expand Down
4 changes: 4 additions & 0 deletions Constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
RESET_PASSWORD: "RESET_PASSWORD",
VERIFY_EMAIL: "VERIFY_EMAIL"
};
35 changes: 25 additions & 10 deletions RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var passwordCrypto = require('./password');
var facebook = require('./facebook');
var Parse = require('parse/node');
var triggers = require('./triggers');
var VERIFY_EMAIL = require('./Constants').VERIFY_EMAIL;

// query and data are both provided in REST API format. So data
// types are encoded by plain old objects.
Expand Down Expand Up @@ -350,13 +351,16 @@ RestWrite.prototype.transformUser = function() {
email: this.data.email,
objectId: {'$ne': this.objectId()}
}, {limit: 1}).then((results) => {
if (results.length > 0) {
throw new Parse.Error(Parse.Error.EMAIL_TAKEN,
'Account already exists for this email ' +
'address');
}
return Promise.resolve();
});
if (results.length > 0) {
throw new Parse.Error(Parse.Error.EMAIL_TAKEN,
'Account already exists for this email ' +
'address');
}
this.data.emailVerified = false;
this.data.perishableToken = rack();
this.data.emailVerifyToken = rack();
return Promise.resolve();
});
});
};

Expand Down Expand Up @@ -654,16 +658,27 @@ RestWrite.prototype.runDatabaseOperation = function() {
}
}

function sendEmailVerification () {
if (typeof this.data.email !== 'undefined' && this.className === "_User" && this.config.emailSender) {
var link = this.config.mount + "/verify_email?token=" + encodeURIComponent(this.data.emailVerifyToken) + "&username=" + encodeURIComponent(this.data.email);
this.config.emailSender(VERIFY_EMAIL, link, this.data.email);
}
}

if (this.query) {
// Run an update
return this.config.database.update(
this.className, this.query, this.data, options).then((resp) => {
this.response = resp;
this.response.updatedAt = this.updatedAt;
});
sendEmailVerification.call(this);
this.response = resp;
this.response.updatedAt = this.updatedAt;
});
} else {
// Run a create
return this.config.database.create(this.className, this.data, options)
.then(()=> {
sendEmailVerification.call(this);
})
.then(() => {
var resp = {
objectId: this.data.objectId,
Expand Down
11 changes: 10 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var batch = require('./batch'),
multer = require('multer'),
Parse = require('parse/node').Parse,
PromiseRouter = require('./PromiseRouter'),
path = require('path');
httpRequest = require('./httpRequest');

// Mutate the Parse object to add the Cloud Code handlers
Expand Down Expand Up @@ -38,6 +39,7 @@ addParseCloud();
// "dotNetKey": optional key from Parse dashboard
// "restAPIKey": optional key from Parse dashboard
// "javascriptKey": optional key from Parse dashboard
// "emailSender": optional function to be called with the parameters required to send a password reset or confirmation email
function ParseServer(args) {
if (!args.appId || !args.masterKey) {
throw 'You must provide an appId and masterKey!';
Expand Down Expand Up @@ -72,7 +74,8 @@ function ParseServer(args) {
dotNetKey: args.dotNetKey || '',
restAPIKey: args.restAPIKey || '',
fileKey: args.fileKey || 'invalid-file-key',
facebookAppIds: args.facebookAppIds || []
facebookAppIds: args.facebookAppIds || [],
emailSender: args.emailSender
};

// To maintain compatibility. TODO: Remove in v2.1
Expand All @@ -92,6 +95,10 @@ function ParseServer(args) {

// File handling needs to be before default middlewares are applied
api.use('/', require('./files').router);
api.set('views', path.join(__dirname, 'views'));
api.use("/request_password_reset", require('./passwordReset').reset(args.appName, args.appId));
api.get("/password_reset_success", require('./passwordReset').success);
api.get("/verify_email", require('./verifyEmail')(args.appId));

// TODO: separate this from the regular ParseServer object
if (process.env.TESTING == 1) {
Expand All @@ -103,6 +110,7 @@ function ParseServer(args) {
api.use(middlewares.allowCrossDomain);
api.use(middlewares.allowMethodOverride);
api.use(middlewares.handleParseHeaders);
api.set('view engine', 'jade');

var router = new PromiseRouter();

Expand Down Expand Up @@ -165,5 +173,6 @@ function getClassName(parseClass) {

module.exports = {
ParseServer: ParseServer,
Constants: require('./Constants'),
S3Adapter: S3Adapter
};
96 changes: 96 additions & 0 deletions passwordReset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
var passwordCrypto = require('./password');
var rack = require('hat').rack();


function passwordReset (appName, appId) {
var DatabaseAdapter = require('./DatabaseAdapter');
var database = DatabaseAdapter.getDatabaseConnection(appId);

return function (req, res) {
var mount = req.protocol + '://' + req.get('host') + req.baseUrl;

Promise.resolve()
.then(()=> {
var error = null;
var password = req.body.password;
var passwordConfirm = req.body.passwordConfirm;
var username = req.body.username;
var token = req.body.token;
if (req.method !== 'POST') {
return Promise.resolve()
}
if (!password) {
error = "Password cannot be empty";
} else if (!passwordConfirm) {
error = "Password confirm cannot be empty";
} else if (password !== passwordConfirm) {
error = "Passwords do not match"
} else if (!username) {
error = "Username invalid: this is an invalid url";
} else if (!token) {
error = "Invalid token: this is an invalid url";
}
if (error) {
return Promise.resolve(error);
}

return database.find('_User', {username: username})
.then((results) => {
if (!results.length) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Invalid username');
}
var user = results[0];

if (user.perishableSessionToken !== token) {
return Promise.resolve("Invalid token: this is an invalid url")
} else {
return passwordCrypto.hash(password)
.then((hashedPassword)=> {
return database.update("_User", {email: username}, {_hashed_password: hashedPassword, _perishable_token: rack()}, {acl: [user.objectId]})
})
.then(()=> {
res.redirect(mount + '/password_reset_success?username=' + username);
return Promise.resolve(true)
})
}
})

})
.then((error)=> {
if (error === true) {
return;
}
var token = req.query.token;
var username = req.query.username;
if (req.body.token && req.body.username) {
token = req.body.token;
username = req.body.username;
}
var actionUrl = mount + '/request_password_reset?token=' + encodeURIComponent(token) + "&username=" + encodeURIComponent(username);
if (!token || !username) {
return res.status(404).render('not-found')
}
res.render('password-reset', {
name: appName,
token: req.query.token,
username: req.query.username,
action: actionUrl,
error: error
})
})
.catch(()=>{
res.status(404).render('not-found')
})
}
}

function success (req, res) {
return res.render("reset-success", {email: req.query.username});
}


module.exports = {
reset: passwordReset,
success: success
}
11 changes: 11 additions & 0 deletions transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ function transformKeyValue(schema, className, restKey, restValue, options) {
key = '_updated_at';
timeField = true;
break;
case 'perishableToken':
case '_perishable_token':
key = "_perishable_token";
break;
case 'emailVerifyToken':
case '_email_verify_token':
key = "_email_verify_token";
break;
case 'sessionToken':
case '_session_token':
key = '_session_token';
Expand Down Expand Up @@ -628,8 +636,11 @@ function untransformObject(schema, className, mongoObject) {
restObject['password'] = mongoObject[key];
break;
case '_acl':
break;
case '_email_verify_token':
restObject['emailVerifyToken'] = mongoObject[key];
case '_perishable_token':
restObject['perishableSessionToken'] = mongoObject[key];
break;
case '_session_token':
restObject['sessionToken'] = mongoObject[key];
Expand Down
34 changes: 30 additions & 4 deletions users.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ var facebook = require('./facebook');
var PromiseRouter = require('./PromiseRouter');
var rest = require('./rest');
var RestWrite = require('./RestWrite');
var Constants = require('./Constants');
var deepcopy = require('deepcopy');

var router = new PromiseRouter();
Expand Down Expand Up @@ -188,9 +189,34 @@ function handleUpdate(req) {
});
}

function notImplementedYet(req) {
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE,
'This path is not implemented yet.');
function handleReset(req) {
if (!req.body.email && req.query.email) {
req.body = req.query;
}

if (!req.body.email) {
throw new Parse.Error(Parse.Error.EMAIL_MISSING,
'email is required.');
}

return req.database.find('_User', {email: req.body.email})
.then((results) => {
if (!results.length) {
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND,
'Email not found.');
}
var emailSender = req.info.app && req.info.app.emailSender;
if (!emailSender) {
throw new Error("No email sender function specified");
}
var perishableSessionToken = encodeURIComponent(results[0].perishableSessionToken);
var encodedEmail = encodeURIComponent(req.body.email)
var endpoint = req.config.mount + "/request_password_reset?token=" + perishableSessionToken + "&username=" + encodedEmail;
return emailSender(Constants.RESET_PASSWORD, endpoint,req.body.email);
})
.then(()=>{
return {response:{}};
})
}

router.route('POST', '/users', handleCreate);
Expand All @@ -202,6 +228,6 @@ router.route('PUT', '/users/:objectId', handleUpdate);
router.route('GET', '/users', handleFind);
router.route('DELETE', '/users/:objectId', handleDelete);

router.route('POST', '/requestPasswordReset', notImplementedYet);
router.route('POST', '/requestPasswordReset', handleReset);

module.exports = router;
44 changes: 44 additions & 0 deletions verifyEmail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
function verifyEmail (appId) {
var DatabaseAdapter = require('./DatabaseAdapter');
var database = DatabaseAdapter.getDatabaseConnection(appId);
return function (req, res) {
var token = req.query.token;
var username = req.query.username;

Promise.resolve()
.then(()=>{
var error = null;
if (!token || !username) {
error = "Unable to verify email, check the URL and try again";
}
return Promise.resolve(error)
})
.then((error)=>{
if (error) {
return Promise.resolve(error);
}
return database.find('_User', {email: username})
.then((results)=>{
if (!results.length) {
return Promise.resolve("Could not find email " + username + " check the URL and try again");
}

var user = results[0];
return database.update("_User", {email: username}, {emailVerified: true}, {acl:[user.objectId]})
.then(()=>Promise.resolve())
})

})
.then((error)=>{
res.render('email-verified', {
email: username,
error: error
})
})
.catch(()=>{
res.status(404).render('not-found')
})
}
}

module.exports = verifyEmail;
8 changes: 8 additions & 0 deletions views/email-verified.jade
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
extends layout
block style
include styles.css
block content
if locals.error
h1.error #{error}
else
h1 Successfully verified #{email}!
Loading