0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

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

* feat(console,core): remove DataHook devFeature guard

remove DataHook devFeature guard

* chore: add changeset

add changeset

* chore: update changesets

update changesets
This commit is contained in:
simeng-li 2024-05-22 11:20:09 +08:00 committed by GitHub
parent 7f5625d1f8
commit b5104d8c19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 103 additions and 66 deletions

View file

@ -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 |

View file

@ -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,
@ -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) => ({

View file

@ -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);

View file

@ -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';
@ -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),
}));

View file

@ -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';
@ -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,

View file

@ -6,7 +6,6 @@ import {
hookEventGuard,
hookEventsGuard,
hookResponseGuard,
interactionHookEventGuard,
type Hook,
type HookResponse,
} from '@logto/schemas';
@ -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';
@ -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));
@ -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,

View file

@ -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,
@ -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 {
@ -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: {
@ -82,9 +80,6 @@ export default function koaInteractionHooks<
});
};
// TODO: remove dev features check
ctx.assignDataHookContext = isDevFeaturesEnabled ? assignDataHookContext : noop;
await next();
if (interactionHookContext.interactionHookResult) {
@ -92,8 +87,7 @@ export default function koaInteractionHooks<
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));
}

View file

@ -8,7 +8,6 @@ import {
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';
@ -112,17 +111,11 @@ export default function organizationRoleRoutes<T extends ManagementApiRouter>(
);
}
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) {
ctx.appendDataHookContext({
event: 'OrganizationRole.Scopes.Updated',
...buildManagementApiContext(ctx),

View file

@ -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';
@ -176,23 +175,18 @@ export default function roleRoutes<T extends ManagementApiRouter>(
scopeIds.map((scopeId) => ({ id: generateStandardId(), roleId: role.id, scopeId }))
);
const { isDevFeaturesEnabled } = EnvSet.values;
// 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);
// 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,
});
ctx.appendDataHookContext({
event: 'Role.Scopes.Updated',
...buildManagementApiContext(ctx),
roleId: role.id,
data: newRolesScopes,
});
}
});
}
ctx.body = role;