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.
This commit is contained in:
Tomi Turtiainen 2024-01-02 10:15:12 +02:00 committed by GitHub
parent ece48d6a13
commit e126ed74f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 53 additions and 1 deletions

View file

@ -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 { interface ErrorResponse {
code: number; code: number;
message: string; message: string;
@ -66,7 +88,7 @@ export function sendErrorResponse(res: Response, error: Error) {
message: error.message ?? 'Unknown error', message: error.message ?? 'Unknown error',
}; };
if (error instanceof ResponseError) { if (isResponseError(error)) {
if (inDevelopment) { if (inDevelopment) {
console.error(picocolors.red(error.httpStatusCode), error.message); console.error(picocolors.red(error.httpStatusCode), error.message);
} }

View file

@ -73,6 +73,8 @@ export class MeController {
} }
} }
await this.externalHooks.run('user.profile.beforeUpdate', [userId, currentEmail, payload]);
await this.userService.update(userId, payload); await this.userService.update(userId, payload);
const user = await this.userService.findOneOrFail({ where: { id: userId } }); const user = await this.userService.findOneOrFail({ where: { id: userId } });

View file

@ -53,6 +53,12 @@ describe('MeController', () => {
await controller.updateCurrentUser(req, res); await controller.updateCurrentUser(req, res);
expect(externalHooks.run).toHaveBeenCalledWith('user.profile.beforeUpdate', [
user.id,
user.email,
reqBody,
]);
expect(userService.update).toHaveBeenCalled(); expect(userService.update).toHaveBeenCalled();
const cookieOptions = captor<CookieOptions>(); const cookieOptions = captor<CookieOptions>();
@ -93,6 +99,28 @@ describe('MeController', () => {
expect(updatedUser.id).not.toBe('0'); expect(updatedUser.id).not.toBe('0');
expect(updatedUser.globalRoleId).not.toBe('42'); expect(updatedUser.globalRoleId).not.toBe('42');
}); });
it('should throw BadRequestError if beforeUpdate hook throws BadRequestError', async () => {
const user = mock<User>({
id: '123',
password: 'password',
authIdentities: [],
globalRoleId: '1',
});
const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' };
const req = mock<MeRequest.UserUpdate>({ 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', () => { describe('updatePassword', () => {