276 lines
8.3 KiB
TypeScript
276 lines
8.3 KiB
TypeScript
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 });
|
|
}
|
|
}
|