Files
2026-05-26 19:28:44 -04:00

83 lines
2.6 KiB
TypeScript

import { Router, Request, Response } from 'express'
import { Webhook } from 'svix'
import { handleInvitationAccepted } from '../services/teamService'
import { EmployeeRole } from '@prisma/client'
const router = Router()
/**
* POST /webhooks/clerk
*
* Handles Clerk webhook events. Registered in the Clerk dashboard.
* Uses svix signature verification — raw body must be preserved
* (use express.raw() before this route, not express.json()).
*
* Events handled:
* - user.created → when an invited employee accepts and creates their account
*/
router.post(
'/clerk',
async (req: Request, res: Response) => {
const webhookSecret = process.env.CLERK_WEBHOOK_SECRET
if (!webhookSecret) {
console.error('CLERK_WEBHOOK_SECRET is not set')
return res.status(500).json({ error: 'Webhook secret not configured' })
}
// Verify signature
const wh = new Webhook(webhookSecret)
let event: any
try {
event = wh.verify(req.body, {
'svix-id': req.headers['svix-id'] as string,
'svix-timestamp': req.headers['svix-timestamp'] as string,
'svix-signature': req.headers['svix-signature'] as string,
})
} catch (err) {
console.error('Clerk webhook signature verification failed:', err)
return res.status(400).json({ error: 'Invalid webhook signature' })
}
const { type, data } = event
// ── user.created ──────────────────────────────────────────
// Fired when an invited employee accepts their invite and creates
// their Clerk account. publicMetadata contains companyId + role
// that we embedded when creating the invitation.
if (type === 'user.created') {
const clerkUserId: string = data.id
const email: string = data.email_addresses?.[0]?.email_address ?? ''
const meta = data.public_metadata ?? {}
const companyId: string | undefined = meta.companyId
const role: EmployeeRole | undefined = meta.role
if (companyId && role && email) {
try {
const employee = await handleInvitationAccepted(
clerkUserId,
email,
companyId,
role
)
if (employee) {
console.log(
`Employee activated: ${employee.firstName} ${employee.lastName} (${employee.id})`
)
}
} catch (err) {
// Log but do not return 500 — Clerk will retry on 5xx
console.error('Failed to activate employee from Clerk webhook:', err)
}
}
}
res.json({ received: true })
}
)
export default router