From 0b8754696066e65a6e6c174ad4ca06d56f059e66 Mon Sep 17 00:00:00 2001 From: Administrator Date: Thu, 30 Apr 2026 18:25:53 +0000 Subject: [PATCH] Upload New File --- webhooks.ts | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 webhooks.ts diff --git a/webhooks.ts b/webhooks.ts new file mode 100644 index 0000000..8f75775 --- /dev/null +++ b/webhooks.ts @@ -0,0 +1,82 @@ +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