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:
parent
7f5625d1f8
commit
b5104d8c19
9 changed files with 103 additions and 66 deletions
79
.changeset/hip-fireants-talk.md
Normal file
79
.changeset/hip-fireants-talk.md
Normal 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 |
|
|
@ -2,7 +2,6 @@ import { type Hook, type HookConfig, type HookEvent } from '@logto/schemas';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
|
||||||
import {
|
import {
|
||||||
dataHookEventsLabel,
|
dataHookEventsLabel,
|
||||||
interactionHookEvents,
|
interactionHookEvents,
|
||||||
|
@ -18,15 +17,12 @@ import { uriValidator } from '@/utils/validator';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
const hookEventGroups: Array<CheckboxOptionGroup<HookEvent>> = [
|
const hookEventGroups: Array<CheckboxOptionGroup<HookEvent>> = [
|
||||||
// TODO: Remove dev feature guard
|
...schemaGroupedDataHookEvents.map(([schema, events]) => ({
|
||||||
...(isDevFeaturesEnabled
|
title: dataHookEventsLabel[schema],
|
||||||
? schemaGroupedDataHookEvents.map(([schema, events]) => ({
|
options: events.map((event) => ({
|
||||||
title: dataHookEventsLabel[schema],
|
value: event,
|
||||||
options: events.map((event) => ({
|
})),
|
||||||
value: event,
|
})),
|
||||||
})),
|
|
||||||
}))
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
title: 'webhooks.schemas.interaction',
|
title: 'webhooks.schemas.interaction',
|
||||||
options: interactionHookEvents.map((event) => ({
|
options: interactionHookEvents.map((event) => ({
|
||||||
|
|
|
@ -4,5 +4,6 @@ const isProduction = process.env.NODE_ENV === 'production';
|
||||||
export const isCloud = yes(process.env.IS_CLOUD);
|
export const isCloud = yes(process.env.IS_CLOUD);
|
||||||
export const adminEndpoint = process.env.ADMIN_ENDPOINT;
|
export const adminEndpoint = process.env.ADMIN_ENDPOINT;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-unused-modules
|
||||||
export const isDevFeaturesEnabled =
|
export const isDevFeaturesEnabled =
|
||||||
!isProduction || yes(process.env.DEV_FEATURES_ENABLED) || yes(process.env.INTEGRATION_TEST);
|
!isProduction || yes(process.env.DEV_FEATURES_ENABLED) || yes(process.env.INTEGRATION_TEST);
|
||||||
|
|
|
@ -8,8 +8,6 @@ import { z } from 'zod';
|
||||||
import EventSelector from '@/components/AuditLogTable/components/EventSelector';
|
import EventSelector from '@/components/AuditLogTable/components/EventSelector';
|
||||||
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
|
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
|
||||||
import { defaultPageSize } from '@/consts';
|
import { defaultPageSize } from '@/consts';
|
||||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
|
||||||
import { interactionHookEvents } from '@/consts/webhooks';
|
|
||||||
import Table from '@/ds-components/Table';
|
import Table from '@/ds-components/Table';
|
||||||
import Tag from '@/ds-components/Tag';
|
import Tag from '@/ds-components/Tag';
|
||||||
import { type RequestError } from '@/hooks/use-api';
|
import { type RequestError } from '@/hooks/use-api';
|
||||||
|
@ -22,10 +20,7 @@ import { buildHookEventLogKey, getHookEventKey } from '../utils';
|
||||||
|
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
// TODO: Remove dev feature guard
|
const hookLogEventOptions = hookEvents.map((event) => ({
|
||||||
const webhookEvents = isDevFeaturesEnabled ? hookEvents : interactionHookEvents;
|
|
||||||
|
|
||||||
const hookLogEventOptions = webhookEvents.map((event) => ({
|
|
||||||
title: event,
|
title: event,
|
||||||
value: buildHookEventLogKey(event),
|
value: buildHookEventLogKey(event),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { trySafe } from '@silverhand/essentials';
|
||||||
import { type MiddlewareType } from 'koa';
|
import { type MiddlewareType } from 'koa';
|
||||||
import { type IRouterParamContext } from 'koa-router';
|
import { type IRouterParamContext } from 'koa-router';
|
||||||
|
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
|
||||||
import { DataHookContextManager } from '#src/libraries/hook/context-manager.js';
|
import { DataHookContextManager } from '#src/libraries/hook/context-manager.js';
|
||||||
import type Libraries from '#src/tenants/Libraries.js';
|
import type Libraries from '#src/tenants/Libraries.js';
|
||||||
import { getConsoleLogFromContext } from '#src/utils/console.js';
|
import { getConsoleLogFromContext } from '#src/utils/console.js';
|
||||||
|
@ -22,12 +21,6 @@ export const koaManagementApiHooks = <StateT, ContextT extends IRouterParamConte
|
||||||
hooks: Libraries['hooks']
|
hooks: Libraries['hooks']
|
||||||
): MiddlewareType<StateT, WithHookContext<ContextT>, ResponseT> => {
|
): MiddlewareType<StateT, WithHookContext<ContextT>, ResponseT> => {
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
// TODO: Remove dev feature guard
|
|
||||||
const { isDevFeaturesEnabled } = EnvSet.values;
|
|
||||||
if (!isDevFeaturesEnabled) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
header: { 'user-agent': userAgent },
|
header: { 'user-agent': userAgent },
|
||||||
ip,
|
ip,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
hookEventGuard,
|
hookEventGuard,
|
||||||
hookEventsGuard,
|
hookEventsGuard,
|
||||||
hookResponseGuard,
|
hookResponseGuard,
|
||||||
interactionHookEventGuard,
|
|
||||||
type Hook,
|
type Hook,
|
||||||
type HookResponse,
|
type HookResponse,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
|
@ -15,7 +14,6 @@ import { conditional, deduplicate, yes } from '@silverhand/essentials';
|
||||||
import { subDays } from 'date-fns';
|
import { subDays } from 'date-fns';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
import koaGuard from '#src/middleware/koa-guard.js';
|
import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
import koaPagination from '#src/middleware/koa-pagination.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';
|
import type { ManagementApiRouter, RouterInitArgs } from './types.js';
|
||||||
|
|
||||||
const { isDevFeaturesEnabled } = EnvSet.values;
|
const nonemptyUniqueHookEventsGuard = hookEventsGuard
|
||||||
// TODO: remove dev features guard
|
|
||||||
const webhookEventsGuard = isDevFeaturesEnabled
|
|
||||||
? hookEventsGuard
|
|
||||||
: interactionHookEventGuard.array();
|
|
||||||
const nonemptyUniqueHookEventsGuard = webhookEventsGuard
|
|
||||||
.nonempty()
|
.nonempty()
|
||||||
.transform((events) => deduplicate(events));
|
.transform((events) => deduplicate(events));
|
||||||
|
|
||||||
|
@ -167,8 +160,7 @@ export default function hookRoutes<T extends ManagementApiRouter>(
|
||||||
koaQuotaGuard({ key: 'hooksLimit', quota }),
|
koaQuotaGuard({ key: 'hooksLimit', quota }),
|
||||||
koaGuard({
|
koaGuard({
|
||||||
body: Hooks.createGuard.omit({ id: true, signingKey: true }).extend({
|
body: Hooks.createGuard.omit({ id: true, signingKey: true }).extend({
|
||||||
// TODO: remove dev features guard
|
event: hookEventGuard.optional(),
|
||||||
event: (isDevFeaturesEnabled ? hookEventGuard : interactionHookEventGuard).optional(),
|
|
||||||
events: nonemptyUniqueHookEventsGuard.optional(),
|
events: nonemptyUniqueHookEventsGuard.optional(),
|
||||||
}),
|
}),
|
||||||
response: Hooks.guard,
|
response: Hooks.guard,
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { userInfoSelectFields, type DataHookEvent, type User } from '@logto/schemas';
|
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 { MiddlewareType } from 'koa';
|
||||||
import type { IRouterParamContext } from 'koa-router';
|
import type { IRouterParamContext } from 'koa-router';
|
||||||
|
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
|
||||||
import {
|
import {
|
||||||
DataHookContextManager,
|
DataHookContextManager,
|
||||||
InteractionHookContextManager,
|
InteractionHookContextManager,
|
||||||
|
@ -41,7 +40,6 @@ export default function koaInteractionHooks<
|
||||||
hooks: { triggerInteractionHooks, triggerDataHooks },
|
hooks: { triggerInteractionHooks, triggerDataHooks },
|
||||||
}: Libraries): MiddlewareType<StateT, WithInteractionHooksContext<ContextT>, ResponseT> {
|
}: Libraries): MiddlewareType<StateT, WithInteractionHooksContext<ContextT>, ResponseT> {
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
const { isDevFeaturesEnabled } = EnvSet.values;
|
|
||||||
const { event: interactionEvent } = getInteractionStorage(ctx.interactionDetails.result);
|
const { event: interactionEvent } = getInteractionStorage(ctx.interactionDetails.result);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -71,7 +69,7 @@ export default function koaInteractionHooks<
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assign user and event data to the data hook context
|
// 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({
|
dataHookContext.appendContext({
|
||||||
event,
|
event,
|
||||||
data: {
|
data: {
|
||||||
|
@ -82,9 +80,6 @@ export default function koaInteractionHooks<
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: remove dev features check
|
|
||||||
ctx.assignDataHookContext = isDevFeaturesEnabled ? assignDataHookContext : noop;
|
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
|
|
||||||
if (interactionHookContext.interactionHookResult) {
|
if (interactionHookContext.interactionHookResult) {
|
||||||
|
@ -92,8 +87,7 @@ export default function koaInteractionHooks<
|
||||||
void trySafe(triggerInteractionHooks(getConsoleLogFromContext(ctx), interactionHookContext));
|
void trySafe(triggerInteractionHooks(getConsoleLogFromContext(ctx), interactionHookContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove dev features check
|
if (dataHookContext.contextArray.length > 0) {
|
||||||
if (isDevFeaturesEnabled && dataHookContext.contextArray.length > 0) {
|
|
||||||
// Hooks should not crash the app
|
// Hooks should not crash the app
|
||||||
void trySafe(triggerDataHooks(getConsoleLogFromContext(ctx), dataHookContext));
|
void trySafe(triggerDataHooks(getConsoleLogFromContext(ctx), dataHookContext));
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import {
|
||||||
import { generateStandardId } from '@logto/shared';
|
import { generateStandardId } from '@logto/shared';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
|
||||||
import { buildManagementApiContext } from '#src/libraries/hook/utils.js';
|
import { buildManagementApiContext } from '#src/libraries/hook/utils.js';
|
||||||
import koaGuard from '#src/middleware/koa-guard.js';
|
import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
import koaPagination from '#src/middleware/koa-pagination.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.body = role;
|
||||||
ctx.status = 201;
|
ctx.status = 201;
|
||||||
|
|
||||||
// Trigger `OrganizationRole.Scope.Updated` event if organizationScopeIds or resourceScopeIds are provided.
|
// Trigger `OrganizationRole.Scope.Updated` event if organizationScopeIds or resourceScopeIds are provided.
|
||||||
// TODO: remove dev feature guard
|
if (organizationScopeIds.length > 0 || resourceScopeIds.length > 0) {
|
||||||
if (
|
|
||||||
isDevFeaturesEnabled &&
|
|
||||||
(organizationScopeIds.length > 0 || resourceScopeIds.length > 0)
|
|
||||||
) {
|
|
||||||
ctx.appendDataHookContext({
|
ctx.appendDataHookContext({
|
||||||
event: 'OrganizationRole.Scopes.Updated',
|
event: 'OrganizationRole.Scopes.Updated',
|
||||||
...buildManagementApiContext(ctx),
|
...buildManagementApiContext(ctx),
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { generateStandardId } from '@logto/shared';
|
||||||
import { pickState, trySafe, tryThat } from '@silverhand/essentials';
|
import { pickState, trySafe, tryThat } from '@silverhand/essentials';
|
||||||
import { number, object, string, z } from 'zod';
|
import { number, object, string, z } from 'zod';
|
||||||
|
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
|
||||||
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 { buildManagementApiContext } from '#src/libraries/hook/utils.js';
|
||||||
import koaGuard from '#src/middleware/koa-guard.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 }))
|
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
|
ctx.appendDataHookContext({
|
||||||
if (isDevFeaturesEnabled) {
|
event: 'Role.Scopes.Updated',
|
||||||
// Trigger the `Role.Scopes.Updated` event if scopeIds are provided. Should not break the request
|
...buildManagementApiContext(ctx),
|
||||||
await trySafe(async () => {
|
roleId: role.id,
|
||||||
// Align the response type with POST /roles/:id/scopes
|
data: newRolesScopes,
|
||||||
const newRolesScopes = await findScopesByIds(scopeIds);
|
|
||||||
|
|
||||||
ctx.appendDataHookContext({
|
|
||||||
event: 'Role.Scopes.Updated',
|
|
||||||
...buildManagementApiContext(ctx),
|
|
||||||
roleId: role.id,
|
|
||||||
data: newRolesScopes,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = role;
|
ctx.body = role;
|
||||||
|
|
Loading…
Reference in a new issue