From f1639f23526974ab7f5cdb265c4cf936123d6cfc Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 08:26:22 +0000 Subject: [PATCH 1/2] feat: migrate authentication from Supabase to Clerk Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/api/clerk/webhook/route.ts | 100 +++++++++++++++++++ app/layout.tsx | 5 + bun.lock | 27 +++++ components/header.tsx | 73 +++++++++++--- components/profile-toggle.tsx | 35 ++++++- components/user-sync.tsx | 20 ++++ lib/actions/sync-user.ts | 15 +++ lib/auth/get-current-user.ts | 174 ++++++++++++++------------------- lib/auth/use-current-user.ts | 32 +++--- lib/db/schema.ts | 1 + lib/supabase/browser-client.ts | 10 +- lib/supabase/client.ts | 48 ++------- middleware.ts | 31 ++---- package.json | 3 + public/sw.js | 2 +- 15 files changed, 372 insertions(+), 204 deletions(-) create mode 100644 app/api/clerk/webhook/route.ts create mode 100644 components/user-sync.tsx create mode 100644 lib/actions/sync-user.ts diff --git a/app/api/clerk/webhook/route.ts b/app/api/clerk/webhook/route.ts new file mode 100644 index 00000000..5228c2fb --- /dev/null +++ b/app/api/clerk/webhook/route.ts @@ -0,0 +1,100 @@ +import { Webhook } from 'svix' +import { headers } from 'next/headers' +import { WebhookEvent } from '@clerk/nextjs/server' +import { db } from '@/lib/db' +import { users } from '@/lib/db/schema' +import { eq } from 'drizzle-orm' + +export async function POST(req: Request) { + // You can find this in the Clerk Dashboard -> Webhooks -> choose the endpoint + const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET + + if (!WEBHOOK_SECRET) { + throw new Error('Please add CLERK_WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local') + } + + // 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 new Response('Error occured -- no svix headers', { + status: 400, + }) + } + + // Get the body + const payload = await req.json() + const body = JSON.stringify(payload) + + // Create a new Svix instance with your secret. + const wh = new Webhook(WEBHOOK_SECRET) + + let evt: WebhookEvent + + // Verify the payload with the headers + 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 new Response('Error occured', { + status: 400, + }) + } + + // Do something with the payload + // For this guide, you can log the payload to your console + const { id } = evt.data + const eventType = evt.type + + if (eventType === 'user.created' || eventType === 'user.updated') { + const { id, email_addresses, first_name, last_name } = evt.data as any + const email = email_addresses[0]?.email_address + + if (id) { + // Upsert user + const [existingUser] = await db.select() + .from(users) + .where(eq(users.clerkUserId, id)) + .limit(1); + + if (existingUser) { + await db.update(users) + .set({ email }) + .where(eq(users.clerkUserId, id)); + } else { + // Check by email first to link + const [existingEmailUser] = await db.select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + if (existingEmailUser) { + await db.update(users) + .set({ clerkUserId: id }) + .where(eq(users.id, existingEmailUser.id)); + } else { + await db.insert(users).values({ + clerkUserId: id, + email, + role: 'viewer' + }); + } + } + } + } + + if (eventType === 'user.deleted') { + // Optionally handle user deletion + // For safety, we might not want to delete user data entirely + } + + return new Response('', { status: 200 }) +} diff --git a/app/layout.tsx b/app/layout.tsx index 21e866c4..465e7f2d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,3 +1,5 @@ +import { ClerkProvider } from "@clerk/nextjs" +import { UserSync } from "@/components/user-sync" import type { Metadata, Viewport } from 'next' import { Inter as FontSans, Poppins } from 'next/font/google' import './globals.css' @@ -75,6 +77,7 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( +