0
Fork 0
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:
simeng-li 2024-06-04 19:09:27 +08:00 committed by GitHub
parent 7ebabc490a
commit 7a279be1fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 47 additions and 23 deletions

View file

@ -0,0 +1,5 @@
---
"@logto/core": patch
---
add user detail data payload to the `User.Deleted` webhook event

View file

@ -16,8 +16,6 @@ import {
hasRegisteredDataHookEvent, hasRegisteredDataHookEvent,
} from './utils.js'; } from './utils.js';
type ManagementApiHooksRegistrationKey = keyof typeof managementApiHooksRegistration;
type DataHookMetadata = { type DataHookMetadata = {
userAgent?: string; userAgent?: string;
ip: string; ip: string;

View file

@ -5,6 +5,7 @@ import { removeUndefinedKeys } from '@silverhand/essentials';
import { mockUser, mockUserResponse } from '#src/__mocks__/index.js'; import { mockUser, mockUserResponse } from '#src/__mocks__/index.js';
import RequestError from '#src/errors/RequestError/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 Libraries from '#src/tenants/Libraries.js';
import type Queries from '#src/tenants/Queries.js'; import type Queries from '#src/tenants/Queries.js';
import { MockTenant, type Partial2 } from '#src/test-utils/tenant.js'; import { MockTenant, type Partial2 } from '#src/test-utils/tenant.js';
@ -95,7 +96,11 @@ describe('adminUserRoutes', () => {
const tenantContext = new MockTenant(undefined, mockedQueries, undefined, { const tenantContext = new MockTenant(undefined, mockedQueries, undefined, {
users: usersLibraries, users: usersLibraries,
}); });
const userRequest = createRequester({ authedRoutes: adminUserRoutes, tenantContext }); const userRequest = createRequester({
middlewares: [koaManagementApiHooks(tenantContext.libraries.hooks)],
authedRoutes: adminUserRoutes,
tenantContext,
});
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();

View file

@ -11,6 +11,7 @@ import { conditional, pick, yes } from '@silverhand/essentials';
import { boolean, literal, nativeEnum, object, string } from 'zod'; import { boolean, literal, nativeEnum, object, string } from 'zod';
import RequestError from '#src/errors/RequestError/index.js'; import RequestError from '#src/errors/RequestError/index.js';
import { buildManagementApiContext } from '#src/libraries/hook/utils.js';
import { encryptUserPassword } from '#src/libraries/user.js'; import { encryptUserPassword } from '#src/libraries/user.js';
import koaGuard from '#src/middleware/koa-guard.js'; import koaGuard from '#src/middleware/koa-guard.js';
import assertThat from '#src/utils/assert-that.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'); throw new RequestError('user.cannot_delete_self');
} }
const user = await findUserById(userId);
await signOutUser(userId); await signOutUser(userId);
await deleteUserById(userId); await deleteUserById(userId);
ctx.status = 204; 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(); return next();
} }
); );

View file

@ -1,4 +1,4 @@
import { pickDefault, createMockUtils } from '@logto/shared/esm'; import { createMockUtils, pickDefault } from '@logto/shared/esm';
import { import {
mockAliyunDmConnector, mockAliyunDmConnector,
@ -63,14 +63,6 @@ describe('GET /.well-known/sign-in-exp', () => {
const sessionRequest = createRequester({ const sessionRequest = createRequester({
anonymousRoutes: wellKnownRoutes, anonymousRoutes: wellKnownRoutes,
tenantContext, tenantContext,
middlewares: [
async (ctx, next) => {
ctx.addLogContext = jest.fn();
ctx.log = jest.fn();
return next();
},
],
}); });
it('should return github and facebook connector instances', async () => { it('should return github and facebook connector instances', async () => {

View file

@ -112,7 +112,7 @@ type RouteLauncher<T extends ManagementApiRouter | AnonymousRouter> = (
tenant: TenantContext tenant: TenantContext
) => void; ) => void;
export function createRequester({ export function createRequester<StateT, ContextT extends IRouterParamContext, ResponseT>({
anonymousRoutes, anonymousRoutes,
authedRoutes, authedRoutes,
middlewares, middlewares,
@ -120,7 +120,7 @@ export function createRequester({
}: { }: {
anonymousRoutes?: RouteLauncher<AnonymousRouter> | Array<RouteLauncher<AnonymousRouter>>; anonymousRoutes?: RouteLauncher<AnonymousRouter> | Array<RouteLauncher<AnonymousRouter>>;
authedRoutes?: RouteLauncher<ManagementApiRouter> | Array<RouteLauncher<ManagementApiRouter>>; authedRoutes?: RouteLauncher<ManagementApiRouter> | Array<RouteLauncher<ManagementApiRouter>>;
middlewares?: Middleware[]; middlewares?: Array<Middleware<StateT, ContextT, ResponseT>>;
tenantContext?: TenantContext; tenantContext?: TenantContext;
}) { }) {
const app = new Koa(); const app = new Koa();

View file

@ -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 { OrganizationRoleApi } from '#src/api/organization-role.js';
import { OrganizationScopeApi } from '#src/api/organization-scope.js'; import { OrganizationScopeApi } from '#src/api/organization-scope.js';
import { createResource, deleteResource } from '#src/api/resource.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 roleApi.delete(organizationRole.id);
await organizationScopeApi.delete(scope.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),
},
});
});
}); });

View file

@ -120,6 +120,11 @@ describe('user data hook events', () => {
expect(hook?.payload.event).toBe(event); expect(hook?.payload.event).toBe(event);
} }
); );
// Clean up
afterAll(async () => {
await authedAdminApi.delete(`users/${userId}`);
});
}); });
describe('role data hook events', () => { describe('role data hook events', () => {

View file

@ -44,13 +44,6 @@ export const userDataHookTestCases: TestCase[] = [
endpoint: `users/{userId}/is-suspended`, endpoint: `users/{userId}/is-suspended`,
payload: { isSuspended: true }, payload: { isSuspended: true },
}, },
{
route: 'DELETE /users/:userId',
event: 'User.Deleted',
method: 'delete',
endpoint: `users/{userId}`,
payload: {},
},
]; ];
export const roleDataHookTestCases: TestCase[] = [ export const roleDataHookTestCases: TestCase[] = [

View file

@ -117,7 +117,7 @@ type ApiMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
*/ */
export const managementApiHooksRegistration = Object.freeze({ export const managementApiHooksRegistration = Object.freeze({
'POST /users': 'User.Created', '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': 'User.Data.Updated',
'PATCH /users/:userId/custom-data': 'User.Data.Updated', 'PATCH /users/:userId/custom-data': 'User.Data.Updated',
'PATCH /users/:userId/profile': 'User.Data.Updated', 'PATCH /users/:userId/profile': 'User.Data.Updated',