Skip to content

Commit c354da3

Browse files
committed
feat: add beforePasswordResetRequest hook
1 parent f27b050 commit c354da3

File tree

4 files changed

+328
-47
lines changed

4 files changed

+328
-47
lines changed

spec/CloudCode.spec.js

Lines changed: 240 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3307,19 +3307,19 @@ describe('afterFind hooks', () => {
33073307
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
33083308
expect(() => {
33093309
Parse.Cloud.beforeLogin('SomeClass', () => { });
3310-
}).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
3310+
}).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
33113311
expect(() => {
33123312
Parse.Cloud.afterLogin(() => { });
3313-
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
3313+
}).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
33143314
expect(() => {
33153315
Parse.Cloud.afterLogin('_User', () => { });
3316-
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
3316+
}).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
33173317
expect(() => {
33183318
Parse.Cloud.afterLogin(Parse.User, () => { });
3319-
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
3319+
}).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
33203320
expect(() => {
33213321
Parse.Cloud.afterLogin('SomeClass', () => { });
3322-
}).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
3322+
}).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
33233323
expect(() => {
33243324
Parse.Cloud.afterLogout(() => { });
33253325
}).not.toThrow();
@@ -3777,60 +3777,258 @@ describe('beforeLogin hook', () => {
37773777
await Parse.User.logIn('tupac', 'shakur');
37783778
done();
37793779
});
3780+
});
3781+
3782+
describe('beforePasswordResetRequest hook', () => {
3783+
it('should run beforePasswordResetRequest with valid user', async done => {
3784+
let hit = 0;
3785+
let sendPasswordResetEmailCalled = false;
3786+
const emailAdapter = {
3787+
sendVerificationEmail: () => Promise.resolve(),
3788+
sendPasswordResetEmail: () => {
3789+
sendPasswordResetEmailCalled = true;
3790+
},
3791+
sendMail: () => {},
3792+
};
37803793

3781-
it('afterFind should not be triggered when saving an object', async () => {
3782-
let beforeSaves = 0;
3783-
Parse.Cloud.beforeSave('SavingTest', () => {
3784-
beforeSaves++;
3794+
await reconfigureServer({
3795+
appName: 'test',
3796+
emailAdapter: emailAdapter,
3797+
publicServerURL: 'http://localhost:8378/1',
37853798
});
37863799

3787-
let afterSaves = 0;
3788-
Parse.Cloud.afterSave('SavingTest', () => {
3789-
afterSaves++;
3800+
Parse.Cloud.beforePasswordResetRequest(req => {
3801+
hit++;
3802+
expect(req.object).toBeDefined();
3803+
expect(req.object.get('email')).toEqual('[email protected]');
3804+
expect(req.object.get('username')).toEqual('testuser');
37903805
});
37913806

3792-
let beforeFinds = 0;
3793-
Parse.Cloud.beforeFind('SavingTest', () => {
3794-
beforeFinds++;
3807+
const user = new Parse.User();
3808+
user.setUsername('testuser');
3809+
user.setPassword('password');
3810+
user.set('email', '[email protected]');
3811+
await user.signUp();
3812+
3813+
await Parse.User.requestPasswordReset('[email protected]');
3814+
expect(hit).toBe(1);
3815+
expect(sendPasswordResetEmailCalled).toBe(true);
3816+
done();
3817+
});
3818+
3819+
it('should be able to block password reset request if an error is thrown', async done => {
3820+
let hit = 0;
3821+
let sendPasswordResetEmailCalled = false;
3822+
const emailAdapter = {
3823+
sendVerificationEmail: () => Promise.resolve(),
3824+
sendPasswordResetEmail: () => {
3825+
sendPasswordResetEmailCalled = true;
3826+
},
3827+
sendMail: () => {},
3828+
};
3829+
3830+
await reconfigureServer({
3831+
appName: 'test',
3832+
emailAdapter: emailAdapter,
3833+
publicServerURL: 'http://localhost:8378/1',
37953834
});
37963835

3797-
let afterFinds = 0;
3798-
Parse.Cloud.afterFind('SavingTest', () => {
3799-
afterFinds++;
3836+
Parse.Cloud.beforePasswordResetRequest(req => {
3837+
hit++;
3838+
if (req.object.get('isBanned')) {
3839+
throw new Error('banned account');
3840+
}
38003841
});
38013842

3802-
const obj = new Parse.Object('SavingTest');
3803-
obj.set('someField', 'some value 1');
3804-
await obj.save();
3843+
const user = new Parse.User();
3844+
user.setUsername('banneduser');
3845+
user.setPassword('password');
3846+
user.set('email', '[email protected]');
3847+
await user.signUp();
3848+
await user.save({ isBanned: true });
38053849

3806-
expect(beforeSaves).toEqual(1);
3807-
expect(afterSaves).toEqual(1);
3808-
expect(beforeFinds).toEqual(0);
3809-
expect(afterFinds).toEqual(0);
3850+
try {
3851+
await Parse.User.requestPasswordReset('[email protected]');
3852+
throw new Error('should not have sent password reset email.');
3853+
} catch (e) {
3854+
expect(e.message).toBe('banned account');
3855+
}
3856+
expect(hit).toBe(1);
3857+
expect(sendPasswordResetEmailCalled).toBe(false);
3858+
done();
3859+
});
38103860

3811-
obj.set('someField', 'some value 2');
3812-
await obj.save();
3861+
it('should be able to block password reset request if an error is thrown even if the user has an attached file', async done => {
3862+
let hit = 0;
3863+
let sendPasswordResetEmailCalled = false;
3864+
const emailAdapter = {
3865+
sendVerificationEmail: () => Promise.resolve(),
3866+
sendPasswordResetEmail: () => {
3867+
sendPasswordResetEmailCalled = true;
3868+
},
3869+
sendMail: () => {},
3870+
};
38133871

3814-
expect(beforeSaves).toEqual(2);
3815-
expect(afterSaves).toEqual(2);
3816-
expect(beforeFinds).toEqual(0);
3817-
expect(afterFinds).toEqual(0);
3872+
await reconfigureServer({
3873+
appName: 'test',
3874+
emailAdapter: emailAdapter,
3875+
publicServerURL: 'http://localhost:8378/1',
3876+
});
38183877

3819-
await obj.fetch();
3878+
Parse.Cloud.beforePasswordResetRequest(req => {
3879+
hit++;
3880+
if (req.object.get('isBanned')) {
3881+
throw new Error('banned account');
3882+
}
3883+
});
38203884

3821-
expect(beforeSaves).toEqual(2);
3822-
expect(afterSaves).toEqual(2);
3823-
expect(beforeFinds).toEqual(1);
3824-
expect(afterFinds).toEqual(1);
3885+
const user = new Parse.User();
3886+
user.setUsername('banneduser2');
3887+
user.setPassword('password');
3888+
user.set('email', '[email protected]');
3889+
await user.signUp();
3890+
const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=';
3891+
const file = new Parse.File('myfile.txt', { base64 });
3892+
await file.save();
3893+
await user.save({ isBanned: true, file });
38253894

3826-
obj.set('someField', 'some value 3');
3827-
await obj.save();
3895+
try {
3896+
await Parse.User.requestPasswordReset('[email protected]');
3897+
throw new Error('should not have sent password reset email.');
3898+
} catch (e) {
3899+
expect(e.message).toBe('banned account');
3900+
}
3901+
expect(hit).toBe(1);
3902+
expect(sendPasswordResetEmailCalled).toBe(false);
3903+
done();
3904+
});
3905+
3906+
it('should not run beforePasswordResetRequest if email does not exist', async done => {
3907+
let hit = 0;
3908+
const emailAdapter = {
3909+
sendVerificationEmail: () => Promise.resolve(),
3910+
sendPasswordResetEmail: () => {},
3911+
sendMail: () => {},
3912+
};
3913+
3914+
await reconfigureServer({
3915+
emailAdapter: emailAdapter,
3916+
publicServerURL: 'http://localhost:8378/1',
3917+
});
3918+
3919+
Parse.Cloud.beforePasswordResetRequest(req => {
3920+
hit++;
3921+
});
3922+
3923+
try {
3924+
await Parse.User.requestPasswordReset('[email protected]');
3925+
} catch (e) {
3926+
// May or may not throw depending on passwordPolicy.resetPasswordSuccessOnInvalidEmail
3927+
}
3928+
expect(hit).toBe(0);
3929+
done();
3930+
});
3931+
3932+
it('should have expected data in request in beforePasswordResetRequest', async done => {
3933+
const emailAdapter = {
3934+
sendVerificationEmail: () => Promise.resolve(),
3935+
sendPasswordResetEmail: () => {},
3936+
sendMail: () => {},
3937+
};
3938+
3939+
await reconfigureServer({
3940+
appName: 'test',
3941+
emailAdapter: emailAdapter,
3942+
publicServerURL: 'http://localhost:8378/1',
3943+
});
3944+
3945+
Parse.Cloud.beforePasswordResetRequest(req => {
3946+
expect(req.object).toBeDefined();
3947+
expect(req.object.get('email')).toBeDefined();
3948+
expect(req.headers).toBeDefined();
3949+
expect(req.ip).toBeDefined();
3950+
expect(req.installationId).toBeDefined();
3951+
expect(req.context).toBeDefined();
3952+
expect(req.config).toBeDefined();
3953+
});
3954+
3955+
const user = new Parse.User();
3956+
user.setUsername('testuser2');
3957+
user.setPassword('password');
3958+
user.set('email', '[email protected]');
3959+
await user.signUp();
3960+
await Parse.User.requestPasswordReset('[email protected]');
3961+
done();
3962+
});
3963+
3964+
it('should validate that only _User class is allowed for beforePasswordResetRequest', () => {
3965+
expect(() => {
3966+
Parse.Cloud.beforePasswordResetRequest('SomeClass', () => { });
3967+
}).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
3968+
expect(() => {
3969+
Parse.Cloud.beforePasswordResetRequest(() => { });
3970+
}).not.toThrow();
3971+
expect(() => {
3972+
Parse.Cloud.beforePasswordResetRequest('_User', () => { });
3973+
}).not.toThrow();
3974+
expect(() => {
3975+
Parse.Cloud.beforePasswordResetRequest(Parse.User, () => { });
3976+
}).not.toThrow();
3977+
});
3978+
});
3979+
3980+
it('afterFind should not be triggered when saving an object', async () => {
3981+
let beforeSaves = 0;
3982+
Parse.Cloud.beforeSave('SavingTest', () => {
3983+
beforeSaves++;
3984+
});
3985+
3986+
let afterSaves = 0;
3987+
Parse.Cloud.afterSave('SavingTest', () => {
3988+
afterSaves++;
3989+
});
3990+
3991+
let beforeFinds = 0;
3992+
Parse.Cloud.beforeFind('SavingTest', () => {
3993+
beforeFinds++;
3994+
});
38283995

3829-
expect(beforeSaves).toEqual(3);
3830-
expect(afterSaves).toEqual(3);
3831-
expect(beforeFinds).toEqual(1);
3832-
expect(afterFinds).toEqual(1);
3996+
let afterFinds = 0;
3997+
Parse.Cloud.afterFind('SavingTest', () => {
3998+
afterFinds++;
38333999
});
4000+
4001+
const obj = new Parse.Object('SavingTest');
4002+
obj.set('someField', 'some value 1');
4003+
await obj.save();
4004+
4005+
expect(beforeSaves).toEqual(1);
4006+
expect(afterSaves).toEqual(1);
4007+
expect(beforeFinds).toEqual(0);
4008+
expect(afterFinds).toEqual(0);
4009+
4010+
obj.set('someField', 'some value 2');
4011+
await obj.save();
4012+
4013+
expect(beforeSaves).toEqual(2);
4014+
expect(afterSaves).toEqual(2);
4015+
expect(beforeFinds).toEqual(0);
4016+
expect(afterFinds).toEqual(0);
4017+
4018+
await obj.fetch();
4019+
4020+
expect(beforeSaves).toEqual(2);
4021+
expect(afterSaves).toEqual(2);
4022+
expect(beforeFinds).toEqual(1);
4023+
expect(afterFinds).toEqual(1);
4024+
4025+
obj.set('someField', 'some value 3');
4026+
await obj.save();
4027+
4028+
expect(beforeSaves).toEqual(3);
4029+
expect(afterSaves).toEqual(3);
4030+
expect(beforeFinds).toEqual(1);
4031+
expect(afterFinds).toEqual(1);
38344032
});
38354033

38364034
describe('afterLogin hook', () => {

src/Routers/UsersRouter.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Types as TriggerTypes,
1313
getRequestObject,
1414
resolveError,
15+
inflate,
1516
} from '../triggers';
1617
import { promiseEnsureIdempotency } from '../middlewares';
1718
import RestWrite from '../RestWrite';
@@ -444,21 +445,58 @@ export class UsersRouter extends ClassesRouter {
444445
if (!email && !token) {
445446
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
446447
}
448+
449+
let userResults = null;
450+
let userData = null;
451+
452+
// We can find the user using token
447453
if (token) {
448-
const results = await req.config.database.find('_User', {
454+
userResults = await req.config.database.find('_User', {
449455
_perishable_token: token,
450456
_perishable_token_expires_at: { $lt: Parse._encode(new Date()) },
451457
});
452-
if (results && results[0] && results[0].email) {
453-
email = results[0].email;
458+
if (userResults && userResults.length > 0) {
459+
userData = userResults[0];
460+
if (userData.email) {
461+
email = userData.email;
462+
}
463+
}
464+
// Or using email if no token provided
465+
} else if (typeof email === 'string') {
466+
userResults = await req.config.database.find(
467+
'_User',
468+
{ $or: [{ email }, { username: email, email: { $exists: false } }] },
469+
{ limit: 1 },
470+
Auth.maintenance(req.config)
471+
);
472+
if (userResults && userResults.length > 0) {
473+
userData = userResults[0];
454474
}
455475
}
476+
456477
if (typeof email !== 'string') {
457478
throw new Parse.Error(
458479
Parse.Error.INVALID_EMAIL_ADDRESS,
459480
'you must provide a valid email string'
460481
);
461482
}
483+
484+
if (userData) {
485+
// Useful to get User attached files in the trigger (photo picture for example)
486+
await req.config.filesController.expandFilesInObject(req.config, userData);
487+
488+
const user = inflate('_User', userData);
489+
490+
await maybeRunTrigger(
491+
TriggerTypes.beforePasswordResetRequest,
492+
req.auth,
493+
user,
494+
null,
495+
req.config,
496+
req.info.context
497+
);
498+
}
499+
462500
const userController = req.config.userController;
463501
try {
464502
await userController.sendPasswordResetEmail(email);

0 commit comments

Comments
 (0)