Files
speckle-server/packages/server/modules/webhooks/services/webhooks.ts
T
Gergő Jedlicska 73cc7e67d3 gergo/webhookRegions (#3459)
* feat(webhooks): multi region webhook resolver

* feat(webhooks): multi region webhook cleanup

* fix(webhooks): DI fixes

* feat(activitystream): region aware save activity

* feat(accessrequests): multi region

* feat(cli): allow multi region project and commit download

* feat(postgres): make docker postgres 0 day multi region ready

* feat(cli): allow multi region project and commit download properly

* fix(cross-server-sync): di fix

* feat(activitystream): non region aware activities, they are not project data

* fix(webhooks): triggers need to be included

* feat(stream/projectCreate): activity save is not needed any more, its all event based

* feat(multiRegion): get all registered db clients

* fix(regions): test equal in any order

* fix(projectDownload): need to await
2024-11-08 10:45:39 +01:00

174 lines
5.0 KiB
TypeScript

import { ForbiddenError } from '@/modules/shared/errors'
import {
CountWebhooksByStreamId,
CreateWebhookConfig,
CreateWebhookEvent,
DeleteWebhookConfig,
GetStreamWebhooks,
GetWebhookById,
UpdateWebhookConfig
} from '@/modules/webhooks/domain/operations'
import { Webhook } from '@/modules/webhooks/domain/types'
import { SetValuesNullable } from '@speckle/shared'
import crs from 'crypto-random-string'
import { StreamWithOptionalRole } from '@/modules/core/repositories/streams'
import { ServerInfo } from '@/modules/core/helpers/types'
import { GetStream } from '@/modules/core/domain/streams/operations'
import { UserWithOptionalRole } from '@/modules/core/domain/users/types'
import { GetUser } from '@/modules/core/domain/users/operations'
import { GetServerInfo } from '@/modules/core/domain/server/operations'
import { getServerOrigin } from '@/modules/shared/helpers/envHelper'
const MAX_STREAM_WEBHOOKS = 100
export const createWebhookFactory =
({
createWebhookConfig,
countWebhooksByStreamId
}: {
createWebhookConfig: CreateWebhookConfig
countWebhooksByStreamId: CountWebhooksByStreamId
}) =>
async ({
streamId,
url,
description,
secret,
enabled,
triggers
}: Pick<Webhook, 'streamId' | 'enabled' | 'triggers'> &
Partial<SetValuesNullable<Pick<Webhook, 'url' | 'description' | 'secret'>>>) => {
const streamWebhookCount = await countWebhooksByStreamId({ streamId })
if (streamWebhookCount >= MAX_STREAM_WEBHOOKS) {
throw new Error(
`Maximum number of webhooks for a stream reached (${MAX_STREAM_WEBHOOKS})`
)
}
return await createWebhookConfig({
id: crs({ length: 10 }),
streamId,
url: url ?? undefined,
description: description ?? undefined,
secret: secret ?? undefined,
enabled,
triggers
})
}
export const updateWebhookFactory =
({ updateWebhookConfig }: { updateWebhookConfig: UpdateWebhookConfig }) =>
async (
webhook: Pick<Webhook, 'id'> &
Partial<SetValuesNullable<Omit<Webhook, 'id' | 'updatedAt'>>>
) => {
const { id, streamId, url, description, secret, enabled, triggers } = webhook
return await updateWebhookConfig({
webhookId: id,
webhookInput: {
streamId: streamId ?? undefined,
url: url ?? undefined,
description: description ?? undefined,
secret: secret ?? undefined,
enabled: enabled ?? undefined,
triggers: triggers ?? undefined,
updatedAt: new Date()
}
})
}
export const deleteWebhookFactory =
({
deleteWebhookConfig,
getWebhookById
}: {
deleteWebhookConfig: DeleteWebhookConfig
getWebhookById: GetWebhookById
}) =>
async ({ id, streamId }: { id: string; streamId: string }) => {
const wh = await getWebhookById({ id })
if (streamId !== wh?.streamId)
throw new ForbiddenError(
'The webhook id and stream id do not match. Please check your inputs.'
)
await deleteWebhookConfig({ id })
return id
}
export const dispatchStreamEventFactory =
({
getServerInfo,
getStream,
createWebhookEvent,
getStreamWebhooks,
getUser
}: {
getServerInfo: GetServerInfo
getStreamWebhooks: GetStreamWebhooks
getStream: GetStream
createWebhookEvent: CreateWebhookEvent
getUser: GetUser
}) =>
async ({
streamId,
event,
eventPayload
}: {
streamId: string
event: string
eventPayload: {
streamId?: string
stream?: StreamWithOptionalRole
userId?: string | null
user?: Partial<UserWithOptionalRole> | null
}
}) => {
const payload: typeof eventPayload & {
server: Partial<Omit<ServerInfo, 'secret'>>
} = {
...eventPayload,
server: { ...(await getServerInfo()), canonicalUrl: getServerOrigin() }
}
// Add server info
payload.server = await getServerInfo()
payload.server.canonicalUrl = getServerOrigin()
delete payload.server.id
// Add stream info
if (payload.streamId) {
payload.stream = await getStream({
streamId: payload.streamId,
userId: payload.userId ?? undefined
})
}
// Add user info (except email and pwd)
if (payload.userId) {
payload.user = await getUser(payload.userId)
if (payload.user) {
delete payload.user.passwordDigest
delete payload.user.email
}
}
// with this select, we must have the streamid available on the webhook config,
// even when the stream is deleted, to dispatch the stream deleted webhook events
const rows = await getStreamWebhooks({ streamId })
for (const wh of rows) {
if (!wh.enabled) continue
if (!wh.triggers.includes(event)) continue
// Add webhook info (the key `webhook` will be replaced for each webhook configured, before serializing the payload and storing it)
wh.triggers = Object.keys(wh.triggers)
delete wh.secret
await createWebhookEvent({
id: crs({ length: 20 }),
webhookId: wh.id,
payload: JSON.stringify({ ...payload, webhook: wh })
})
}
}