betterstart-cli 0.0.27 → 0.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/adapters/next/integrations/mailchimp/actions/mailchimp.ts +0 -1
- package/dist/assets/adapters/next/integrations/vercel-blob/actions/vercel-blob.ts +39 -0
- package/dist/assets/adapters/next/integrations/vercel-blob/integration.ts +36 -0
- package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +1 -1
- package/dist/assets/adapters/next/plugins/blog/schemas/posts.json +1 -1
- package/dist/assets/adapters/next/templates/init/admin-globals.css +1 -1
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-nav-link.tsx +12 -2
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-settings-sidebar.tsx +75 -0
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-nav-link.tsx +12 -2
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar.tsx +2 -8
- package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid.tsx +14 -5
- package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx +111 -112
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/dev-mode-types.ts +4 -0
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode-integrate.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-drawer.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +45 -47
- package/dist/assets/adapters/next/templates/init/components/shared/page-header.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/data/webhook-events.ts +5 -0
- package/dist/assets/adapters/next/templates/init/hooks/use-table-utils.ts +27 -7
- package/dist/assets/adapters/next/templates/init/hooks/use-webhooks.ts +211 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/types.ts +2 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/forms/index.ts +0 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/forms/types.ts +0 -4
- package/dist/assets/adapters/next/templates/init/lib/actions/forms/upsert-form-settings.ts +0 -2
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/create-webhook.ts +55 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/delete-webhook.ts +26 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/dispatch.ts +135 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-deliveries.ts +124 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-secret.ts +28 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-subscriptions.ts +32 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhooks.ts +154 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/index.ts +30 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/internal-get-active-endpoints.ts +20 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/regenerate-webhook-secret.ts +35 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/register.ts +70 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/send-test-webhook.ts +81 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/types.ts +104 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/update-webhook.ts +72 -0
- package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +71 -2
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/register.ts +2 -0
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +3 -1
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +6 -1
- package/dist/assets/adapters/next/templates/init/pages/auth-gate-rsc.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/media/media-page-content.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/media/media-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-columns.tsx +150 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-page-content.tsx +137 -0
- package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-table.tsx +295 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/edit-form-notifications-dialog.tsx +128 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page-content.tsx +111 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page-skeleton.tsx +7 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page.tsx +14 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/settings-layout.tsx +10 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/delete-webhook-dialog.tsx +82 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-actions.tsx +53 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-enabled-switch.tsx +18 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-endpoint-dialog.tsx +370 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-columns.tsx +258 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-columns.tsx +200 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-page-content.tsx +117 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-page.tsx +14 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-table.tsx +253 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page-content.tsx +161 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page-skeleton.tsx +7 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page.tsx +14 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-table.tsx +264 -0
- package/dist/assets/adapters/next/templates/init/pages/users/users-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +103 -109
- package/dist/assets/adapters/next/templates/init/types/webhooks.ts +7 -0
- package/dist/assets/adapters/next/templates/init/utils/webhook/signature.ts +13 -0
- package/dist/assets/shared-assets/react-admin/ui/card.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +1 -1
- package/dist/cli.js +2191 -1304
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/forms/test-form-webhook.ts +0 -40
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +0 -550
- package/dist/assets/adapters/next/templates/init/utils/webhook/webhook.ts +0 -28
- /package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-detail-drawer.tsx +0 -0
- /package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-detail-row.tsx +0 -0
- /package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-page.tsx +0 -0
package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { Card, CardContent } from '@admin/components/ui/card'
|
|
3
4
|
import {
|
|
4
5
|
InputGroup,
|
|
5
6
|
InputGroupAddon,
|
|
@@ -88,52 +89,49 @@ export function MediaUrlImporter({ onImportFromUrl, accept, className }: MediaUr
|
|
|
88
89
|
}, [isImporting, onImportFromUrl, urlValue])
|
|
89
90
|
|
|
90
91
|
return (
|
|
91
|
-
<
|
|
92
|
-
className=
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
</InputGroupAddon>
|
|
136
|
-
</InputGroup>
|
|
137
|
-
</div>
|
|
92
|
+
<Card className={cn('flex items-center justify-center', className)}>
|
|
93
|
+
<CardContent className="w-full h-full">
|
|
94
|
+
<div className="flex w-full flex-col h-full justify-center items-center gap-3 text-center">
|
|
95
|
+
<InputGroup className="bg-background mx-auto max-w-2xl">
|
|
96
|
+
<InputGroupInput
|
|
97
|
+
type="url"
|
|
98
|
+
name="url"
|
|
99
|
+
aria-label="Media URL"
|
|
100
|
+
value={urlValue}
|
|
101
|
+
placeholder={urlPlaceholder}
|
|
102
|
+
autoComplete="off"
|
|
103
|
+
spellCheck={false}
|
|
104
|
+
onChange={(event) => {
|
|
105
|
+
setUrlValue(event.target.value)
|
|
106
|
+
if (urlError) setUrlError(null)
|
|
107
|
+
}}
|
|
108
|
+
onKeyDown={(event) => {
|
|
109
|
+
if (event.key === 'Enter') {
|
|
110
|
+
event.preventDefault()
|
|
111
|
+
void handleImport()
|
|
112
|
+
}
|
|
113
|
+
}}
|
|
114
|
+
disabled={isImporting}
|
|
115
|
+
/>
|
|
116
|
+
<InputGroupAddon align="inline-end">
|
|
117
|
+
<InputGroupButton
|
|
118
|
+
type="button"
|
|
119
|
+
variant="outline"
|
|
120
|
+
onClick={() => void handleImport()}
|
|
121
|
+
disabled={isImporting}
|
|
122
|
+
className="h-full px-5"
|
|
123
|
+
>
|
|
124
|
+
{isImporting ? <LoaderCircle className="animate-spin" /> : <Upload />}
|
|
125
|
+
Import
|
|
126
|
+
</InputGroupButton>
|
|
127
|
+
</InputGroupAddon>
|
|
128
|
+
</InputGroup>
|
|
129
|
+
|
|
130
|
+
<div className="flex w-full flex-col items-center text-center">
|
|
131
|
+
<p className="text-sm text-muted-foreground">{urlHelpText}</p>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</CardContent>
|
|
135
|
+
</Card>
|
|
138
136
|
)
|
|
139
137
|
}
|
|
@@ -23,7 +23,7 @@ export function PageHeader({
|
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
25
|
<>
|
|
26
|
-
<header className="fixed top-0 border-t right-0 left-(--admin-shell-content-left)
|
|
26
|
+
<header className="fixed z-200 top-0 border-t right-0 left-(--admin-shell-content-left) grid h-14 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center border-b border-border/80 px-4 bg-sidebar transition-[left] duration-200 ease-linear">
|
|
27
27
|
<div className="flex w-full items-center justify-start gap-2">
|
|
28
28
|
{hasBackButton && (
|
|
29
29
|
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
|
@@ -174,15 +174,35 @@ export function useTableUtils<TData extends RowData>({
|
|
|
174
174
|
setColumnSizing((current) => {
|
|
175
175
|
const next: ColumnSizingState = {}
|
|
176
176
|
let changed = Object.keys(current).length !== baseColumns.length
|
|
177
|
+
const targetTotal = Math.round(Math.max(containerWidth, fixedTotal + flexibleMinTotal))
|
|
178
|
+
const flexibleSizing = flexibleColumns.map((column) => {
|
|
179
|
+
const columnShrinkRoom = Math.max(column.size - column.minSize, 0)
|
|
180
|
+
const shrink = shrinkRoom > 0 ? shrinkNeeded * (columnShrinkRoom / shrinkRoom) : 0
|
|
181
|
+
const grow = extraPerColumn
|
|
182
|
+
return {
|
|
183
|
+
id: column.id,
|
|
184
|
+
size: Math.round(column.size + grow - shrink)
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
const flexibleSizingById = new Map(flexibleSizing.map((column) => [column.id, column.size]))
|
|
188
|
+
const roundedFixedTotal = baseColumns
|
|
189
|
+
.filter((column) => !flexibleColumnIds.has(column.id))
|
|
190
|
+
.reduce((total, column) => total + Math.round(column.size), 0)
|
|
191
|
+
const roundedFlexibleTotal = flexibleSizing.reduce((total, column) => total + column.size, 0)
|
|
192
|
+
const roundingDelta = targetTotal - roundedFixedTotal - roundedFlexibleTotal
|
|
193
|
+
const lastFlexibleColumn = flexibleSizing.at(-1)
|
|
194
|
+
|
|
195
|
+
if (lastFlexibleColumn) {
|
|
196
|
+
flexibleSizingById.set(
|
|
197
|
+
lastFlexibleColumn.id,
|
|
198
|
+
Math.max(lastFlexibleColumn.size + roundingDelta, 0)
|
|
199
|
+
)
|
|
200
|
+
}
|
|
177
201
|
|
|
178
202
|
for (const column of baseColumns) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
? shrinkNeeded * (columnShrinkRoom / shrinkRoom)
|
|
183
|
-
: 0
|
|
184
|
-
const grow = flexibleColumnIds.has(column.id) ? extraPerColumn : 0
|
|
185
|
-
const size = Math.round(column.size + grow - shrink)
|
|
203
|
+
const size = flexibleColumnIds.has(column.id)
|
|
204
|
+
? (flexibleSizingById.get(column.id) ?? Math.round(column.size))
|
|
205
|
+
: Math.round(column.size)
|
|
186
206
|
next[column.id] = size
|
|
187
207
|
if (current[column.id] !== size) {
|
|
188
208
|
changed = true
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { FormSettingsData } from '@admin/actions/forms'
|
|
4
|
+
import { getAllFormSettings } from '@admin/actions/forms'
|
|
5
|
+
import type {
|
|
6
|
+
WebhookDeliveriesPage,
|
|
7
|
+
WebhookDeliveriesQuery,
|
|
8
|
+
WebhookSubscriptionEntry,
|
|
9
|
+
WebhooksPage,
|
|
10
|
+
WebhooksQuery
|
|
11
|
+
} from '@admin/actions/webhooks'
|
|
12
|
+
import {
|
|
13
|
+
getWebhookDeliveries,
|
|
14
|
+
getWebhookSubscriptions,
|
|
15
|
+
getWebhooks,
|
|
16
|
+
sendTestWebhook,
|
|
17
|
+
updateWebhook
|
|
18
|
+
} from '@admin/actions/webhooks'
|
|
19
|
+
import { type UseQueryResult, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
20
|
+
import { toast } from 'sonner'
|
|
21
|
+
|
|
22
|
+
export function useWebhooks(
|
|
23
|
+
search?: string,
|
|
24
|
+
enabled?: string,
|
|
25
|
+
lastDelivery?: string,
|
|
26
|
+
createdAtFrom?: string,
|
|
27
|
+
createdAtTo?: string,
|
|
28
|
+
page?: number,
|
|
29
|
+
pageSize?: number,
|
|
30
|
+
orderBy?: string,
|
|
31
|
+
orderDirection?: 'asc' | 'desc',
|
|
32
|
+
options?: { enabled?: boolean }
|
|
33
|
+
): UseQueryResult<WebhooksPage, Error> {
|
|
34
|
+
const safePage = Number.isFinite(page ?? 0) && (page ?? 0) > 0 ? (page ?? 0) : 0
|
|
35
|
+
const safePageSize =
|
|
36
|
+
pageSize === -1 || (Number.isFinite(pageSize ?? 20) && (pageSize ?? 20) > 0)
|
|
37
|
+
? (pageSize ?? 20)
|
|
38
|
+
: 20
|
|
39
|
+
|
|
40
|
+
return useQuery({
|
|
41
|
+
queryKey: [
|
|
42
|
+
'webhooks',
|
|
43
|
+
search ?? '',
|
|
44
|
+
enabled ?? '',
|
|
45
|
+
lastDelivery ?? '',
|
|
46
|
+
createdAtFrom ?? '',
|
|
47
|
+
createdAtTo ?? '',
|
|
48
|
+
safePage,
|
|
49
|
+
safePageSize,
|
|
50
|
+
orderBy ?? '',
|
|
51
|
+
orderDirection ?? ''
|
|
52
|
+
],
|
|
53
|
+
queryFn: () => {
|
|
54
|
+
const filters: WebhooksQuery = {}
|
|
55
|
+
if (search) filters.search = search
|
|
56
|
+
if (enabled === 'enabled') filters.enabled = true
|
|
57
|
+
if (enabled === 'disabled') filters.enabled = false
|
|
58
|
+
if (lastDelivery === 'delivered' || lastDelivery === 'failed' || lastDelivery === 'never') {
|
|
59
|
+
filters.lastDelivery = lastDelivery
|
|
60
|
+
}
|
|
61
|
+
if (createdAtFrom) filters.createdAtFrom = createdAtFrom
|
|
62
|
+
if (createdAtTo) filters.createdAtTo = createdAtTo
|
|
63
|
+
if (safePageSize !== -1) {
|
|
64
|
+
filters.limit = safePageSize
|
|
65
|
+
filters.offset = safePage * safePageSize
|
|
66
|
+
}
|
|
67
|
+
if (orderBy) filters.orderBy = orderBy
|
|
68
|
+
if (orderDirection) filters.orderDirection = orderDirection
|
|
69
|
+
return getWebhooks(Object.keys(filters).length > 0 ? filters : undefined)
|
|
70
|
+
},
|
|
71
|
+
enabled: options?.enabled ?? true
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function useWebhookOptions(): UseQueryResult<WebhooksPage, Error> {
|
|
76
|
+
return useQuery({
|
|
77
|
+
queryKey: ['webhooks', 'options'],
|
|
78
|
+
queryFn: () => getWebhooks()
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function useToggleWebhookEnabled() {
|
|
83
|
+
const queryClient = useQueryClient()
|
|
84
|
+
|
|
85
|
+
return useMutation({
|
|
86
|
+
mutationFn: async (input: { id: string; enabled: boolean }) => {
|
|
87
|
+
const result = await updateWebhook(input.id, { enabled: input.enabled })
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
throw new Error(result.error || 'Failed to update webhook')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result
|
|
93
|
+
},
|
|
94
|
+
onMutate: async (input) => {
|
|
95
|
+
await queryClient.cancelQueries({ queryKey: ['webhooks'] })
|
|
96
|
+
const previous = queryClient.getQueriesData<WebhooksPage>({
|
|
97
|
+
queryKey: ['webhooks']
|
|
98
|
+
})
|
|
99
|
+
queryClient.setQueriesData<WebhooksPage>({ queryKey: ['webhooks'] }, (page) =>
|
|
100
|
+
page
|
|
101
|
+
? {
|
|
102
|
+
...page,
|
|
103
|
+
webhooks: page.webhooks.map((webhook) =>
|
|
104
|
+
webhook.id === input.id ? { ...webhook, enabled: input.enabled } : webhook
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
: page
|
|
108
|
+
)
|
|
109
|
+
return { previous }
|
|
110
|
+
},
|
|
111
|
+
onError: (error: Error, _input, context) => {
|
|
112
|
+
for (const [queryKey, data] of context?.previous ?? []) {
|
|
113
|
+
queryClient.setQueryData(queryKey, data)
|
|
114
|
+
}
|
|
115
|
+
toast.error(error.message || 'Failed to update webhook')
|
|
116
|
+
},
|
|
117
|
+
onSettled: async () => {
|
|
118
|
+
await queryClient.invalidateQueries({ queryKey: ['webhooks'] })
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function useSendTestWebhook() {
|
|
124
|
+
const queryClient = useQueryClient()
|
|
125
|
+
|
|
126
|
+
return useMutation({
|
|
127
|
+
mutationFn: async (id: string) => {
|
|
128
|
+
const result = await sendTestWebhook(id)
|
|
129
|
+
if (!result.success) {
|
|
130
|
+
throw new Error(result.error || 'Test webhook failed')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result
|
|
134
|
+
},
|
|
135
|
+
onSuccess: async (result) => {
|
|
136
|
+
toast.success(
|
|
137
|
+
result.statusCode ? `Test delivered (status ${result.statusCode})` : 'Test delivered'
|
|
138
|
+
)
|
|
139
|
+
await queryClient.invalidateQueries({ queryKey: ['webhook-deliveries'] })
|
|
140
|
+
await queryClient.invalidateQueries({ queryKey: ['webhooks'] })
|
|
141
|
+
},
|
|
142
|
+
onError: async (error: Error) => {
|
|
143
|
+
toast.error(error.message || 'Test webhook failed')
|
|
144
|
+
await queryClient.invalidateQueries({ queryKey: ['webhook-deliveries'] })
|
|
145
|
+
await queryClient.invalidateQueries({ queryKey: ['webhooks'] })
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function useWebhookSubscriptions() {
|
|
151
|
+
return useQuery<WebhookSubscriptionEntry[]>({
|
|
152
|
+
queryKey: ['webhook-subscriptions'],
|
|
153
|
+
queryFn: () => getWebhookSubscriptions()
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function useWebhookDeliveries(
|
|
158
|
+
search?: string,
|
|
159
|
+
status?: string,
|
|
160
|
+
endpoint?: string,
|
|
161
|
+
createdAtFrom?: string,
|
|
162
|
+
createdAtTo?: string,
|
|
163
|
+
page?: number,
|
|
164
|
+
pageSize?: number,
|
|
165
|
+
orderBy?: string,
|
|
166
|
+
orderDirection?: 'asc' | 'desc'
|
|
167
|
+
): UseQueryResult<WebhookDeliveriesPage, Error> {
|
|
168
|
+
const safePage = Number.isFinite(page ?? 0) && (page ?? 0) > 0 ? (page ?? 0) : 0
|
|
169
|
+
const safePageSize =
|
|
170
|
+
pageSize === -1 || (Number.isFinite(pageSize ?? 20) && (pageSize ?? 20) > 0)
|
|
171
|
+
? (pageSize ?? 20)
|
|
172
|
+
: 20
|
|
173
|
+
|
|
174
|
+
return useQuery({
|
|
175
|
+
queryKey: [
|
|
176
|
+
'webhook-deliveries',
|
|
177
|
+
search ?? '',
|
|
178
|
+
status ?? '',
|
|
179
|
+
endpoint ?? '',
|
|
180
|
+
createdAtFrom ?? '',
|
|
181
|
+
createdAtTo ?? '',
|
|
182
|
+
safePage,
|
|
183
|
+
safePageSize,
|
|
184
|
+
orderBy ?? '',
|
|
185
|
+
orderDirection ?? ''
|
|
186
|
+
],
|
|
187
|
+
queryFn: () => {
|
|
188
|
+
const filters: WebhookDeliveriesQuery = {}
|
|
189
|
+
if (search) filters.search = search
|
|
190
|
+
if (status === 'success') filters.success = true
|
|
191
|
+
if (status === 'failed') filters.success = false
|
|
192
|
+
if (endpoint) filters.webhookId = endpoint
|
|
193
|
+
if (createdAtFrom) filters.createdAtFrom = createdAtFrom
|
|
194
|
+
if (createdAtTo) filters.createdAtTo = createdAtTo
|
|
195
|
+
if (safePageSize !== -1) {
|
|
196
|
+
filters.limit = safePageSize
|
|
197
|
+
filters.offset = safePage * safePageSize
|
|
198
|
+
}
|
|
199
|
+
if (orderBy) filters.orderBy = orderBy
|
|
200
|
+
if (orderDirection) filters.orderDirection = orderDirection
|
|
201
|
+
return getWebhookDeliveries(Object.keys(filters).length > 0 ? filters : undefined)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function useFormSettingsList() {
|
|
207
|
+
return useQuery<FormSettingsData[]>({
|
|
208
|
+
queryKey: ['form-settings'],
|
|
209
|
+
queryFn: () => getAllFormSettings()
|
|
210
|
+
})
|
|
211
|
+
}
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
export interface FormSettingsData {
|
|
2
2
|
id: string
|
|
3
3
|
formName: string
|
|
4
|
-
webhookUrl: string | null
|
|
5
|
-
webhookEnabled: boolean
|
|
6
4
|
notificationEmails: string | null
|
|
7
5
|
createdAt: string
|
|
8
6
|
updatedAt: string
|
|
9
7
|
}
|
|
10
8
|
|
|
11
9
|
export interface UpsertFormSettingsInput {
|
|
12
|
-
webhookUrl?: string | null
|
|
13
|
-
webhookEnabled?: boolean
|
|
14
10
|
notificationEmails?: string | null
|
|
15
11
|
}
|
|
16
12
|
|
|
@@ -18,8 +18,6 @@ export async function upsertFormSettings(
|
|
|
18
18
|
.insert(formSettings)
|
|
19
19
|
.values({
|
|
20
20
|
formName,
|
|
21
|
-
webhookUrl: data.webhookUrl ?? null,
|
|
22
|
-
webhookEnabled: data.webhookEnabled ?? false,
|
|
23
21
|
notificationEmails: data.notificationEmails ?? null
|
|
24
22
|
})
|
|
25
23
|
.onConflictDoUpdate({
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
4
|
+
import db from '@admin/db'
|
|
5
|
+
import { webhookSubscriptions, webhooks } from '@admin/db/schema'
|
|
6
|
+
import { generateWebhookSecret } from '@admin/utils/webhook/signature'
|
|
7
|
+
import { updateTag } from 'next/cache'
|
|
8
|
+
import type { CreateWebhookInput, WebhookResult } from './types'
|
|
9
|
+
import { webhooksCacheTags } from './types'
|
|
10
|
+
|
|
11
|
+
export async function createWebhook(input: CreateWebhookInput): Promise<WebhookResult> {
|
|
12
|
+
await requireRole([UserRole.ADMIN])
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const created = await db.transaction(async (tx) => {
|
|
16
|
+
const [row] = await tx
|
|
17
|
+
.insert(webhooks)
|
|
18
|
+
.values({
|
|
19
|
+
name: input.name,
|
|
20
|
+
url: input.url,
|
|
21
|
+
secret: generateWebhookSecret(),
|
|
22
|
+
enabled: input.enabled
|
|
23
|
+
})
|
|
24
|
+
.returning()
|
|
25
|
+
|
|
26
|
+
if (input.events.length > 0) {
|
|
27
|
+
await tx
|
|
28
|
+
.insert(webhookSubscriptions)
|
|
29
|
+
.values(input.events.map((event) => ({ webhookId: row.id, event })))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return row
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
updateTag(webhooksCacheTags.endpoints)
|
|
36
|
+
updateTag(webhooksCacheTags.subscriptions)
|
|
37
|
+
return {
|
|
38
|
+
success: true,
|
|
39
|
+
webhook: {
|
|
40
|
+
id: created.id,
|
|
41
|
+
name: created.name,
|
|
42
|
+
url: created.url,
|
|
43
|
+
enabled: created.enabled,
|
|
44
|
+
createdAt: created.createdAt,
|
|
45
|
+
updatedAt: created.updatedAt
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Error creating webhook:', error)
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
error: error instanceof Error ? error.message : 'Failed to create webhook'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
4
|
+
import db from '@admin/db'
|
|
5
|
+
import { webhooks } from '@admin/db/schema'
|
|
6
|
+
import { eq } from 'drizzle-orm'
|
|
7
|
+
import { updateTag } from 'next/cache'
|
|
8
|
+
import type { WebhookResult } from './types'
|
|
9
|
+
import { webhooksCacheTags } from './types'
|
|
10
|
+
|
|
11
|
+
export async function deleteWebhook(id: string): Promise<WebhookResult> {
|
|
12
|
+
await requireRole([UserRole.ADMIN])
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await db.delete(webhooks).where(eq(webhooks.id, id))
|
|
16
|
+
updateTag(webhooksCacheTags.endpoints)
|
|
17
|
+
updateTag(webhooksCacheTags.subscriptions)
|
|
18
|
+
return { success: true }
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error('Error deleting webhook:', error)
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
error: error instanceof Error ? error.message : 'Failed to delete webhook'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import db from '@admin/db'
|
|
2
|
+
import { webhookDeliveries } from '@admin/db/schema'
|
|
3
|
+
import { createWebhookEventId, signWebhookPayload } from '@admin/utils/webhook/signature'
|
|
4
|
+
import { and, desc, eq, notInArray } from 'drizzle-orm'
|
|
5
|
+
import { after } from 'next/server'
|
|
6
|
+
import type { ActiveWebhookEndpoint } from './internal-get-active-endpoints'
|
|
7
|
+
import { getActiveEndpointsForEvent } from './internal-get-active-endpoints'
|
|
8
|
+
|
|
9
|
+
const MAX_ATTEMPTS = 3
|
|
10
|
+
const RETRY_DELAYS_MS = [1_000, 5_000]
|
|
11
|
+
const REQUEST_TIMEOUT_MS = 10_000
|
|
12
|
+
const DELIVERY_LOG_LIMIT = 100
|
|
13
|
+
|
|
14
|
+
export function emitWebhookEvent(event: string, data: unknown): void {
|
|
15
|
+
const eventId = createWebhookEventId()
|
|
16
|
+
const body = JSON.stringify({
|
|
17
|
+
id: eventId,
|
|
18
|
+
event,
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
data
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// A missing request scope must never crash the mutation that emitted the event.
|
|
24
|
+
try {
|
|
25
|
+
after(() => deliverWebhookEvent(eventId, event, body))
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('Webhook dispatch skipped (no request scope):', {
|
|
28
|
+
event,
|
|
29
|
+
error
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function deliverWebhookEvent(eventId: string, event: string, body: string): Promise<void> {
|
|
35
|
+
try {
|
|
36
|
+
const endpoints = await getActiveEndpointsForEvent(event)
|
|
37
|
+
if (endpoints.length === 0) return
|
|
38
|
+
|
|
39
|
+
await Promise.allSettled(
|
|
40
|
+
endpoints.map((endpoint) => deliverToEndpoint(endpoint, eventId, event, body))
|
|
41
|
+
)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Webhook delivery failed:', { event, error })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function deliverToEndpoint(
|
|
48
|
+
endpoint: ActiveWebhookEndpoint,
|
|
49
|
+
eventId: string,
|
|
50
|
+
event: string,
|
|
51
|
+
body: string
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
54
|
+
const startedAt = Date.now()
|
|
55
|
+
let statusCode: number | null = null
|
|
56
|
+
let errorMessage: string | null = null
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const timestamp = Math.floor(Date.now() / 1000).toString()
|
|
60
|
+
const response = await fetch(endpoint.url, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
'content-type': 'application/json',
|
|
64
|
+
'x-webhook-id': eventId,
|
|
65
|
+
'x-webhook-timestamp': timestamp,
|
|
66
|
+
'x-webhook-signature': signWebhookPayload(endpoint.secret, timestamp, body)
|
|
67
|
+
},
|
|
68
|
+
body,
|
|
69
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
70
|
+
})
|
|
71
|
+
statusCode = response.status
|
|
72
|
+
if (!response.ok) errorMessage = `Webhook returned status ${response.status}`
|
|
73
|
+
} catch (error) {
|
|
74
|
+
errorMessage = error instanceof Error ? error.message : 'Webhook request failed'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await recordWebhookDelivery({
|
|
78
|
+
webhookId: endpoint.id,
|
|
79
|
+
event,
|
|
80
|
+
eventId,
|
|
81
|
+
attempt,
|
|
82
|
+
success: errorMessage === null,
|
|
83
|
+
statusCode,
|
|
84
|
+
durationMs: Date.now() - startedAt,
|
|
85
|
+
error: errorMessage
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (errorMessage === null) return
|
|
89
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAYS_MS[attempt - 1]))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function recordWebhookDelivery(entry: {
|
|
96
|
+
webhookId: string
|
|
97
|
+
event: string
|
|
98
|
+
eventId: string
|
|
99
|
+
attempt: number
|
|
100
|
+
success: boolean
|
|
101
|
+
statusCode: number | null
|
|
102
|
+
durationMs: number
|
|
103
|
+
error: string | null
|
|
104
|
+
}): Promise<void> {
|
|
105
|
+
try {
|
|
106
|
+
await db.insert(webhookDeliveries).values(entry)
|
|
107
|
+
await pruneWebhookDeliveries(entry.webhookId)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('Failed to record webhook delivery:', {
|
|
110
|
+
event: entry.event,
|
|
111
|
+
error
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function pruneWebhookDeliveries(webhookId: string): Promise<void> {
|
|
117
|
+
const keepRows = await db
|
|
118
|
+
.select({ id: webhookDeliveries.id })
|
|
119
|
+
.from(webhookDeliveries)
|
|
120
|
+
.where(eq(webhookDeliveries.webhookId, webhookId))
|
|
121
|
+
.orderBy(desc(webhookDeliveries.createdAt))
|
|
122
|
+
.limit(DELIVERY_LOG_LIMIT)
|
|
123
|
+
|
|
124
|
+
if (keepRows.length < DELIVERY_LOG_LIMIT) return
|
|
125
|
+
|
|
126
|
+
await db.delete(webhookDeliveries).where(
|
|
127
|
+
and(
|
|
128
|
+
eq(webhookDeliveries.webhookId, webhookId),
|
|
129
|
+
notInArray(
|
|
130
|
+
webhookDeliveries.id,
|
|
131
|
+
keepRows.map((row) => row.id)
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
}
|