feat: Add beforePasswordResetRequest hook (#9906)
This commit is contained in:
@@ -3307,19 +3307,19 @@ describe('afterFind hooks', () => {
|
|||||||
}).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 and afterLogin triggers');
|
||||||
expect(() => {
|
expect(() => {
|
||||||
Parse.Cloud.beforeLogin('SomeClass', () => { });
|
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(() => {
|
expect(() => {
|
||||||
Parse.Cloud.afterLogin(() => { });
|
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(() => {
|
expect(() => {
|
||||||
Parse.Cloud.afterLogin('_User', () => { });
|
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(() => {
|
expect(() => {
|
||||||
Parse.Cloud.afterLogin(Parse.User, () => { });
|
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(() => {
|
expect(() => {
|
||||||
Parse.Cloud.afterLogin('SomeClass', () => { });
|
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(() => {
|
expect(() => {
|
||||||
Parse.Cloud.afterLogout(() => { });
|
Parse.Cloud.afterLogout(() => { });
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
@@ -4656,3 +4656,157 @@ describe('sendEmail', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('beforePasswordResetRequest hook', () => {
|
||||||
|
it('should run beforePasswordResetRequest with valid user', async () => {
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.beforePasswordResetRequest(req => {
|
||||||
|
hit++;
|
||||||
|
expect(req.object).toBeDefined();
|
||||||
|
expect(req.object.get('email')).toEqual('test@example.com');
|
||||||
|
expect(req.object.get('username')).toEqual('testuser');
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = new Parse.User();
|
||||||
|
user.setUsername('testuser');
|
||||||
|
user.setPassword('password');
|
||||||
|
user.set('email', 'test@example.com');
|
||||||
|
await user.signUp();
|
||||||
|
|
||||||
|
await Parse.User.requestPasswordReset('test@example.com');
|
||||||
|
expect(hit).toBe(1);
|
||||||
|
expect(sendPasswordResetEmailCalled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to block password reset request if an error is thrown', async () => {
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.beforePasswordResetRequest(req => {
|
||||||
|
hit++;
|
||||||
|
throw new Error('password reset blocked');
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = new Parse.User();
|
||||||
|
user.setUsername('testuser');
|
||||||
|
user.setPassword('password');
|
||||||
|
user.set('email', 'test@example.com');
|
||||||
|
await user.signUp();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Parse.User.requestPasswordReset('test@example.com');
|
||||||
|
throw new Error('should not have sent password reset email.');
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe('password reset blocked');
|
||||||
|
}
|
||||||
|
expect(hit).toBe(1);
|
||||||
|
expect(sendPasswordResetEmailCalled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run beforePasswordResetRequest if email does not exist', async () => {
|
||||||
|
let hit = 0;
|
||||||
|
const emailAdapter = {
|
||||||
|
sendVerificationEmail: () => Promise.resolve(),
|
||||||
|
sendPasswordResetEmail: () => {},
|
||||||
|
sendMail: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await reconfigureServer({
|
||||||
|
appName: 'test',
|
||||||
|
emailAdapter: emailAdapter,
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.beforePasswordResetRequest(req => {
|
||||||
|
hit++;
|
||||||
|
});
|
||||||
|
|
||||||
|
await Parse.User.requestPasswordReset('nonexistent@example.com');
|
||||||
|
|
||||||
|
expect(hit).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have expected data in request in beforePasswordResetRequest', async () => {
|
||||||
|
const emailAdapter = {
|
||||||
|
sendVerificationEmail: () => Promise.resolve(),
|
||||||
|
sendPasswordResetEmail: () => {},
|
||||||
|
sendMail: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await reconfigureServer({
|
||||||
|
appName: 'test',
|
||||||
|
emailAdapter: emailAdapter,
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=';
|
||||||
|
const file = new Parse.File('myfile.txt', { base64 });
|
||||||
|
await file.save();
|
||||||
|
|
||||||
|
Parse.Cloud.beforePasswordResetRequest(req => {
|
||||||
|
expect(req.object).toBeDefined();
|
||||||
|
expect(req.object.get('email')).toBeDefined();
|
||||||
|
expect(req.object.get('email')).toBe('test2@example.com');
|
||||||
|
expect(req.object.get('file')).toBeDefined();
|
||||||
|
expect(req.object.get('file')).toBeInstanceOf(Parse.File);
|
||||||
|
expect(req.object.get('file').name()).toContain('myfile.txt');
|
||||||
|
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', 'test2@example.com');
|
||||||
|
user.set('file', file);
|
||||||
|
await user.signUp();
|
||||||
|
|
||||||
|
await Parse.User.requestPasswordReset('test2@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Types as TriggerTypes,
|
Types as TriggerTypes,
|
||||||
getRequestObject,
|
getRequestObject,
|
||||||
resolveError,
|
resolveError,
|
||||||
|
inflate,
|
||||||
} from '../triggers';
|
} from '../triggers';
|
||||||
import { promiseEnsureIdempotency } from '../middlewares';
|
import { promiseEnsureIdempotency } from '../middlewares';
|
||||||
import RestWrite from '../RestWrite';
|
import RestWrite from '../RestWrite';
|
||||||
@@ -444,21 +445,59 @@ export class UsersRouter extends ClassesRouter {
|
|||||||
if (!email && !token) {
|
if (!email && !token) {
|
||||||
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
|
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) {
|
if (token) {
|
||||||
const results = await req.config.database.find('_User', {
|
userResults = await req.config.database.find('_User', {
|
||||||
_perishable_token: token,
|
_perishable_token: token,
|
||||||
_perishable_token_expires_at: { $lt: Parse._encode(new Date()) },
|
_perishable_token_expires_at: { $lt: Parse._encode(new Date()) },
|
||||||
});
|
});
|
||||||
if (results && results[0] && results[0].email) {
|
if (userResults?.length > 0) {
|
||||||
email = results[0].email;
|
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?.length > 0) {
|
||||||
|
userData = userResults[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof email !== 'string') {
|
if (typeof email !== 'string') {
|
||||||
throw new Parse.Error(
|
throw new Parse.Error(
|
||||||
Parse.Error.INVALID_EMAIL_ADDRESS,
|
Parse.Error.INVALID_EMAIL_ADDRESS,
|
||||||
'you must provide a valid email string'
|
'you must provide a valid email string'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userData) {
|
||||||
|
this._sanitizeAuthData(userData);
|
||||||
|
// Get files attached to user
|
||||||
|
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;
|
const userController = req.config.userController;
|
||||||
try {
|
try {
|
||||||
await userController.sendPasswordResetEmail(email);
|
await userController.sendPasswordResetEmail(email);
|
||||||
|
|||||||
@@ -349,6 +349,48 @@ ParseCloud.afterLogout = function (handler) {
|
|||||||
triggers.addTrigger(triggers.Types.afterLogout, className, handler, Parse.applicationId);
|
triggers.addTrigger(triggers.Types.afterLogout, className, handler, Parse.applicationId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the before password reset request function.
|
||||||
|
*
|
||||||
|
* **Available in Cloud Code only.**
|
||||||
|
*
|
||||||
|
* This function provides control in validating a password reset request
|
||||||
|
* before the reset email is sent. It is triggered after the user is found
|
||||||
|
* by email, but before the reset token is generated and the email is sent.
|
||||||
|
*
|
||||||
|
* Code example:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* Parse.Cloud.beforePasswordResetRequest(request => {
|
||||||
|
* if (request.object.get('banned')) {
|
||||||
|
* throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User is banned.');
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @method beforePasswordResetRequest
|
||||||
|
* @name Parse.Cloud.beforePasswordResetRequest
|
||||||
|
* @param {Function} func The function to run before a password reset request. This function can be async and should take one parameter a {@link Parse.Cloud.TriggerRequest};
|
||||||
|
*/
|
||||||
|
ParseCloud.beforePasswordResetRequest = function (handler, validationHandler) {
|
||||||
|
let className = '_User';
|
||||||
|
if (typeof handler === 'string' || isParseObjectConstructor(handler)) {
|
||||||
|
// validation will occur downstream, this is to maintain internal
|
||||||
|
// code consistency with the other hook types.
|
||||||
|
className = triggers.getClassName(handler);
|
||||||
|
handler = arguments[1];
|
||||||
|
validationHandler = arguments.length >= 2 ? arguments[2] : null;
|
||||||
|
}
|
||||||
|
triggers.addTrigger(triggers.Types.beforePasswordResetRequest, className, handler, Parse.applicationId);
|
||||||
|
if (validationHandler && validationHandler.rateLimit) {
|
||||||
|
addRateLimit(
|
||||||
|
{ requestPath: `/requestPasswordReset`, requestMethods: 'POST', ...validationHandler.rateLimit },
|
||||||
|
Parse.applicationId,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers an after save function.
|
* Registers an after save function.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const Types = {
|
|||||||
beforeLogin: 'beforeLogin',
|
beforeLogin: 'beforeLogin',
|
||||||
afterLogin: 'afterLogin',
|
afterLogin: 'afterLogin',
|
||||||
afterLogout: 'afterLogout',
|
afterLogout: 'afterLogout',
|
||||||
|
beforePasswordResetRequest: 'beforePasswordResetRequest',
|
||||||
beforeSave: 'beforeSave',
|
beforeSave: 'beforeSave',
|
||||||
afterSave: 'afterSave',
|
afterSave: 'afterSave',
|
||||||
beforeDelete: 'beforeDelete',
|
beforeDelete: 'beforeDelete',
|
||||||
@@ -58,10 +59,10 @@ function validateClassNameForTriggers(className, type) {
|
|||||||
// TODO: Allow proper documented way of using nested increment ops
|
// TODO: Allow proper documented way of using nested increment ops
|
||||||
throw 'Only afterSave is allowed on _PushStatus';
|
throw 'Only afterSave is allowed on _PushStatus';
|
||||||
}
|
}
|
||||||
if ((type === Types.beforeLogin || type === Types.afterLogin) && className !== '_User') {
|
if ((type === Types.beforeLogin || type === Types.afterLogin || type === Types.beforePasswordResetRequest) && className !== '_User') {
|
||||||
// TODO: check if upstream code will handle `Error` instance rather
|
// TODO: check if upstream code will handle `Error` instance rather
|
||||||
// than this anti-pattern of throwing strings
|
// than this anti-pattern of throwing strings
|
||||||
throw 'Only the _User class is allowed for the beforeLogin and afterLogin triggers';
|
throw 'Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers';
|
||||||
}
|
}
|
||||||
if (type === Types.afterLogout && className !== '_Session') {
|
if (type === Types.afterLogout && className !== '_Session') {
|
||||||
// TODO: check if upstream code will handle `Error` instance rather
|
// TODO: check if upstream code will handle `Error` instance rather
|
||||||
@@ -287,6 +288,7 @@ export function getRequestObject(
|
|||||||
triggerType === Types.afterDelete ||
|
triggerType === Types.afterDelete ||
|
||||||
triggerType === Types.beforeLogin ||
|
triggerType === Types.beforeLogin ||
|
||||||
triggerType === Types.afterLogin ||
|
triggerType === Types.afterLogin ||
|
||||||
|
triggerType === Types.beforePasswordResetRequest ||
|
||||||
triggerType === Types.afterFind
|
triggerType === Types.afterFind
|
||||||
) {
|
) {
|
||||||
// Set a copy of the context on the request object.
|
// Set a copy of the context on the request object.
|
||||||
|
|||||||
Reference in New Issue
Block a user