mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
refactor(core,schemas): add user detail payload to User.Deleted webhook event (#5986)
* refactor(core,schemas): add user detail payload to User.Deleted DataHook event add user detail data payload to the User.Deleted DataHook event * fix(core): fix unit test fix unit test
This commit is contained in:
parent
7ebabc490a
commit
7a279be1fc
10 changed files with 47 additions and 23 deletions
5
.changeset/fuzzy-eyes-add.md
Normal file
5
.changeset/fuzzy-eyes-add.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@logto/core": patch
|
||||
---
|
||||
|
||||
add user detail data payload to the `User.Deleted` webhook event
|
|
@ -16,8 +16,6 @@ import {
|
|||
hasRegisteredDataHookEvent,
|
||||
} from './utils.js';
|
||||
|
||||
type ManagementApiHooksRegistrationKey = keyof typeof managementApiHooksRegistration;
|
||||
|
||||
type DataHookMetadata = {
|
||||
userAgent?: string;
|
||||
ip: string;
|
||||
|
|
|
@ -5,6 +5,7 @@ import { removeUndefinedKeys } from '@silverhand/essentials';
|
|||
|
||||
import { mockUser, mockUserResponse } from '#src/__mocks__/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { koaManagementApiHooks } from '#src/middleware/koa-management-api-hooks.js';
|
||||
import type Libraries from '#src/tenants/Libraries.js';
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
import { MockTenant, type Partial2 } from '#src/test-utils/tenant.js';
|
||||
|
@ -95,7 +96,11 @@ describe('adminUserRoutes', () => {
|
|||
const tenantContext = new MockTenant(undefined, mockedQueries, undefined, {
|
||||
users: usersLibraries,
|
||||
});
|
||||
const userRequest = createRequester({ authedRoutes: adminUserRoutes, tenantContext });
|
||||
const userRequest = createRequester({
|
||||
middlewares: [koaManagementApiHooks(tenantContext.libraries.hooks)],
|
||||
authedRoutes: adminUserRoutes,
|
||||
tenantContext,
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
|
@ -11,6 +11,7 @@ import { conditional, pick, yes } from '@silverhand/essentials';
|
|||
import { boolean, literal, nativeEnum, object, string } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { buildManagementApiContext } from '#src/libraries/hook/utils.js';
|
||||
import { encryptUserPassword } from '#src/libraries/user.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
@ -373,11 +374,20 @@ export default function adminUserBasicsRoutes<T extends ManagementApiRouter>(
|
|||
throw new RequestError('user.cannot_delete_self');
|
||||
}
|
||||
|
||||
const user = await findUserById(userId);
|
||||
|
||||
await signOutUser(userId);
|
||||
await deleteUserById(userId);
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
// Manually trigger the `User.Deleted` hook since we need to send the user data in the payload
|
||||
ctx.appendDataHookContext({
|
||||
event: 'User.Deleted',
|
||||
...buildManagementApiContext(ctx),
|
||||
data: pick(user, ...userInfoSelectFields),
|
||||
});
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { pickDefault, createMockUtils } from '@logto/shared/esm';
|
||||
import { createMockUtils, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import {
|
||||
mockAliyunDmConnector,
|
||||
|
@ -63,14 +63,6 @@ describe('GET /.well-known/sign-in-exp', () => {
|
|||
const sessionRequest = createRequester({
|
||||
anonymousRoutes: wellKnownRoutes,
|
||||
tenantContext,
|
||||
middlewares: [
|
||||
async (ctx, next) => {
|
||||
ctx.addLogContext = jest.fn();
|
||||
ctx.log = jest.fn();
|
||||
|
||||
return next();
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
it('should return github and facebook connector instances', async () => {
|
||||
|
|
|
@ -112,7 +112,7 @@ type RouteLauncher<T extends ManagementApiRouter | AnonymousRouter> = (
|
|||
tenant: TenantContext
|
||||
) => void;
|
||||
|
||||
export function createRequester({
|
||||
export function createRequester<StateT, ContextT extends IRouterParamContext, ResponseT>({
|
||||
anonymousRoutes,
|
||||
authedRoutes,
|
||||
middlewares,
|
||||
|
@ -120,7 +120,7 @@ export function createRequester({
|
|||
}: {
|
||||
anonymousRoutes?: RouteLauncher<AnonymousRouter> | Array<RouteLauncher<AnonymousRouter>>;
|
||||
authedRoutes?: RouteLauncher<ManagementApiRouter> | Array<RouteLauncher<ManagementApiRouter>>;
|
||||
middlewares?: Middleware[];
|
||||
middlewares?: Array<Middleware<StateT, ContextT, ResponseT>>;
|
||||
tenantContext?: TenantContext;
|
||||
}) {
|
||||
const app = new Koa();
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { hookEvents } from '@logto/schemas';
|
||||
import { hookEvents, userInfoSelectFields } from '@logto/schemas';
|
||||
import { pick } from '@silverhand/essentials';
|
||||
|
||||
import { createUser, deleteUser } from '#src/api/admin-user.js';
|
||||
import { OrganizationRoleApi } from '#src/api/organization-role.js';
|
||||
import { OrganizationScopeApi } from '#src/api/organization-scope.js';
|
||||
import { createResource, deleteResource } from '#src/api/resource.js';
|
||||
|
@ -94,4 +96,18 @@ describe('trigger custom data hook events', () => {
|
|||
await roleApi.delete(organizationRole.id);
|
||||
await organizationScopeApi.delete(scope.id);
|
||||
});
|
||||
|
||||
it('delete user should trigger User.Deleted event with selected user info', async () => {
|
||||
const user = await createUser();
|
||||
const hook = webHookApi.hooks.get(hookName)!;
|
||||
|
||||
await deleteUser(user.id);
|
||||
|
||||
await assertHookLogResult(hook, 'User.Deleted', {
|
||||
hookPayload: {
|
||||
event: 'User.Deleted',
|
||||
data: pick(user, ...userInfoSelectFields),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -120,6 +120,11 @@ describe('user data hook events', () => {
|
|||
expect(hook?.payload.event).toBe(event);
|
||||
}
|
||||
);
|
||||
|
||||
// Clean up
|
||||
afterAll(async () => {
|
||||
await authedAdminApi.delete(`users/${userId}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('role data hook events', () => {
|
||||
|
|
|
@ -44,13 +44,6 @@ export const userDataHookTestCases: TestCase[] = [
|
|||
endpoint: `users/{userId}/is-suspended`,
|
||||
payload: { isSuspended: true },
|
||||
},
|
||||
{
|
||||
route: 'DELETE /users/:userId',
|
||||
event: 'User.Deleted',
|
||||
method: 'delete',
|
||||
endpoint: `users/{userId}`,
|
||||
payload: {},
|
||||
},
|
||||
];
|
||||
|
||||
export const roleDataHookTestCases: TestCase[] = [
|
||||
|
|
|
@ -117,7 +117,7 @@ type ApiMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|||
*/
|
||||
export const managementApiHooksRegistration = Object.freeze({
|
||||
'POST /users': 'User.Created',
|
||||
'DELETE /users/:userId': 'User.Deleted',
|
||||
// `User.Deleted` event is triggered manually in the `DELETE /users/:userId` route for better payload control
|
||||
'PATCH /users/:userId': 'User.Data.Updated',
|
||||
'PATCH /users/:userId/custom-data': 'User.Data.Updated',
|
||||
'PATCH /users/:userId/profile': 'User.Data.Updated',
|
||||
|
|
Loading…
Reference in a new issue