sync user
from clerk -> db
This commit is contained in:
1 parent
3a58d420d6
commit
64bc4aa58b
5 files changed
+549
-120
No files matched your search
@@ -2,6 +2,8 @@
|
||||
# Get these values from https://dashboard.clerk.com
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_bmVlZGVkLWVsZXBoYW50LTY0LmNsZXJrLmFjY291bnRzLmRldiQ
|
||||
CLERK_SECRET_KEY=sk_test_qnWnZSem1ZkodRip9NZDXszDnCP91HwlNwtAUAcHZ1
|
||||
CLERK_WEBHOOK_SECRET=whsec_iYJM17VbGIDQnMX/5VsyjF7egjdlwuXC
|
||||
|
||||
|
||||
|
||||
# Clerk URLs (customize these for your app)
|
||||
|
||||
Binary file not shown.
@@ -1,29 +1,32 @@
|
||||
import { Webhook } from 'svix';
|
||||
import { headers } from 'next/headers';
|
||||
import { WebhookEvent } from '@clerk/nextjs/server';
|
||||
import { db } from '@fitai/database';
|
||||
import { users } from '@fitai/database/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
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');
|
||||
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');
|
||||
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 new Response('Error: Missing svix headers', {
|
||||
status: 400,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: "Missing svix headers" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Get the body
|
||||
@@ -38,15 +41,13 @@ export async function POST(req: Request) {
|
||||
// Verify the webhook signature
|
||||
try {
|
||||
evt = wh.verify(body, {
|
||||
'svix-id': svix_id,
|
||||
'svix-timestamp': svix_timestamp,
|
||||
'svix-signature': svix_signature,
|
||||
"svix-id": svix_id,
|
||||
"svix-timestamp": svix_timestamp,
|
||||
"svix-signature": svix_signature,
|
||||
}) as WebhookEvent;
|
||||
} catch (err) {
|
||||
console.error('Error verifying webhook:', err);
|
||||
return new Response('Error: Verification failed', {
|
||||
status: 400,
|
||||
});
|
||||
console.error("Error verifying webhook:", err);
|
||||
return NextResponse.json({ error: "Verification failed" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Handle the webhook
|
||||
@@ -54,94 +55,130 @@ export async function POST(req: Request) {
|
||||
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;
|
||||
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
|
||||
(email) => email.id === evt.data.primary_email_address_id,
|
||||
);
|
||||
|
||||
if (!primaryEmail?.email_address) {
|
||||
console.error('No primary email found for user:', id);
|
||||
return new Response('Error: No primary email', { status: 400 });
|
||||
console.error("No primary email found for user:", id);
|
||||
db.close();
|
||||
return NextResponse.json(
|
||||
{ error: "No primary email" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Determine role from metadata or default to 'client'
|
||||
const role = (public_metadata?.role as 'admin' | 'trainer' | 'client') || 'client';
|
||||
const role =
|
||||
(public_metadata?.role as "admin" | "trainer" | "client") || "client";
|
||||
|
||||
// Insert user into database
|
||||
await db.insert(users).values({
|
||||
// Insert user into database with Clerk's user ID
|
||||
const now = new Date().toISOString();
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO users (id, email, firstName, lastName, password, phone, role, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
id,
|
||||
email: primaryEmail.email_address,
|
||||
firstName: first_name || '',
|
||||
lastName: last_name || '',
|
||||
password: '', // Clerk handles authentication, no password needed
|
||||
primaryEmail.email_address,
|
||||
first_name || "",
|
||||
last_name || "",
|
||||
"", // Clerk handles authentication
|
||||
null, // phone
|
||||
role,
|
||||
phone: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
now,
|
||||
now,
|
||||
);
|
||||
|
||||
console.log(`✅ User ${id} created in database`);
|
||||
db.close();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'user.updated': {
|
||||
const { id, email_addresses, first_name, last_name, public_metadata } = evt.data;
|
||||
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
|
||||
(email) => email.id === evt.data.primary_email_address_id,
|
||||
);
|
||||
|
||||
if (!primaryEmail?.email_address) {
|
||||
console.error('No primary email found for user:', id);
|
||||
return new Response('Error: No primary email', { status: 400 });
|
||||
console.error("No primary email found for user:", id);
|
||||
db.close();
|
||||
return NextResponse.json(
|
||||
{ error: "No primary email" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Determine role from metadata
|
||||
const role = (public_metadata?.role as 'admin' | 'trainer' | 'client') || 'client';
|
||||
const role =
|
||||
(public_metadata?.role as "admin" | "trainer" | "client") || "client";
|
||||
|
||||
// Update user in database
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
email: primaryEmail.email_address,
|
||||
firstName: first_name || '',
|
||||
lastName: last_name || '',
|
||||
role,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(users.id, id));
|
||||
const now = new Date().toISOString();
|
||||
const stmt = db.prepare(`
|
||||
UPDATE users
|
||||
SET email = ?, firstName = ?, lastName = ?, role = ?, updatedAt = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
primaryEmail.email_address,
|
||||
first_name || "",
|
||||
last_name || "",
|
||||
role,
|
||||
now,
|
||||
id,
|
||||
);
|
||||
|
||||
console.log(`✅ User ${id} updated in database`);
|
||||
db.close();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'user.deleted': {
|
||||
case "user.deleted": {
|
||||
const { id } = evt.data;
|
||||
|
||||
if (!id) {
|
||||
console.error('No user ID provided for deletion');
|
||||
return new Response('Error: No user ID', { status: 400 });
|
||||
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)
|
||||
await db.delete(users).where(eq(users.id, id));
|
||||
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 new Response('Webhook processed successfully', { status: 200 });
|
||||
return NextResponse.json(
|
||||
{ message: "Webhook processed successfully" },
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error processing webhook:', error);
|
||||
return new Response('Error: Processing failed', { status: 500 });
|
||||
console.error("Error processing webhook:", error);
|
||||
return NextResponse.json({ error: "Processing failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,83 +1,88 @@
|
||||
// Database Entity Types
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
password: string
|
||||
phone?: string
|
||||
role: 'admin' | 'client'
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
password: string;
|
||||
phone?: string;
|
||||
role: "admin" | "trainer" | "client";
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
id: string
|
||||
userId: string
|
||||
membershipType: 'basic' | 'premium' | 'vip'
|
||||
membershipStatus: 'active' | 'inactive' | 'expired'
|
||||
joinDate: Date
|
||||
id: string;
|
||||
userId: string;
|
||||
membershipType: "basic" | "premium" | "vip";
|
||||
membershipStatus: "active" | "inactive" | "expired";
|
||||
joinDate: Date;
|
||||
}
|
||||
|
||||
export interface FitnessProfile {
|
||||
userId: string
|
||||
height: string
|
||||
weight: string
|
||||
age: string
|
||||
gender: 'male' | 'female' | 'other'
|
||||
activityLevel: 'sedentary' | 'light' | 'moderate' | 'active' | 'very_active'
|
||||
fitnessGoals: string[]
|
||||
exerciseHabits: string
|
||||
dietHabits: string
|
||||
medicalConditions: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
userId: string;
|
||||
height: string;
|
||||
weight: string;
|
||||
age: string;
|
||||
gender: "male" | "female" | "other";
|
||||
activityLevel: "sedentary" | "light" | "moderate" | "active" | "very_active";
|
||||
fitnessGoals: string[];
|
||||
exerciseHabits: string;
|
||||
dietHabits: string;
|
||||
medicalConditions: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// Database Interface - allows us to swap implementations
|
||||
export interface IDatabase {
|
||||
// Connection management
|
||||
connect(): Promise<void>
|
||||
disconnect(): Promise<void>
|
||||
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
|
||||
// User operations
|
||||
createUser(user: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User>
|
||||
getUserById(id: string): Promise<User | null>
|
||||
getUserByEmail(email: string): Promise<User | null>
|
||||
getAllUsers(): Promise<User[]>
|
||||
updateUser(id: string, updates: Partial<User>): Promise<User | null>
|
||||
deleteUser(id: string): Promise<boolean>
|
||||
|
||||
createUser(user: Omit<User, "id" | "createdAt" | "updatedAt">): Promise<User>;
|
||||
getUserById(id: string): Promise<User | null>;
|
||||
getUserByEmail(email: string): Promise<User | null>;
|
||||
getAllUsers(): Promise<User[]>;
|
||||
updateUser(id: string, updates: Partial<User>): Promise<User | null>;
|
||||
deleteUser(id: string): Promise<boolean>;
|
||||
|
||||
// Client operations
|
||||
createClient(client: Omit<Client, 'id'>): Promise<Client>
|
||||
getClientById(id: string): Promise<Client | null>
|
||||
getClientByUserId(userId: string): Promise<Client | null>
|
||||
getAllClients(): Promise<Client[]>
|
||||
updateClient(id: string, updates: Partial<Client>): Promise<Client | null>
|
||||
deleteClient(id: string): Promise<boolean>
|
||||
|
||||
createClient(client: Omit<Client, "id">): Promise<Client>;
|
||||
getClientById(id: string): Promise<Client | null>;
|
||||
getClientByUserId(userId: string): Promise<Client | null>;
|
||||
getAllClients(): Promise<Client[]>;
|
||||
updateClient(id: string, updates: Partial<Client>): Promise<Client | null>;
|
||||
deleteClient(id: string): Promise<boolean>;
|
||||
|
||||
// Fitness Profile operations
|
||||
createFitnessProfile(profile: Omit<FitnessProfile, 'createdAt' | 'updatedAt'>): Promise<FitnessProfile>
|
||||
getFitnessProfileByUserId(userId: string): Promise<FitnessProfile | null>
|
||||
getAllFitnessProfiles(): Promise<FitnessProfile[]>
|
||||
updateFitnessProfile(userId: string, updates: Partial<FitnessProfile>): Promise<FitnessProfile | null>
|
||||
deleteFitnessProfile(userId: string): Promise<boolean>
|
||||
createFitnessProfile(
|
||||
profile: Omit<FitnessProfile, "createdAt" | "updatedAt">,
|
||||
): Promise<FitnessProfile>;
|
||||
getFitnessProfileByUserId(userId: string): Promise<FitnessProfile | null>;
|
||||
getAllFitnessProfiles(): Promise<FitnessProfile[]>;
|
||||
updateFitnessProfile(
|
||||
userId: string,
|
||||
updates: Partial<FitnessProfile>,
|
||||
): Promise<FitnessProfile | null>;
|
||||
deleteFitnessProfile(userId: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
// Database configuration
|
||||
export interface DatabaseConfig {
|
||||
type: 'sqlite' | 'postgresql' | 'mysql' | 'mongodb'
|
||||
type: "sqlite" | "postgresql" | "mysql" | "mongodb";
|
||||
connection: {
|
||||
filename?: string // for SQLite
|
||||
host?: string // for SQL databases
|
||||
port?: number
|
||||
database?: string
|
||||
username?: string
|
||||
password?: string
|
||||
}
|
||||
filename?: string; // for SQLite
|
||||
host?: string; // for SQL databases
|
||||
port?: number;
|
||||
database?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
options?: {
|
||||
logging?: boolean
|
||||
poolSize?: number
|
||||
timeout?: number
|
||||
}
|
||||
}
|
||||
logging?: boolean;
|
||||
poolSize?: number;
|
||||
timeout?: number;
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user