From 09a1dca5e3605e1e4cd2db62a9594dc0e698e663 Mon Sep 17 00:00:00 2001 From: David Corona Date: Tue, 11 Feb 2020 17:38:14 -0600 Subject: [PATCH] Add new afterLogin cloud code hook (#6387) * add new afterLogin cloud code hook * include user on req.user for afterLogin hook --- spec/CloudCode.spec.js | 93 +++++++++++++++++++++++++++++++++-- src/Routers/UsersRouter.js | 12 +++++ src/cloud-code/Parse.Cloud.js | 36 ++++++++++++++ src/triggers.js | 11 +++-- 4 files changed, 145 insertions(+), 7 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index eb80c986..56884983 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2269,21 +2269,43 @@ describe('afterFind hooks', () => { expect(() => { Parse.Cloud.beforeLogin(() => {}); }).not.toThrow( - 'Only the _User class is allowed for the beforeLogin trigger' + 'Only the _User class is allowed for the beforeLogin and afterLogin triggers' ); expect(() => { Parse.Cloud.beforeLogin('_User', () => {}); }).not.toThrow( - 'Only the _User class is allowed for the beforeLogin trigger' + 'Only the _User class is allowed for the beforeLogin and afterLogin triggers' ); expect(() => { Parse.Cloud.beforeLogin(Parse.User, () => {}); }).not.toThrow( - 'Only the _User class is allowed for the beforeLogin trigger' + '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 trigger'); + }).toThrow( + 'Only the _User class is allowed for the beforeLogin and afterLogin triggers' + ); + expect(() => { + Parse.Cloud.afterLogin(() => {}); + }).not.toThrow( + 'Only the _User class is allowed for the beforeLogin and afterLogin triggers' + ); + expect(() => { + Parse.Cloud.afterLogin('_User', () => {}); + }).not.toThrow( + 'Only the _User class is allowed for the beforeLogin and afterLogin triggers' + ); + expect(() => { + Parse.Cloud.afterLogin(Parse.User, () => {}); + }).not.toThrow( + 'Only the _User class is allowed for the beforeLogin and afterLogin triggers' + ); + expect(() => { + Parse.Cloud.afterLogin('SomeClass', () => {}); + }).toThrow( + 'Only the _User class is allowed for the beforeLogin and afterLogin triggers' + ); expect(() => { Parse.Cloud.afterLogout(() => {}); }).not.toThrow(); @@ -2574,3 +2596,66 @@ describe('beforeLogin hook', () => { expect(afterFinds).toEqual(1); }); }); + +describe('afterLogin hook', () => { + it('should run afterLogin after successful login', async done => { + let hit = 0; + Parse.Cloud.afterLogin(req => { + hit++; + expect(req.object.get('username')).toEqual('testuser'); + }); + + await Parse.User.signUp('testuser', 'p@ssword'); + const user = await Parse.User.logIn('testuser', 'p@ssword'); + expect(hit).toBe(1); + expect(user).toBeDefined(); + expect(user.getUsername()).toBe('testuser'); + expect(user.getSessionToken()).toBeDefined(); + done(); + }); + + it('should not run afterLogin after unsuccessful login', async done => { + let hit = 0; + Parse.Cloud.afterLogin(req => { + hit++; + expect(req.object.get('username')).toEqual('testuser'); + }); + + await Parse.User.signUp('testuser', 'p@ssword'); + try { + await Parse.User.logIn('testuser', 'badpassword'); + } catch (e) { + expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND); + } + expect(hit).toBe(0); + done(); + }); + + it('should not run afterLogin on sign up', async done => { + let hit = 0; + Parse.Cloud.afterLogin(req => { + hit++; + expect(req.object.get('username')).toEqual('testuser'); + }); + + const user = await Parse.User.signUp('testuser', 'p@ssword'); + expect(user).toBeDefined(); + expect(hit).toBe(0); + done(); + }); + + it('should have expected data in request', async done => { + Parse.Cloud.afterLogin(req => { + expect(req.object).toBeDefined(); + expect(req.user).toBeDefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeUndefined(); + }); + + await Parse.User.signUp('testuser', 'p@ssword'); + await Parse.User.logIn('testuser', 'p@ssword'); + done(); + }); +}); diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 1da42681..c2587bed 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -264,6 +264,18 @@ export class UsersRouter extends ClassesRouter { user.sessionToken = sessionData.sessionToken; await createSession(); + + const afterLoginUser = Parse.User.fromJSON( + Object.assign({ className: '_User' }, user) + ); + maybeRunTrigger( + TriggerTypes.afterLogin, + { ...req.auth, user: afterLoginUser }, + afterLoginUser, + null, + req.config + ); + return { response: user }; } diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 5ad6769d..8b5e0d1c 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -165,6 +165,42 @@ ParseCloud.beforeLogin = function(handler) { ); }; +/** + * + * Registers the after login function. + * + * **Available in Cloud Code only.** + * + * This function is triggered after a user logs in successfully, + * and after a _Session object has been created. + * + * ``` + * Parse.Cloud.afterLogin((request) => { + * // code here + * }) + * + * ``` + * + * @method afterLogin + * @name Parse.Cloud.afterLogin + * @param {Function} func The function to run after a login. This function can be async and should take one parameter a {@link Parse.Cloud.TriggerRequest}; + */ +ParseCloud.afterLogin = function(handler) { + 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 = getClassName(handler); + handler = arguments[1]; + } + triggers.addTrigger( + triggers.Types.afterLogin, + className, + handler, + Parse.applicationId + ); +}; + /** * * Registers the after logout function. diff --git a/src/triggers.js b/src/triggers.js index 05ee7c27..41c33ee9 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -4,6 +4,7 @@ import { logger } from './logger'; export const Types = { beforeLogin: 'beforeLogin', + afterLogin: 'afterLogin', afterLogout: 'afterLogout', beforeSave: 'beforeSave', afterSave: 'afterSave', @@ -39,10 +40,13 @@ function validateClassNameForTriggers(className, type) { // TODO: Allow proper documented way of using nested increment ops throw 'Only afterSave is allowed on _PushStatus'; } - if (type === Types.beforeLogin && className !== '_User') { + if ( + (type === Types.beforeLogin || type === Types.afterLogin) && + className !== '_User' + ) { // TODO: check if upstream code will handle `Error` instance rather // than this anti-pattern of throwing strings - throw 'Only the _User class is allowed for the beforeLogin trigger'; + throw 'Only the _User class is allowed for the beforeLogin and afterLogin triggers'; } if (type === Types.afterLogout && className !== '_Session') { // TODO: check if upstream code will handle `Error` instance rather @@ -615,7 +619,8 @@ export function maybeRunTrigger( const promise = trigger(request); if ( triggerType === Types.afterSave || - triggerType === Types.afterDelete + triggerType === Types.afterDelete || + triggerType === Types.afterLogin ) { logTriggerAfterHook( triggerType,