83 lines
2.6 KiB
TypeScript
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
|