Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(console,core): remove DataHook devFeature guard #5898

Merged
merged 3 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .changeset/hip-fireants-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
"@logto/console": minor
"@logto/core": minor
"@logto/schemas": minor
---

add new webhook events

We introduce a new event type `DataHook` to unlock a series of events that can be triggered by data updates (mostly Management API):

- User.Created
- User.Deleted
- User.Data.Updated
- User.SuspensionStatus.Updated
- Role.Created
- Role.Deleted
- Role.Data.Updated
- Role.Scopes.Updated
- Scope.Created
- Scope.Deleted
- Scope.Data.Updated
- Organization.Created
- Organization.Deleted
- Organization.Data.Updated
- Organization.Membership.Updated
- OrganizationRole.Created
- OrganizationRole.Deleted
- OrganizationRole.Data.Updated
- OrganizationRole.Scopes.Updated
- OrganizationScope.Created
- OrganizationScope.Deleted
- OrganizationScope.Data.Updated

DataHook events are triggered when the data associated with the event is updated via management API request or user interaction actions.

### Management API triggered events

| API endpoint | Event |
| ---------------------------------------------------------- | ----------------------------------------------------------- |
| POST /users | User.Created |
| DELETE /users/:userId | User.Deleted |
| PATCH /users/:userId | User.Data.Updated |
| PATCH /users/:userId/custom-data | User.Data.Updated |
| PATCH /users/:userId/profile | User.Data.Updated |
| PATCH /users/:userId/password | User.Data.Updated |
| PATCH /users/:userId/is-suspended | User.SuspensionStatus.Updated |
| POST /roles | Role.Created, (Role.Scopes.Update) |
| DELETE /roles/:id | Role.Deleted |
| PATCH /roles/:id | Role.Data.Updated |
| POST /roles/:id/scopes | Role.Scopes.Updated |
| DELETE /roles/:id/scopes/:scopeId | Role.Scopes.Updated |
| POST /resources/:resourceId/scopes | Scope.Created |
| DELETE /resources/:resourceId/scopes/:scopeId | Scope.Deleted |
| PATCH /resources/:resourceId/scopes/:scopeId | Scope.Data.Updated |
| POST /organizations | Organization.Created |
| DELETE /organizations/:id | Organization.Deleted |
| PATCH /organizations/:id | Organization.Data.Updated |
| PUT /organizations/:id/users | Organization.Membership.Updated |
| POST /organizations/:id/users | Organization.Membership.Updated |
| DELETE /organizations/:id/users/:userId | Organization.Membership.Updated |
| POST /organization-roles | OrganizationRole.Created, (OrganizationRole.Scopes.Updated) |
| DELETE /organization-roles/:id | OrganizationRole.Deleted |
| PATCH /organization-roles/:id | OrganizationRole.Data.Updated |
| POST /organization-scopes | OrganizationScope.Created |
| DELETE /organization-scopes/:id | OrganizationScope.Deleted |
| PATCH /organization-scopes/:id | OrganizationScope.Data.Updated |
| PUT /organization-roles/:id/scopes | OrganizationRole.Scopes.Updated |
| POST /organization-roles/:id/scopes | OrganizationRole.Scopes.Updated |
| DELETE /organization-roles/:id/scopes/:organizationScopeId | OrganizationRole.Scopes.Updated |

### User interaction triggered events

| User interaction action | Event |
| ------------------------ | ----------------- |
| User email/phone linking | User.Data.Updated |
| User MFAs linking | User.Data.Updated |
| User social/SSO linking | User.Data.Updated |
| User password reset | User.Data.Updated |
| User registration | User.Created |
16 changes: 6 additions & 10 deletions packages/console/src/components/BasicWebhookForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { type Hook, type HookConfig, type HookEvent } from '@logto/schemas';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

import { isDevFeaturesEnabled } from '@/consts/env';
import {
dataHookEventsLabel,
interactionHookEvents,
Expand All @@ -18,15 +17,12 @@ import { uriValidator } from '@/utils/validator';
import * as styles from './index.module.scss';

const hookEventGroups: Array<CheckboxOptionGroup<HookEvent>> = [
// TODO: Remove dev feature guard
...(isDevFeaturesEnabled
? schemaGroupedDataHookEvents.map(([schema, events]) => ({
title: dataHookEventsLabel[schema],
options: events.map((event) => ({
value: event,
})),
}))
: []),
...schemaGroupedDataHookEvents.map(([schema, events]) => ({
title: dataHookEventsLabel[schema],
options: events.map((event) => ({
value: event,
})),
})),
{
title: 'webhooks.schemas.interaction',
options: interactionHookEvents.map((event) => ({
Expand Down
1 change: 1 addition & 0 deletions packages/console/src/consts/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ const isProduction = process.env.NODE_ENV === 'production';
export const isCloud = yes(process.env.IS_CLOUD);
export const adminEndpoint = process.env.ADMIN_ENDPOINT;

// eslint-disable-next-line import/no-unused-modules
export const isDevFeaturesEnabled =
!isProduction || yes(process.env.DEV_FEATURES_ENABLED) || yes(process.env.INTEGRATION_TEST);
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import { z } from 'zod';
import EventSelector from '@/components/AuditLogTable/components/EventSelector';
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
import { defaultPageSize } from '@/consts';
import { isDevFeaturesEnabled } from '@/consts/env';
import { interactionHookEvents } from '@/consts/webhooks';
import Table from '@/ds-components/Table';
import Tag from '@/ds-components/Tag';
import { type RequestError } from '@/hooks/use-api';
Expand All @@ -22,10 +20,7 @@ import { buildHookEventLogKey, getHookEventKey } from '../utils';

import * as styles from './index.module.scss';

// TODO: Remove dev feature guard
const webhookEvents = isDevFeaturesEnabled ? hookEvents : interactionHookEvents;

const hookLogEventOptions = webhookEvents.map((event) => ({
const hookLogEventOptions = hookEvents.map((event) => ({
title: event,
value: buildHookEventLogKey(event),
}));
Expand Down
7 changes: 0 additions & 7 deletions packages/core/src/middleware/koa-management-api-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { trySafe } from '@silverhand/essentials';
import { type MiddlewareType } from 'koa';
import { type IRouterParamContext } from 'koa-router';

import { EnvSet } from '#src/env-set/index.js';
import { DataHookContextManager } from '#src/libraries/hook/context-manager.js';
import type Libraries from '#src/tenants/Libraries.js';
import { getConsoleLogFromContext } from '#src/utils/console.js';
Expand All @@ -22,12 +21,6 @@ export const koaManagementApiHooks = <StateT, ContextT extends IRouterParamConte
hooks: Libraries['hooks']
): MiddlewareType<StateT, WithHookContext<ContextT>, ResponseT> => {
return async (ctx, next) => {
// TODO: Remove dev feature guard
const { isDevFeaturesEnabled } = EnvSet.values;
if (!isDevFeaturesEnabled) {
return next();
}

const {
header: { 'user-agent': userAgent },
ip,
Expand Down
12 changes: 2 additions & 10 deletions packages/core/src/routes/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
hookEventGuard,
hookEventsGuard,
hookResponseGuard,
interactionHookEventGuard,
type Hook,
type HookResponse,
} from '@logto/schemas';
Expand All @@ -15,7 +14,6 @@ import { conditional, deduplicate, yes } from '@silverhand/essentials';
import { subDays } from 'date-fns';
import { z } from 'zod';

import { EnvSet } from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import koaPagination from '#src/middleware/koa-pagination.js';
Expand All @@ -25,12 +23,7 @@ import assertThat from '#src/utils/assert-that.js';

import type { ManagementApiRouter, RouterInitArgs } from './types.js';

const { isDevFeaturesEnabled } = EnvSet.values;
// TODO: remove dev features guard
const webhookEventsGuard = isDevFeaturesEnabled
? hookEventsGuard
: interactionHookEventGuard.array();
const nonemptyUniqueHookEventsGuard = webhookEventsGuard
const nonemptyUniqueHookEventsGuard = hookEventsGuard
.nonempty()
.transform((events) => deduplicate(events));

Expand Down Expand Up @@ -167,8 +160,7 @@ export default function hookRoutes<T extends ManagementApiRouter>(
koaQuotaGuard({ key: 'hooksLimit', quota }),
koaGuard({
body: Hooks.createGuard.omit({ id: true, signingKey: true }).extend({
// TODO: remove dev features guard
event: (isDevFeaturesEnabled ? hookEventGuard : interactionHookEventGuard).optional(),
event: hookEventGuard.optional(),
events: nonemptyUniqueHookEventsGuard.optional(),
}),
response: Hooks.guard,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { userInfoSelectFields, type DataHookEvent, type User } from '@logto/schemas';
import { conditional, conditionalString, noop, pick, trySafe } from '@silverhand/essentials';
import { conditional, conditionalString, pick, trySafe } from '@silverhand/essentials';
import type { MiddlewareType } from 'koa';
import type { IRouterParamContext } from 'koa-router';

import { EnvSet } from '#src/env-set/index.js';
import {
DataHookContextManager,
InteractionHookContextManager,
Expand Down Expand Up @@ -41,7 +40,6 @@ export default function koaInteractionHooks<
hooks: { triggerInteractionHooks, triggerDataHooks },
}: Libraries): MiddlewareType<StateT, WithInteractionHooksContext<ContextT>, ResponseT> {
return async (ctx, next) => {
const { isDevFeaturesEnabled } = EnvSet.values;
const { event: interactionEvent } = getInteractionStorage(ctx.interactionDetails.result);

const {
Expand Down Expand Up @@ -71,7 +69,7 @@ export default function koaInteractionHooks<
});

// Assign user and event data to the data hook context
const assignDataHookContext: AssignDataHookContext = ({ event, user, data: extraData }) => {
ctx.assignDataHookContext = ({ event, user, data: extraData }) => {
dataHookContext.appendContext({
event,
data: {
Expand All @@ -82,18 +80,14 @@ export default function koaInteractionHooks<
});
};

// TODO: remove dev features check
ctx.assignDataHookContext = isDevFeaturesEnabled ? assignDataHookContext : noop;

await next();

if (interactionHookContext.interactionHookResult) {
// Hooks should not crash the app
void trySafe(triggerInteractionHooks(getConsoleLogFromContext(ctx), interactionHookContext));
}

// TODO: remove dev features check
if (isDevFeaturesEnabled && dataHookContext.contextArray.length > 0) {
if (dataHookContext.contextArray.length > 0) {
// Hooks should not crash the app
void trySafe(triggerDataHooks(getConsoleLogFromContext(ctx), dataHookContext));
}
Expand Down
9 changes: 1 addition & 8 deletions packages/core/src/routes/organization/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import { generateStandardId } from '@logto/shared';
import { z } from 'zod';

import { EnvSet } from '#src/env-set/index.js';
import { buildManagementApiContext } from '#src/libraries/hook/utils.js';
import koaGuard from '#src/middleware/koa-guard.js';
import koaPagination from '#src/middleware/koa-pagination.js';
Expand Down Expand Up @@ -112,17 +111,11 @@
);
}

const { isDevFeaturesEnabled } = EnvSet.values;

ctx.body = role;
ctx.status = 201;

// Trigger `OrganizationRole.Scope.Updated` event if organizationScopeIds or resourceScopeIds are provided.
// TODO: remove dev feature guard
if (
isDevFeaturesEnabled &&
(organizationScopeIds.length > 0 || resourceScopeIds.length > 0)
) {
if (organizationScopeIds.length > 0 || resourceScopeIds.length > 0) {

Check warning on line 118 in packages/core/src/routes/organization/roles.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/roles.ts#L118

Added line #L118 was not covered by tests
ctx.appendDataHookContext({
event: 'OrganizationRole.Scopes.Updated',
...buildManagementApiContext(ctx),
Expand Down
28 changes: 11 additions & 17 deletions packages/core/src/routes/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { generateStandardId } from '@logto/shared';
import { pickState, trySafe, tryThat } from '@silverhand/essentials';
import { number, object, string, z } from 'zod';

import { EnvSet } from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import { buildManagementApiContext } from '#src/libraries/hook/utils.js';
import koaGuard from '#src/middleware/koa-guard.js';
Expand Down Expand Up @@ -176,23 +175,18 @@ export default function roleRoutes<T extends ManagementApiRouter>(
scopeIds.map((scopeId) => ({ id: generateStandardId(), roleId: role.id, scopeId }))
);

const { isDevFeaturesEnabled } = EnvSet.values;

// TODO: Remove dev feature guard
if (isDevFeaturesEnabled) {
// Trigger the `Role.Scopes.Updated` event if scopeIds are provided. Should not break the request
await trySafe(async () => {
// Align the response type with POST /roles/:id/scopes
const newRolesScopes = await findScopesByIds(scopeIds);

ctx.appendDataHookContext({
event: 'Role.Scopes.Updated',
...buildManagementApiContext(ctx),
roleId: role.id,
data: newRolesScopes,
});
// Trigger the `Role.Scopes.Updated` event if scopeIds are provided. Should not break the request
await trySafe(async () => {
// Align the response type with POST /roles/:id/scopes
const newRolesScopes = await findScopesByIds(scopeIds);

ctx.appendDataHookContext({
event: 'Role.Scopes.Updated',
...buildManagementApiContext(ctx),
roleId: role.id,
data: newRolesScopes,
});
}
});
}

ctx.body = role;
Expand Down
Loading