fitaiProto/apps/admin/src/app/api/webhooks/route.ts
2025-12-18 17:19:11 +01:00

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 });
}
}