From e126ed74f3d9ed3dae72252cb8c9e8a6f7620808 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 2 Jan 2024 10:15:12 +0200 Subject: [PATCH] feat(core): Add user.profile.beforeUpdate hook (#8144) Add `user.profile.beforeUpdate` hook so we can prevent user email change if it overlaps with other users email. --- packages/cli/src/ResponseHelper.ts | 24 +++++++++++++++- packages/cli/src/controllers/me.controller.ts | 2 ++ .../unit/controllers/me.controller.test.ts | 28 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/ResponseHelper.ts b/packages/cli/src/ResponseHelper.ts index 5213e3964e..d477af6060 100644 --- a/packages/cli/src/ResponseHelper.ts +++ b/packages/cli/src/ResponseHelper.ts @@ -51,6 +51,28 @@ export function sendSuccessResponse( } } +/** + * Checks if the given error is a ResponseError. It can be either an + * instance of ResponseError or an error which has the same properties. + * The latter case is for external hooks. + */ +function isResponseError(error: Error): error is ResponseError { + if (error instanceof ResponseError) { + return true; + } + + if (error instanceof Error) { + return ( + 'httpStatusCode' in error && + typeof error.httpStatusCode === 'number' && + 'errorCode' in error && + typeof error.errorCode === 'number' + ); + } + + return false; +} + interface ErrorResponse { code: number; message: string; @@ -66,7 +88,7 @@ export function sendErrorResponse(res: Response, error: Error) { message: error.message ?? 'Unknown error', }; - if (error instanceof ResponseError) { + if (isResponseError(error)) { if (inDevelopment) { console.error(picocolors.red(error.httpStatusCode), error.message); } diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index 19060e43fe..1c518ef744 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -73,6 +73,8 @@ export class MeController { } } + await this.externalHooks.run('user.profile.beforeUpdate', [userId, currentEmail, payload]); + await this.userService.update(userId, payload); const user = await this.userService.findOneOrFail({ where: { id: userId } }); diff --git a/packages/cli/test/unit/controllers/me.controller.test.ts b/packages/cli/test/unit/controllers/me.controller.test.ts index c27f071102..9982b91963 100644 --- a/packages/cli/test/unit/controllers/me.controller.test.ts +++ b/packages/cli/test/unit/controllers/me.controller.test.ts @@ -53,6 +53,12 @@ describe('MeController', () => { await controller.updateCurrentUser(req, res); + expect(externalHooks.run).toHaveBeenCalledWith('user.profile.beforeUpdate', [ + user.id, + user.email, + reqBody, + ]); + expect(userService.update).toHaveBeenCalled(); const cookieOptions = captor(); @@ -93,6 +99,28 @@ describe('MeController', () => { expect(updatedUser.id).not.toBe('0'); expect(updatedUser.globalRoleId).not.toBe('42'); }); + + it('should throw BadRequestError if beforeUpdate hook throws BadRequestError', async () => { + const user = mock({ + id: '123', + password: 'password', + authIdentities: [], + globalRoleId: '1', + }); + const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' }; + const req = mock({ user, body: reqBody }); + userService.findOneOrFail.mockResolvedValue(user); + + externalHooks.run.mockImplementationOnce(async (hookName) => { + if (hookName === 'user.profile.beforeUpdate') { + throw new BadRequestError('Invalid email address'); + } + }); + + await expect(controller.updateCurrentUser(req, mock())).rejects.toThrowError( + new BadRequestError('Invalid email address'), + ); + }); }); describe('updatePassword', () => {