Skip to content

Commit 29fec01

Browse files
xyziembaArthur Cinader
authored and
Arthur Cinader
committed
Resend Verification Email Endpoint (#3543)
* Endpoint to Handle Verification Email Request * Adds tests for verificationEmailRequest endpoint * Better error responses for `/verificationEmailRequest`
1 parent 92b6999 commit 29fec01

File tree

3 files changed

+291
-6
lines changed

3 files changed

+291
-6
lines changed

spec/EmailVerificationToken.spec.js

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use strict";
22

33
const request = require('request');
4+
const requestp = require('request-promise');
45
const Config = require('../src/Config');
56

67
describe("Email Verification Token Expiration: ", () => {
@@ -482,6 +483,257 @@ describe("Email Verification Token Expiration: ", () => {
482483
});
483484
});
484485

486+
it('should send a new verification email when a resend is requested and the user is UNVERIFIED', done => {
487+
var user = new Parse.User();
488+
var sendEmailOptions;
489+
var sendVerificationEmailCallCount = 0;
490+
var emailAdapter = {
491+
sendVerificationEmail: options => {
492+
sendEmailOptions = options;
493+
sendVerificationEmailCallCount++;
494+
},
495+
sendPasswordResetEmail: () => Promise.resolve(),
496+
sendMail: () => {}
497+
}
498+
reconfigureServer({
499+
appName: 'emailVerifyToken',
500+
verifyUserEmails: true,
501+
emailAdapter: emailAdapter,
502+
emailVerifyTokenValidityDuration: 5, // 5 seconds
503+
publicServerURL: 'http://localhost:8378/1'
504+
})
505+
.then(() => {
506+
user.setUsername('resends_verification_token');
507+
user.setPassword('expiringToken');
508+
user.set('email', '[email protected]');
509+
return user.signUp();
510+
})
511+
.then(() => {
512+
expect(sendVerificationEmailCallCount).toBe(1);
513+
514+
return requestp.post({
515+
uri: 'http://localhost:8378/1/verificationEmailRequest',
516+
body: {
517+
518+
},
519+
headers: {
520+
'X-Parse-Application-Id': Parse.applicationId,
521+
'X-Parse-REST-API-Key': 'rest',
522+
},
523+
json: true,
524+
resolveWithFullResponse: true,
525+
simple: false // this promise is only rejected if the call itself failed
526+
})
527+
.then((response) => {
528+
expect(response.statusCode).toBe(200);
529+
expect(sendVerificationEmailCallCount).toBe(2);
530+
expect(sendEmailOptions).toBeDefined();
531+
done();
532+
});
533+
})
534+
.catch(error => {
535+
jfail(error);
536+
done();
537+
});
538+
});
539+
540+
it('should not send a new verification email when a resend is requested and the user is VERIFIED', done => {
541+
var user = new Parse.User();
542+
var sendEmailOptions;
543+
var sendVerificationEmailCallCount = 0;
544+
var emailAdapter = {
545+
sendVerificationEmail: options => {
546+
sendEmailOptions = options;
547+
sendVerificationEmailCallCount++;
548+
},
549+
sendPasswordResetEmail: () => Promise.resolve(),
550+
sendMail: () => {}
551+
}
552+
reconfigureServer({
553+
appName: 'emailVerifyToken',
554+
verifyUserEmails: true,
555+
emailAdapter: emailAdapter,
556+
emailVerifyTokenValidityDuration: 5, // 5 seconds
557+
publicServerURL: 'http://localhost:8378/1'
558+
})
559+
.then(() => {
560+
user.setUsername('no_new_verification_token_once_verified');
561+
user.setPassword('expiringToken');
562+
user.set('email', '[email protected]');
563+
return user.signUp();
564+
})
565+
.then(() => {
566+
return requestp.get({
567+
url: sendEmailOptions.link,
568+
followRedirect: false,
569+
resolveWithFullResponse: true,
570+
simple: false
571+
})
572+
.then((response) => {
573+
expect(response.statusCode).toEqual(302);
574+
});
575+
})
576+
.then(() => {
577+
expect(sendVerificationEmailCallCount).toBe(1);
578+
579+
return requestp.post({
580+
uri: 'http://localhost:8378/1/verificationEmailRequest',
581+
body: {
582+
583+
},
584+
headers: {
585+
'X-Parse-Application-Id': Parse.applicationId,
586+
'X-Parse-REST-API-Key': 'rest',
587+
},
588+
json: true,
589+
resolveWithFullResponse: true,
590+
simple: false // this promise is only rejected if the call itself failed
591+
})
592+
.then((response) => {
593+
expect(response.statusCode).toBe(400);
594+
expect(sendVerificationEmailCallCount).toBe(1);
595+
done();
596+
});
597+
})
598+
.catch(error => {
599+
jfail(error);
600+
done();
601+
});
602+
});
603+
604+
it('should not send a new verification email if this user does not exist', done => {
605+
var sendEmailOptions;
606+
var sendVerificationEmailCallCount = 0;
607+
var emailAdapter = {
608+
sendVerificationEmail: options => {
609+
sendEmailOptions = options;
610+
sendVerificationEmailCallCount++;
611+
},
612+
sendPasswordResetEmail: () => Promise.resolve(),
613+
sendMail: () => {}
614+
}
615+
reconfigureServer({
616+
appName: 'emailVerifyToken',
617+
verifyUserEmails: true,
618+
emailAdapter: emailAdapter,
619+
emailVerifyTokenValidityDuration: 5, // 5 seconds
620+
publicServerURL: 'http://localhost:8378/1'
621+
})
622+
.then(() => {
623+
return requestp.post({
624+
uri: 'http://localhost:8378/1/verificationEmailRequest',
625+
body: {
626+
627+
},
628+
headers: {
629+
'X-Parse-Application-Id': Parse.applicationId,
630+
'X-Parse-REST-API-Key': 'rest',
631+
},
632+
json: true,
633+
resolveWithFullResponse: true,
634+
simple: false
635+
})
636+
.then(response => {
637+
expect(response.statusCode).toBe(400);
638+
expect(sendVerificationEmailCallCount).toBe(0);
639+
expect(sendEmailOptions).not.toBeDefined();
640+
done();
641+
});
642+
})
643+
.catch(error => {
644+
jfail(error);
645+
done();
646+
});
647+
});
648+
649+
it('should fail if no email is supplied', done => {
650+
var sendEmailOptions;
651+
var sendVerificationEmailCallCount = 0;
652+
var emailAdapter = {
653+
sendVerificationEmail: options => {
654+
sendEmailOptions = options;
655+
sendVerificationEmailCallCount++;
656+
},
657+
sendPasswordResetEmail: () => Promise.resolve(),
658+
sendMail: () => {}
659+
}
660+
reconfigureServer({
661+
appName: 'emailVerifyToken',
662+
verifyUserEmails: true,
663+
emailAdapter: emailAdapter,
664+
emailVerifyTokenValidityDuration: 5, // 5 seconds
665+
publicServerURL: 'http://localhost:8378/1'
666+
})
667+
.then(() => {
668+
request.post({
669+
uri: 'http://localhost:8378/1/verificationEmailRequest',
670+
body: {},
671+
headers: {
672+
'X-Parse-Application-Id': Parse.applicationId,
673+
'X-Parse-REST-API-Key': 'rest',
674+
},
675+
json: true,
676+
resolveWithFullResponse: true,
677+
simple: false
678+
}, (err, response) => {
679+
expect(response.statusCode).toBe(400);
680+
expect(response.body.code).toBe(Parse.Error.EMAIL_MISSING);
681+
expect(response.body.error).toBe('you must provide an email');
682+
expect(sendVerificationEmailCallCount).toBe(0);
683+
expect(sendEmailOptions).not.toBeDefined();
684+
done();
685+
});
686+
})
687+
.catch(error => {
688+
jfail(error);
689+
done();
690+
});
691+
});
692+
693+
it('should fail if email is not a string', done => {
694+
var sendEmailOptions;
695+
var sendVerificationEmailCallCount = 0;
696+
var emailAdapter = {
697+
sendVerificationEmail: options => {
698+
sendEmailOptions = options;
699+
sendVerificationEmailCallCount++;
700+
},
701+
sendPasswordResetEmail: () => Promise.resolve(),
702+
sendMail: () => {}
703+
}
704+
reconfigureServer({
705+
appName: 'emailVerifyToken',
706+
verifyUserEmails: true,
707+
emailAdapter: emailAdapter,
708+
emailVerifyTokenValidityDuration: 5, // 5 seconds
709+
publicServerURL: 'http://localhost:8378/1'
710+
})
711+
.then(() => {
712+
request.post({
713+
uri: 'http://localhost:8378/1/verificationEmailRequest',
714+
body: {email: 3},
715+
headers: {
716+
'X-Parse-Application-Id': Parse.applicationId,
717+
'X-Parse-REST-API-Key': 'rest',
718+
},
719+
json: true,
720+
resolveWithFullResponse: true,
721+
simple: false
722+
}, (err, response) => {
723+
expect(response.statusCode).toBe(400);
724+
expect(response.body.code).toBe(Parse.Error.INVALID_EMAIL_ADDRESS);
725+
expect(response.body.error).toBe('you must provide a valid email string');
726+
expect(sendVerificationEmailCallCount).toBe(0);
727+
expect(sendEmailOptions).not.toBeDefined();
728+
done();
729+
});
730+
})
731+
.catch(error => {
732+
jfail(error);
733+
done();
734+
});
735+
});
736+
485737
it('client should not see the _email_verify_token_expires_at field', done => {
486738
var user = new Parse.User();
487739
var sendEmailOptions;

spec/ValidationAndPasswordsReset.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
383383
fail('sending password reset email should not have succeeded');
384384
done();
385385
}, error => {
386-
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.')
386+
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.')
387387
done();
388388
});
389389
})
@@ -414,7 +414,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
414414
fail('sending password reset email should not have succeeded');
415415
done();
416416
}, error => {
417-
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.')
417+
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.')
418418
done();
419419
});
420420
})
@@ -442,7 +442,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
442442
fail('sending password reset email should not have succeeded');
443443
done();
444444
}, error => {
445-
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.')
445+
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.')
446446
done();
447447
});
448448
})

src/Routers/UsersRouter.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export class UsersRouter extends ClassesRouter {
191191
return Promise.resolve(success);
192192
}
193193

194-
handleResetRequest(req) {
194+
_throwOnBadEmailConfig(req) {
195195
try {
196196
Config.validateEmailConfiguration({
197197
emailAdapter: req.config.userController.adapter,
@@ -202,11 +202,16 @@ export class UsersRouter extends ClassesRouter {
202202
} catch (e) {
203203
if (typeof e === 'string') {
204204
// Maybe we need a Bad Configuration error, but the SDKs won't understand it. For now, Internal Server Error.
205-
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'An appName, publicServerURL, and emailAdapter are required for password reset functionality.');
205+
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.');
206206
} else {
207207
throw e;
208208
}
209209
}
210+
}
211+
212+
handleResetRequest(req) {
213+
this._throwOnBadEmailConfig(req);
214+
210215
const { email } = req.body;
211216
if (!email) {
212217
throw new Parse.Error(Parse.Error.EMAIL_MISSING, "you must provide an email");
@@ -228,6 +233,33 @@ export class UsersRouter extends ClassesRouter {
228233
});
229234
}
230235

236+
handleVerificationEmailRequest(req) {
237+
this._throwOnBadEmailConfig(req);
238+
239+
const { email } = req.body;
240+
if (!email) {
241+
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
242+
}
243+
if (typeof email !== 'string') {
244+
throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string');
245+
}
246+
247+
return req.config.database.find('_User', { email: email }).then((results) => {
248+
if (!results.length || results.length < 1) {
249+
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`);
250+
}
251+
const user = results[0];
252+
253+
if (user.emailVerified) {
254+
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `Email ${email} is already verified.`);
255+
}
256+
257+
const userController = req.config.userController;
258+
userController.sendVerificationEmail(user);
259+
return { response: {} };
260+
});
261+
}
262+
231263

232264
mountRoutes() {
233265
this.route('GET', '/users', req => { return this.handleFind(req); });
@@ -238,7 +270,8 @@ export class UsersRouter extends ClassesRouter {
238270
this.route('DELETE', '/users/:objectId', req => { return this.handleDelete(req); });
239271
this.route('GET', '/login', req => { return this.handleLogIn(req); });
240272
this.route('POST', '/logout', req => { return this.handleLogOut(req); });
241-
this.route('POST', '/requestPasswordReset', req => { return this.handleResetRequest(req); })
273+
this.route('POST', '/requestPasswordReset', req => { return this.handleResetRequest(req); });
274+
this.route('POST', '/verificationEmailRequest', req => { return this.handleVerificationEmailRequest(req); });
242275
}
243276
}
244277

0 commit comments

Comments
 (0)