import { Webhook } from "svix"; import { headers } from "next/headers"; import { WebhookEvent } from "@clerk/nextjs/server"; import { NextResponse } from "next/server"; import Database from "better-sqlite3"; import path from "path"; export async function POST(req: Request) { // Get the webhook secret from environment variables const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET; if (!WEBHOOK_SECRET) { throw new Error( "Please add CLERK_WEBHOOK_SECRET to your environment variables", ); } // Get the headers const headerPayload = await headers(); const svix_id = headerPayload.get("svix-id"); const svix_timestamp = headerPayload.get("svix-timestamp"); const svix_signature = headerPayload.get("svix-signature"); // If there are no headers, error out if (!svix_id || !svix_timestamp || !svix_signature) { return NextResponse.json( { error: "Missing svix headers" }, { status: 400 }, ); } // Get the body const payload = await req.json(); const body = JSON.stringify(payload); // Create a new Svix instance with your webhook secret const wh = new Webhook(WEBHOOK_SECRET); let evt: WebhookEvent; // Verify the webhook signature try { evt = wh.verify(body, { "svix-id": svix_id, "svix-timestamp": svix_timestamp, "svix-signature": svix_signature, }) as WebhookEvent; } catch (err) { console.error("Error verifying webhook:", err); return NextResponse.json({ error: "Verification failed" }, { status: 400 }); } // Handle the webhook const eventType = evt.type; console.log(`Received webhook with ID ${evt.data.id} and type ${eventType}`); try { // Connect to database directly for webhook operations const dbPath = path.join(process.cwd(), "data", "fitai.db"); const db = new Database(dbPath); switch (eventType) { case "user.created": { const { id, email_addresses, first_name, last_name, public_metadata } = evt.data; // Get primary email const primaryEmail = email_addresses.find( (email) => email.id === evt.data.primary_email_address_id, ); if (!primaryEmail?.email_address) { console.error("No primary email found for user:", id); db.close(); return NextResponse.json( { error: "No primary email" }, { status: 400 }, ); } // Determine role & gym from metadata const role = (public_metadata?.role as | "superAdmin" | "admin" | "trainer" | "client" | "generalUser") || "client"; let gymId = (public_metadata?.gymId as string | null) ?? null; const inviterUserId = (public_metadata?.inviterUserId as string | undefined) ?? undefined; const roleAssigned = (public_metadata?.roleAssigned as | "superAdmin" | "admin" | "trainer" | "client" | "generalUser" | undefined) ?? role; // Insert user into database with Clerk's user ID const now = new Date().toISOString(); const stmt = db.prepare(` INSERT INTO users (id, email, first_name, last_name, password, phone, role, gym_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run( id, primaryEmail.email_address, first_name || "", last_name || "", "", // Clerk handles authentication null, // phone role, gymId, now, now, ); // If this is a client invited by a trainer, create trainer-client link if (roleAssigned === "client" && inviterUserId && gymId) { const inviterRow = db .prepare("SELECT role FROM users WHERE id = ?") .get(inviterUserId) as { role?: string } | undefined; if (inviterRow?.role === "trainer") { const linkId = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`; const linkStmt = db.prepare(` INSERT INTO trainer_clients (id, trainer_user_id, client_user_id, gym_id, created_at) VALUES (?, ?, ?, ?, ?) `); linkStmt.run( linkId, inviterUserId, id, gymId, new Date().toISOString(), ); } } // If this is a trainer without a gymId but has an inviter, inherit inviter's gymId if ( (roleAssigned === "trainer" || role === "trainer") && !gymId && inviterUserId ) { const inviterGymRow = db .prepare("SELECT gym_id FROM users WHERE id = ?") .get(inviterUserId) as { gym_id?: string } | undefined; if (inviterGymRow?.gym_id) { const inheritStmt = db.prepare(` UPDATE users SET gym_id = ?, updated_at = ? WHERE id = ? `); inheritStmt.run(inviterGymRow.gym_id, new Date().toISOString(), id); gymId = inviterGymRow.gym_id; } } console.log( `✅ User ${id} created in database (role=${role}, gymId=${gymId ?? "null"})`, ); db.close(); break; } case "user.updated": { const { id, email_addresses, first_name, last_name, public_metadata } = evt.data; // Get primary email const primaryEmail = email_addresses.find( (email) => email.id === evt.data.primary_email_address_id, ); if (!primaryEmail?.email_address) { console.error("No primary email found for user:", id); db.close(); return NextResponse.json( { error: "No primary email" }, { status: 400 }, ); } // Determine role & gym from metadata const role = (public_metadata?.role as | "superAdmin" | "admin" | "trainer" | "client" | "generalUser") || "client"; let gymId = (public_metadata?.gymId as string | null) ?? null; // Update user in database const now = new Date().toISOString(); const stmt = db.prepare(` UPDATE users SET email = ?, first_name = ?, last_name = ?, role = ?, gym_id = ?, updated_at = ? WHERE id = ? `); stmt.run( primaryEmail.email_address, first_name || "", last_name || "", role, gymId, now, id, ); // If user is a trainer and gymId is missing, attempt to inherit from inviter when available if ( role === "trainer" && !gymId && evt.data.public_metadata?.inviterUserId ) { const inviterUserId = String(evt.data.public_metadata.inviterUserId); const inviterGymRow = db .prepare("SELECT gym_id FROM users WHERE id = ?") .get(inviterUserId) as { gym_id?: string } | undefined; if (inviterGymRow?.gym_id) { const inheritStmt = db.prepare(` UPDATE users SET gym_id = ?, updated_at = ? WHERE id = ? `); inheritStmt.run(inviterGymRow.gym_id, new Date().toISOString(), id); gymId = inviterGymRow.gym_id; } } console.log(`✅ User ${id} updated in database`); db.close(); break; } case "user.deleted": { const { id } = evt.data; if (!id) { console.error("No user ID provided for deletion"); db.close(); return NextResponse.json({ error: "No user ID" }, { status: 400 }); } // Delete user from database (cascade will handle related records) const stmt = db.prepare("DELETE FROM users WHERE id = ?"); stmt.run(id); console.log(`✅ User ${id} deleted from database`); db.close(); break; } default: console.log(`Unhandled webhook event type: ${eventType}`); db.close(); } return NextResponse.json( { message: "Webhook processed successfully" }, { status: 200 }, ); } catch (error) { console.error("Error processing webhook:", error); return NextResponse.json({ error: "Processing failed" }, { status: 500 }); } }