Resend Verification Email Endpoint (#3543)
* Endpoint to Handle Verification Email Request * Adds tests for verificationEmailRequest endpoint * Better error responses for `/verificationEmailRequest`
This commit is contained in:
committed by
Arthur Cinader
parent
92b699920e
commit
29fec01a42
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const request = require('request');
|
||||
const requestp = require('request-promise');
|
||||
const Config = require('../src/Config');
|
||||
|
||||
describe("Email Verification Token Expiration: ", () => {
|
||||
@@ -482,6 +483,257 @@ describe("Email Verification Token Expiration: ", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should send a new verification email when a resend is requested and the user is UNVERIFIED', done => {
|
||||
var user = new Parse.User();
|
||||
var sendEmailOptions;
|
||||
var sendVerificationEmailCallCount = 0;
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
sendVerificationEmailCallCount++;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})
|
||||
.then(() => {
|
||||
user.setUsername('resends_verification_token');
|
||||
user.setPassword('expiringToken');
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
expect(sendVerificationEmailCallCount).toBe(1);
|
||||
|
||||
return requestp.post({
|
||||
uri: 'http://localhost:8378/1/verificationEmailRequest',
|
||||
body: {
|
||||
email: 'user@parse.com'
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
simple: false // this promise is only rejected if the call itself failed
|
||||
})
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(sendVerificationEmailCallCount).toBe(2);
|
||||
expect(sendEmailOptions).toBeDefined();
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
jfail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not send a new verification email when a resend is requested and the user is VERIFIED', done => {
|
||||
var user = new Parse.User();
|
||||
var sendEmailOptions;
|
||||
var sendVerificationEmailCallCount = 0;
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
sendVerificationEmailCallCount++;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})
|
||||
.then(() => {
|
||||
user.setUsername('no_new_verification_token_once_verified');
|
||||
user.setPassword('expiringToken');
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
return requestp.get({
|
||||
url: sendEmailOptions.link,
|
||||
followRedirect: false,
|
||||
resolveWithFullResponse: true,
|
||||
simple: false
|
||||
})
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
expect(sendVerificationEmailCallCount).toBe(1);
|
||||
|
||||
return requestp.post({
|
||||
uri: 'http://localhost:8378/1/verificationEmailRequest',
|
||||
body: {
|
||||
email: 'user@parse.com'
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
simple: false // this promise is only rejected if the call itself failed
|
||||
})
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(sendVerificationEmailCallCount).toBe(1);
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
jfail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not send a new verification email if this user does not exist', done => {
|
||||
var sendEmailOptions;
|
||||
var sendVerificationEmailCallCount = 0;
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
sendVerificationEmailCallCount++;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})
|
||||
.then(() => {
|
||||
return requestp.post({
|
||||
uri: 'http://localhost:8378/1/verificationEmailRequest',
|
||||
body: {
|
||||
email: 'user@parse.com'
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
simple: false
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(sendVerificationEmailCallCount).toBe(0);
|
||||
expect(sendEmailOptions).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
jfail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if no email is supplied', done => {
|
||||
var sendEmailOptions;
|
||||
var sendVerificationEmailCallCount = 0;
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
sendVerificationEmailCallCount++;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})
|
||||
.then(() => {
|
||||
request.post({
|
||||
uri: 'http://localhost:8378/1/verificationEmailRequest',
|
||||
body: {},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
simple: false
|
||||
}, (err, response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body.code).toBe(Parse.Error.EMAIL_MISSING);
|
||||
expect(response.body.error).toBe('you must provide an email');
|
||||
expect(sendVerificationEmailCallCount).toBe(0);
|
||||
expect(sendEmailOptions).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
jfail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if email is not a string', done => {
|
||||
var sendEmailOptions;
|
||||
var sendVerificationEmailCallCount = 0;
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
sendVerificationEmailCallCount++;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})
|
||||
.then(() => {
|
||||
request.post({
|
||||
uri: 'http://localhost:8378/1/verificationEmailRequest',
|
||||
body: {email: 3},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
simple: false
|
||||
}, (err, response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body.code).toBe(Parse.Error.INVALID_EMAIL_ADDRESS);
|
||||
expect(response.body.error).toBe('you must provide a valid email string');
|
||||
expect(sendVerificationEmailCallCount).toBe(0);
|
||||
expect(sendEmailOptions).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
jfail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('client should not see the _email_verify_token_expires_at field', done => {
|
||||
var user = new Parse.User();
|
||||
var sendEmailOptions;
|
||||
|
||||
@@ -383,7 +383,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
|
||||
fail('sending password reset email should not have succeeded');
|
||||
done();
|
||||
}, error => {
|
||||
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.')
|
||||
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.')
|
||||
done();
|
||||
});
|
||||
})
|
||||
@@ -414,7 +414,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
|
||||
fail('sending password reset email should not have succeeded');
|
||||
done();
|
||||
}, error => {
|
||||
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.')
|
||||
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.')
|
||||
done();
|
||||
});
|
||||
})
|
||||
@@ -442,7 +442,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
|
||||
fail('sending password reset email should not have succeeded');
|
||||
done();
|
||||
}, error => {
|
||||
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.')
|
||||
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.')
|
||||
done();
|
||||
});
|
||||
})
|
||||
|
||||
@@ -191,7 +191,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
return Promise.resolve(success);
|
||||
}
|
||||
|
||||
handleResetRequest(req) {
|
||||
_throwOnBadEmailConfig(req) {
|
||||
try {
|
||||
Config.validateEmailConfiguration({
|
||||
emailAdapter: req.config.userController.adapter,
|
||||
@@ -202,11 +202,16 @@ export class UsersRouter extends ClassesRouter {
|
||||
} catch (e) {
|
||||
if (typeof e === 'string') {
|
||||
// Maybe we need a Bad Configuration error, but the SDKs won't understand it. For now, Internal Server Error.
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'An appName, publicServerURL, and emailAdapter are required for password reset functionality.');
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.');
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleResetRequest(req) {
|
||||
this._throwOnBadEmailConfig(req);
|
||||
|
||||
const { email } = req.body;
|
||||
if (!email) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_MISSING, "you must provide an email");
|
||||
@@ -228,6 +233,33 @@ export class UsersRouter extends ClassesRouter {
|
||||
});
|
||||
}
|
||||
|
||||
handleVerificationEmailRequest(req) {
|
||||
this._throwOnBadEmailConfig(req);
|
||||
|
||||
const { email } = req.body;
|
||||
if (!email) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
|
||||
}
|
||||
if (typeof email !== 'string') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string');
|
||||
}
|
||||
|
||||
return req.config.database.find('_User', { email: email }).then((results) => {
|
||||
if (!results.length || results.length < 1) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`);
|
||||
}
|
||||
const user = results[0];
|
||||
|
||||
if (user.emailVerified) {
|
||||
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `Email ${email} is already verified.`);
|
||||
}
|
||||
|
||||
const userController = req.config.userController;
|
||||
userController.sendVerificationEmail(user);
|
||||
return { response: {} };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/users', req => { return this.handleFind(req); });
|
||||
@@ -238,7 +270,8 @@ export class UsersRouter extends ClassesRouter {
|
||||
this.route('DELETE', '/users/:objectId', req => { return this.handleDelete(req); });
|
||||
this.route('GET', '/login', req => { return this.handleLogIn(req); });
|
||||
this.route('POST', '/logout', req => { return this.handleLogOut(req); });
|
||||
this.route('POST', '/requestPasswordReset', req => { return this.handleResetRequest(req); })
|
||||
this.route('POST', '/requestPasswordReset', req => { return this.handleResetRequest(req); });
|
||||
this.route('POST', '/verificationEmailRequest', req => { return this.handleVerificationEmailRequest(req); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user