Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c354da3
feat: add beforePasswordResetRequest hook
coratgerl Nov 6, 2025
e30572d
fix: feedbacks
coratgerl Nov 7, 2025
2971acf
Merge branch 'alpha' into before-password-reset-request
mtrezza Nov 7, 2025
8076a65
fix: test placement missing
coratgerl Nov 8, 2025
659ab5e
fix: test format
coratgerl Nov 8, 2025
fce3723
Merge branch 'alpha' into before-password-reset-request
mtrezza Nov 8, 2025
2045d87
Merge branch 'alpha' into before-password-reset-request
mtrezza Nov 8, 2025
8bfc13b
Merge branch 'alpha' into before-password-reset-request
mtrezza Nov 8, 2025
39f0e47
Merge branch 'alpha' into before-password-reset-request
mtrezza Nov 9, 2025
73aa8dd
Merge branch 'alpha' into before-password-reset-request
mtrezza Nov 9, 2025
0b3b21d
Merge branch 'alpha' into before-password-reset-request
mtrezza Nov 9, 2025
57712ce
Merge branch 'alpha' into before-password-reset-request
mtrezza Nov 9, 2025
903c00f
Merge branch 'alpha' into before-password-reset-request
mtrezza Nov 9, 2025
10acf6a
Merge branch 'alpha' into before-password-reset-request
coratgerl Nov 17, 2025
2cccdc4
Merge branch 'alpha' into before-password-reset-request
mtrezza Nov 17, 2025
6dee8bf
fix: feedbacks
coratgerl Nov 17, 2025
39a6a0b
Merge branch 'alpha' into before-password-reset-request
coratgerl Nov 17, 2025
ed0af6a
Merge branch 'alpha' into before-password-reset-request
mtrezza Nov 17, 2025
7d17d09
fix: feedbacks
coratgerl Nov 17, 2025
4316248
fix: feedbacks
coratgerl Nov 18, 2025
6d5c94d
Merge branch 'alpha' into before-password-reset-request
mtrezza Nov 18, 2025
08654bd
Update UsersRouter.js
mtrezza Nov 18, 2025
2a6df76
Update UsersRouter.js
mtrezza Nov 18, 2025
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
282 changes: 240 additions & 42 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3307,19 +3307,19 @@ describe('afterFind hooks', () => {
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
expect(() => {
Parse.Cloud.beforeLogin('SomeClass', () => { });
}).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
}).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
expect(() => {
Parse.Cloud.afterLogin(() => { });
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
}).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
expect(() => {
Parse.Cloud.afterLogin('_User', () => { });
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
}).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
expect(() => {
Parse.Cloud.afterLogin(Parse.User, () => { });
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
}).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
expect(() => {
Parse.Cloud.afterLogin('SomeClass', () => { });
}).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
}).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
expect(() => {
Parse.Cloud.afterLogout(() => { });
}).not.toThrow();
Expand Down Expand Up @@ -3777,60 +3777,258 @@ describe('beforeLogin hook', () => {
await Parse.User.logIn('tupac', 'shakur');
done();
});
});

describe('beforePasswordResetRequest hook', () => {
it('should run beforePasswordResetRequest with valid user', async done => {
let hit = 0;
let sendPasswordResetEmailCalled = false;
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => {
sendPasswordResetEmailCalled = true;
},
sendMail: () => {},
};

it('afterFind should not be triggered when saving an object', async () => {
let beforeSaves = 0;
Parse.Cloud.beforeSave('SavingTest', () => {
beforeSaves++;
await reconfigureServer({
appName: 'test',
emailAdapter: emailAdapter,
publicServerURL: 'http://localhost:8378/1',
});

let afterSaves = 0;
Parse.Cloud.afterSave('SavingTest', () => {
afterSaves++;
Parse.Cloud.beforePasswordResetRequest(req => {
hit++;
expect(req.object).toBeDefined();
expect(req.object.get('email')).toEqual('[email protected]');
expect(req.object.get('username')).toEqual('testuser');
});

let beforeFinds = 0;
Parse.Cloud.beforeFind('SavingTest', () => {
beforeFinds++;
const user = new Parse.User();
user.setUsername('testuser');
user.setPassword('password');
user.set('email', '[email protected]');
await user.signUp();

await Parse.User.requestPasswordReset('[email protected]');
expect(hit).toBe(1);
expect(sendPasswordResetEmailCalled).toBe(true);
done();
});

it('should be able to block password reset request if an error is thrown', async done => {
let hit = 0;
let sendPasswordResetEmailCalled = false;
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => {
sendPasswordResetEmailCalled = true;
},
sendMail: () => {},
};

await reconfigureServer({
appName: 'test',
emailAdapter: emailAdapter,
publicServerURL: 'http://localhost:8378/1',
});

let afterFinds = 0;
Parse.Cloud.afterFind('SavingTest', () => {
afterFinds++;
Parse.Cloud.beforePasswordResetRequest(req => {
hit++;
if (req.object.get('isBanned')) {
throw new Error('banned account');
}
});

const obj = new Parse.Object('SavingTest');
obj.set('someField', 'some value 1');
await obj.save();
const user = new Parse.User();
user.setUsername('banneduser');
user.setPassword('password');
user.set('email', '[email protected]');
await user.signUp();
await user.save({ isBanned: true });

expect(beforeSaves).toEqual(1);
expect(afterSaves).toEqual(1);
expect(beforeFinds).toEqual(0);
expect(afterFinds).toEqual(0);
try {
await Parse.User.requestPasswordReset('[email protected]');
throw new Error('should not have sent password reset email.');
} catch (e) {
expect(e.message).toBe('banned account');
}
expect(hit).toBe(1);
expect(sendPasswordResetEmailCalled).toBe(false);
done();
});

obj.set('someField', 'some value 2');
await obj.save();
it('should be able to block password reset request if an error is thrown even if the user has an attached file', async done => {
let hit = 0;
let sendPasswordResetEmailCalled = false;
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => {
sendPasswordResetEmailCalled = true;
},
sendMail: () => {},
};

expect(beforeSaves).toEqual(2);
expect(afterSaves).toEqual(2);
expect(beforeFinds).toEqual(0);
expect(afterFinds).toEqual(0);
await reconfigureServer({
appName: 'test',
emailAdapter: emailAdapter,
publicServerURL: 'http://localhost:8378/1',
});

await obj.fetch();
Parse.Cloud.beforePasswordResetRequest(req => {
hit++;
if (req.object.get('isBanned')) {
throw new Error('banned account');
}
});

expect(beforeSaves).toEqual(2);
expect(afterSaves).toEqual(2);
expect(beforeFinds).toEqual(1);
expect(afterFinds).toEqual(1);
const user = new Parse.User();
user.setUsername('banneduser2');
user.setPassword('password');
user.set('email', '[email protected]');
await user.signUp();
const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=';
const file = new Parse.File('myfile.txt', { base64 });
await file.save();
await user.save({ isBanned: true, file });

obj.set('someField', 'some value 3');
await obj.save();
try {
await Parse.User.requestPasswordReset('[email protected]');
throw new Error('should not have sent password reset email.');
} catch (e) {
expect(e.message).toBe('banned account');
}
expect(hit).toBe(1);
expect(sendPasswordResetEmailCalled).toBe(false);
done();
});

it('should not run beforePasswordResetRequest if email does not exist', async done => {
let hit = 0;
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => {},
sendMail: () => {},
};

await reconfigureServer({
emailAdapter: emailAdapter,
publicServerURL: 'http://localhost:8378/1',
});

Parse.Cloud.beforePasswordResetRequest(req => {
hit++;
});

try {
await Parse.User.requestPasswordReset('[email protected]');
} catch (e) {
// May or may not throw depending on passwordPolicy.resetPasswordSuccessOnInvalidEmail
}
expect(hit).toBe(0);
done();
});

it('should have expected data in request in beforePasswordResetRequest', async done => {
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => {},
sendMail: () => {},
};

await reconfigureServer({
appName: 'test',
emailAdapter: emailAdapter,
publicServerURL: 'http://localhost:8378/1',
});

Parse.Cloud.beforePasswordResetRequest(req => {
expect(req.object).toBeDefined();
expect(req.object.get('email')).toBeDefined();
expect(req.headers).toBeDefined();
expect(req.ip).toBeDefined();
expect(req.installationId).toBeDefined();
expect(req.context).toBeDefined();
expect(req.config).toBeDefined();
});

const user = new Parse.User();
user.setUsername('testuser2');
user.setPassword('password');
user.set('email', '[email protected]');
await user.signUp();
await Parse.User.requestPasswordReset('[email protected]');
done();
});

it('should validate that only _User class is allowed for beforePasswordResetRequest', () => {
expect(() => {
Parse.Cloud.beforePasswordResetRequest('SomeClass', () => { });
}).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
expect(() => {
Parse.Cloud.beforePasswordResetRequest(() => { });
}).not.toThrow();
expect(() => {
Parse.Cloud.beforePasswordResetRequest('_User', () => { });
}).not.toThrow();
expect(() => {
Parse.Cloud.beforePasswordResetRequest(Parse.User, () => { });
}).not.toThrow();
});
});

it('afterFind should not be triggered when saving an object', async () => {
let beforeSaves = 0;
Parse.Cloud.beforeSave('SavingTest', () => {
beforeSaves++;
});

let afterSaves = 0;
Parse.Cloud.afterSave('SavingTest', () => {
afterSaves++;
});

let beforeFinds = 0;
Parse.Cloud.beforeFind('SavingTest', () => {
beforeFinds++;
});

expect(beforeSaves).toEqual(3);
expect(afterSaves).toEqual(3);
expect(beforeFinds).toEqual(1);
expect(afterFinds).toEqual(1);
let afterFinds = 0;
Parse.Cloud.afterFind('SavingTest', () => {
afterFinds++;
});

const obj = new Parse.Object('SavingTest');
obj.set('someField', 'some value 1');
await obj.save();

expect(beforeSaves).toEqual(1);
expect(afterSaves).toEqual(1);
expect(beforeFinds).toEqual(0);
expect(afterFinds).toEqual(0);

obj.set('someField', 'some value 2');
await obj.save();

expect(beforeSaves).toEqual(2);
expect(afterSaves).toEqual(2);
expect(beforeFinds).toEqual(0);
expect(afterFinds).toEqual(0);

await obj.fetch();

expect(beforeSaves).toEqual(2);
expect(afterSaves).toEqual(2);
expect(beforeFinds).toEqual(1);
expect(afterFinds).toEqual(1);

obj.set('someField', 'some value 3');
await obj.save();

expect(beforeSaves).toEqual(3);
expect(afterSaves).toEqual(3);
expect(beforeFinds).toEqual(1);
expect(afterFinds).toEqual(1);
});

describe('afterLogin hook', () => {
Expand Down
45 changes: 42 additions & 3 deletions src/Routers/UsersRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Types as TriggerTypes,
getRequestObject,
resolveError,
inflate,
} from '../triggers';
import { promiseEnsureIdempotency } from '../middlewares';
import RestWrite from '../RestWrite';
Expand Down Expand Up @@ -444,21 +445,59 @@ export class UsersRouter extends ClassesRouter {
if (!email && !token) {
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
}

let userResults = null;
let userData = null;

// We can find the user using token
if (token) {
const results = await req.config.database.find('_User', {
userResults = await req.config.database.find('_User', {
_perishable_token: token,
_perishable_token_expires_at: { $lt: Parse._encode(new Date()) },
});
if (results && results[0] && results[0].email) {
email = results[0].email;
if (userResults && userResults.length > 0) {
userData = userResults[0];
if (userData.email) {
email = userData.email;
}
}
// Or using email if no token provided
} else if (typeof email === 'string') {
userResults = await req.config.database.find(
'_User',
{ $or: [{ email }, { username: email, email: { $exists: false } }] },
{ limit: 1 },
Auth.maintenance(req.config)
);
if (userResults && userResults.length > 0) {
userData = userResults[0];
}
}

if (typeof email !== 'string') {
throw new Parse.Error(
Parse.Error.INVALID_EMAIL_ADDRESS,
'you must provide a valid email string'
);
}

if (userData) {
this._sanitizeAuthData(userData);
// Useful to get User attached files in the trigger (photo picture for example)
await req.config.filesController.expandFilesInObject(req.config, userData);

const user = inflate('_User', userData);

await maybeRunTrigger(
TriggerTypes.beforePasswordResetRequest,
req.auth,
user,
null,
req.config,
req.info.context
);
}

const userController = req.config.userController;
try {
await userController.sendPasswordResetEmail(email);
Expand Down
Loading
Loading