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,
|
hasRegisteredDataHookEvent,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
|
||||||
type ManagementApiHooksRegistrationKey = keyof typeof managementApiHooksRegistration;
|
|
||||||
|
|
||||||
type DataHookMetadata = {
|
type DataHookMetadata = {
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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[] = [
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in a new issue