mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 13:19:09 +03:00
## What
Wires the existing (mocked) Platform Webhooks UI to the real Management
API backend, per FE-3749.
- New `data/platform-webhooks/` query/mutation hooks, calling the real
`/v2/organizations/{slug}/webhooks/...` and
`/v2/projects/{ref}/webhooks/...` endpoints
- Updated `PlatformWebhooks.types.ts` to match the real API contract
(field names, shapes)
- Added a signing secret field to the create-endpoint form (API requires
a user-provided secret, not server-generated like the mock assumed)
- Removed the event-type column from the deliveries table (the real
list-deliveries endpoint doesn't return event type, only the
single-delivery endpoint does)
- Removed `PlatformWebhooks.mock.ts` / `.store.ts` and their tests
## Why
UI was built ~80% against mock data months ago; this closes the gap to a
real, working feature.
## Known open items (not blocking, tracked separately)
- `name` field has no backend storage yet — client-only for now, open
question with Ali/Paweł
- Deliveries table only fetches the backend's default single page — real
server-side pagination not yet wired (see FE-3750)
## Follow-up PR (stacked)
Tests for the new `data/platform-webhooks/` hooks (MSW-based), per the
ticket's own scope split.
## Testing
Manually tested end-to-end against local backend: created/edited/deleted
an endpoint, verified the real signature using webhook.site,
independently recomputed the HMAC and confirmed it matches the
`webhook-signature` header.
Linear: FE-3749
---------
Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
95 lines
2.4 KiB
TypeScript
95 lines
2.4 KiB
TypeScript
import type { WebhookDelivery, WebhookEndpoint } from './PlatformWebhooks.types'
|
|
|
|
const WEBHOOK_NAME_ADJECTIVES = [
|
|
'swift',
|
|
'winged',
|
|
'wayfinding',
|
|
'moonlit',
|
|
'fleet',
|
|
'nimble',
|
|
'roving',
|
|
'brisk',
|
|
'gliding',
|
|
'steady',
|
|
'northbound',
|
|
'starlit',
|
|
'quiet',
|
|
'amber',
|
|
'far-flung',
|
|
'secret',
|
|
'flying',
|
|
]
|
|
|
|
const WEBHOOK_NAME_NOUNS = [
|
|
'pigeon',
|
|
'courier',
|
|
'postmark',
|
|
'relay',
|
|
'dispatch',
|
|
'lantern',
|
|
'beacon',
|
|
'messenger',
|
|
'waypost',
|
|
'sparrow',
|
|
'satchel',
|
|
'signalfire',
|
|
'envelope',
|
|
'parcel',
|
|
'gull',
|
|
'kite',
|
|
'beagle',
|
|
]
|
|
|
|
const getRandomItem = (values: string[]) => values[Math.floor(Math.random() * values.length)]
|
|
|
|
export const generateWebhookEndpointName = () =>
|
|
`${getRandomItem(WEBHOOK_NAME_ADJECTIVES)}-${getRandomItem(WEBHOOK_NAME_NOUNS)}`
|
|
|
|
const secureRandomHex = (length: number) => {
|
|
if (length <= 0) return ''
|
|
|
|
const cryptoApi = globalThis.crypto
|
|
if (!cryptoApi?.getRandomValues) {
|
|
throw new Error('Web Crypto API is not available')
|
|
}
|
|
|
|
const byteCount = Math.ceil(length / 2)
|
|
const bytes = new Uint8Array(byteCount)
|
|
cryptoApi.getRandomValues(bytes)
|
|
|
|
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0'))
|
|
.join('')
|
|
.slice(0, length)
|
|
}
|
|
|
|
export const generateSigningSecret = () => `whsec_${secureRandomHex(16)}`
|
|
|
|
export const getWebhookEndpointDisplayName = (endpoint: Pick<WebhookEndpoint, 'name' | 'url'>) =>
|
|
endpoint.name.trim() || endpoint.url
|
|
|
|
const normalizeSearch = (value: string) => value.trim().toLowerCase()
|
|
|
|
export const filterWebhookEndpoints = (endpoints: WebhookEndpoint[], search: string) => {
|
|
const normalizedSearch = normalizeSearch(search)
|
|
if (normalizedSearch.length === 0) return endpoints
|
|
|
|
return endpoints.filter((endpoint) => {
|
|
const haystack =
|
|
`${getWebhookEndpointDisplayName(endpoint)} ${endpoint.url} ${endpoint.description}`.toLowerCase()
|
|
return haystack.includes(normalizedSearch)
|
|
})
|
|
}
|
|
|
|
export const filterWebhookDeliveries = (deliveries: WebhookDelivery[], search: string) => {
|
|
const normalizedSearch = normalizeSearch(search)
|
|
|
|
return deliveries
|
|
.filter((delivery) => {
|
|
if (normalizedSearch.length === 0) return true
|
|
const haystack =
|
|
`${delivery.eventType ?? ''} ${delivery.status} ${delivery.responseCode ?? ''}`.toLowerCase()
|
|
return haystack.includes(normalizedSearch)
|
|
})
|
|
.sort((a, b) => new Date(b.attemptAt).getTime() - new Date(a.attemptAt).getTime())
|
|
}
|