-
-
Notifications
You must be signed in to change notification settings - Fork 8
Migrate Authentication from Supabase to Clerk #705
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
Comment on lines
+59
to
+77
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Guard against a missing email.
🤖 Prompt for AI Agents |
||
|
|
||
| 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' | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+61
to
+91
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== File list ==\n'
git ls-files | rg '^(app/api/clerk/webhook/route\.ts|.*syncUser.*|.*resolveClerkUserToDbUser.*|.*getCurrentUserIdOnServer.*|.*users.*)$' || true
printf '\n== Outline: app/api/clerk/webhook/route.ts ==\n'
ast-grep outline app/api/clerk/webhook/route.ts --view expanded || true
printf '\n== Relevant searches ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
'resolveClerkUserToDbUser|syncUser|getCurrentUserIdOnServer|onConflictDoNothing|clerkUserId|email_addresses|email_address|try\s*\{' \
app api . || true
printf '\n== route.ts excerpt ==\n'
sed -n '1,220p' app/api/clerk/webhook/route.ts || trueRepository: QueueLab/QCX Length of output: 50368 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== route.ts (30-110) ==\n'
sed -n '30,110p' app/api/clerk/webhook/route.ts
printf '\n== lib/actions/users.ts ==\n'
sed -n '1,220p' lib/actions/users.ts
printf '\n== sync-user references ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' 'syncUser|resolveClerkUserToDbUser|getCurrentUserIdOnServer' lib app componentsRepository: QueueLab/QCX Length of output: 11169 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== lib/auth/get-current-user.ts ==\n'
sed -n '1,220p' lib/auth/get-current-user.ts
printf '\n== lib/actions/sync-user.ts ==\n'
sed -n '1,200p' lib/actions/sync-user.ts
printf '\n== user schema ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' 'pgTable\\(|users =|email:|clerkUserId:|selectedModel:' lib db appRepository: QueueLab/QCX Length of output: 3937 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== schema files ==\n'
git ls-files | rg '(^|/)(schema|schemas|db/schema)\.(ts|tsx|js|jsx)$|lib/db/schema' || true
printf '\n== schema outline ==\n'
ast-grep outline lib/db/schema.ts --view expanded || true
printf '\n== schema excerpt ==\n'
sed -n '1,260p' lib/db/schema.ts || true
printf '\n== unique/nullability search ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
'uniqueIndex|unique\(|clerkUserId|email:.*(text|varchar|uuid)|notNull\(\)|nullable' \
lib/db/schema.tsRepository: QueueLab/QCX Length of output: 12301 Make the webhook write idempotent 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| if (eventType === 'user.deleted') { | ||
| // Optionally handle user deletion | ||
| // For safety, we might not want to delete user data entirely | ||
| } | ||
|
|
||
| return new Response('', { status: 200 }) | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,28 +1,34 @@ | ||||||||||||||||||||||
| 'use client' | ||||||||||||||||||||||
| import { useState, useEffect } from "react" | ||||||||||||||||||||||
| import { User, Settings, Paintbrush, Shield, CircleUserRound } from "lucide-react" | ||||||||||||||||||||||
| import { User, Settings, Shield, CircleUserRound, LogOut } from "lucide-react" | ||||||||||||||||||||||
| import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" | ||||||||||||||||||||||
| import { Button } from "@/components/ui/button" | ||||||||||||||||||||||
| import { ProfileToggleEnum, useProfileToggle } from "./profile-toggle-context" | ||||||||||||||||||||||
| import { useUsageToggle } from "./usage-toggle-context" | ||||||||||||||||||||||
| import { useClerk, useUser, SignInButton } from "@clerk/nextjs" | ||||||||||||||||||||||
| import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export function ProfileToggle() { | ||||||||||||||||||||||
| const { toggleProfileSection, activeView } = useProfileToggle() | ||||||||||||||||||||||
| const { isUsageOpen, closeUsage } = useUsageToggle() | ||||||||||||||||||||||
| const [alignValue, setAlignValue] = useState<'start' | 'end'>("end") | ||||||||||||||||||||||
| const [isMobile, setIsMobile] = useState(false) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Call hooks unconditionally | ||||||||||||||||||||||
| const { isLoaded, isSignedIn, user } = useUser() | ||||||||||||||||||||||
| const { signOut } = useClerk() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||
| const handleResize = () => { | ||||||||||||||||||||||
| const mobile = window.innerWidth < 768 | ||||||||||||||||||||||
| setIsMobile(mobile) | ||||||||||||||||||||||
| if (mobile) { | ||||||||||||||||||||||
| setAlignValue("start") // Right align on mobile too | ||||||||||||||||||||||
| setAlignValue("start") | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| setAlignValue("start") // Right align on desktop | ||||||||||||||||||||||
| setAlignValue("start") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
25
to
29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Redundant branch: both paths set The Proposed change const mobile = window.innerWidth < 768
setIsMobile(mobile)
- if (mobile) {
- setAlignValue("start")
- } else {
- setAlignValue("start")
- }
+ setAlignValue("start")📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| handleResize() // Set initial value | ||||||||||||||||||||||
| handleResize() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let resizeTimer: NodeJS.Timeout; | ||||||||||||||||||||||
| const debouncedResize = () => { | ||||||||||||||||||||||
|
|
@@ -35,17 +41,34 @@ export function ProfileToggle() { | |||||||||||||||||||||
| }, []) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const handleSectionToggle = (section: ProfileToggleEnum) => { | ||||||||||||||||||||||
| // If we're about to open a profile section and usage is open, close usage first | ||||||||||||||||||||||
| if (activeView !== section && isUsageOpen) { | ||||||||||||||||||||||
| closeUsage() | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| toggleProfileSection(section) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const handleSignOut = () => { | ||||||||||||||||||||||
| signOut(() => { | ||||||||||||||||||||||
| window.location.href = "/" | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+50
to
+54
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: In the Clerk authentication library, redirectUrl is the current, standard approach for handling redirection after a user signs out [1][2]. It replaced the older signOutCallback signature, which had previously allowed developers to pass a function to execute after the sign-out process [1]. Key differences and context: 1. redirectUrl (Current Standard): This is a property within the SignOutOptions object passed to the signOut function [3][4][5]. It specifies the destination path or URL to which the user should be redirected automatically after a successful sign-out [6][3][7]. This approach is consistent across Clerk's UI components (like SignOutButton) and the programmatic signOut method [2]. 2. signOutCallback (Deprecated/Legacy): Previously, some Clerk components and methods accepted a callback function (e.g., => { window.location.href = '/path' }) to handle post-sign-out logic [1]. This has been largely deprecated in favor of the declarative redirectUrl option to improve API consistency and simplify usage [1][2]. If you are currently implementing or migrating a sign-out flow, you should use the redirectUrl property within the options object [6][3]: // Programmatic sign-out await signOut({ redirectUrl: '/home' }); // Using SignOutButton component Sign Out For multi-session applications, you can also include a sessionId within the same options object to sign out of a specific session while still utilizing the redirectUrl for navigation [6][3][5]. Citations:
🏁 Script executed: set -euo pipefail
printf '\n== package references ==\n'
rg -n '"`@clerk/nextjs`"|`@clerk/nextjs`' package.json pnpm-lock.yaml package-lock.json yarn.lock **/package.json 2>/dev/null || true
printf '\n== profile-toggle.tsx context ==\n'
if [ -f components/profile-toggle.tsx ]; then
cat -n components/profile-toggle.tsx | sed -n '1,120p'
fi
printf '\n== repo signOut usages ==\n'
rg -n 'signOut\(' . --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' || trueRepository: QueueLab/QCX Length of output: 1947 🌐 Web query:
💡 Result: In Clerk version 7.5.13, the signOut method and associated components like SignOutButton use a redirectUrl option to control navigation after a successful sign-out [1][2]. The signOut method accepts an optional object with the following signature [1][2]: signOut(options?: { redirectUrl?: string; sessionId?: string }): Promise Parameters: - redirectUrl (string, optional): The full URL or path to navigate to after the sign-out process is complete [3][1][2]. - sessionId (string, optional): The ID of a specific session to sign out of, which is particularly useful for multi-session applications [3][1][2]. For UI components like SignOutButton, the redirectUrl prop is used directly to achieve the same result [3][4]. Note that older versions of Clerk components previously used a signOutCallback prop, which has been replaced by redirectUrl to align with Clerk's standard API surface [5]. If you are seeing a redirect_url query parameter appended to your URL after signing out, this is often part of Clerk's internal redirect handling for protected routes and post-authentication flows [6]. Citations:
Use Clerk’s 🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const ProfileIcon = () => { | ||||||||||||||||||||||
| if (isLoaded && isSignedIn && user?.imageUrl) { | ||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <Avatar className="h-[1.2rem] w-[1.2rem]"> | ||||||||||||||||||||||
| <AvatarImage src={user.imageUrl} alt={user.fullName || 'User'} /> | ||||||||||||||||||||||
| <AvatarFallback><CircleUserRound className="h-full w-full" /></AvatarFallback> | ||||||||||||||||||||||
| </Avatar> | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return <CircleUserRound className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (isMobile) { | ||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <Button variant="ghost" size="icon" className="relative" data-testid="profile-toggle" disabled> | ||||||||||||||||||||||
| <CircleUserRound className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> | ||||||||||||||||||||||
| <ProfileIcon /> | ||||||||||||||||||||||
| <span className="sr-only">Open profile menu</span> | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
@@ -55,7 +78,7 @@ export function ProfileToggle() { | |||||||||||||||||||||
| <DropdownMenu> | ||||||||||||||||||||||
| <DropdownMenuTrigger asChild> | ||||||||||||||||||||||
| <Button variant="ghost" size="icon" className="relative" data-testid="profile-toggle"> | ||||||||||||||||||||||
| <CircleUserRound className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> | ||||||||||||||||||||||
| <ProfileIcon /> | ||||||||||||||||||||||
| <span className="sr-only">Open profile menu</span> | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| </DropdownMenuTrigger> | ||||||||||||||||||||||
|
|
@@ -68,14 +91,25 @@ export function ProfileToggle() { | |||||||||||||||||||||
| <Settings className="mr-2 h-4 w-4" /> | ||||||||||||||||||||||
| <span>Settings</span> | ||||||||||||||||||||||
| </DropdownMenuItem> | ||||||||||||||||||||||
| <DropdownMenuItem onClick={() => handleSectionToggle(ProfileToggleEnum.Appearance)} data-testid="profile-appearance"> | ||||||||||||||||||||||
| <Paintbrush className="mr-2 h-4 w-4" /> | ||||||||||||||||||||||
| <span>Appearance</span> | ||||||||||||||||||||||
| </DropdownMenuItem> | ||||||||||||||||||||||
| <DropdownMenuItem onClick={() => handleSectionToggle(ProfileToggleEnum.Security)} data-testid="profile-security"> | ||||||||||||||||||||||
| <Shield className="mr-2 h-4 w-4" /> | ||||||||||||||||||||||
| <span>Security</span> | ||||||||||||||||||||||
| </DropdownMenuItem> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {isLoaded && isSignedIn && ( | ||||||||||||||||||||||
| <DropdownMenuItem onClick={handleSignOut} className="text-destructive"> | ||||||||||||||||||||||
| <LogOut className="mr-2 h-4 w-4" /> | ||||||||||||||||||||||
| <span>Sign out</span> | ||||||||||||||||||||||
| </DropdownMenuItem> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| {isLoaded && !isSignedIn && ( | ||||||||||||||||||||||
| <SignInButton mode="modal"> | ||||||||||||||||||||||
| <DropdownMenuItem> | ||||||||||||||||||||||
| <LogOut className="mr-2 h-4 w-4" /> | ||||||||||||||||||||||
| <span>Sign in</span> | ||||||||||||||||||||||
| </DropdownMenuItem> | ||||||||||||||||||||||
| </SignInButton> | ||||||||||||||||||||||
|
Comment on lines
+105
to
+111
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: To use the Clerk SignInButton component with a custom child, the mode set to modal, and an onClick event, you should wrap your custom element in the SignInButton component and configure it as follows [1][2]. When you provide a custom child (such as a custom or ) to SignInButton, you should generally include the asChild prop [2]. The asChild prop tells the component to merge its functionality into the provided child element rather than rendering a wrapper element [2]. For the modal functionality, ensure the mode prop is set to 'modal' [1][3][4]. Regarding the onClick event, because the SignInButton handles the click to trigger the modal, adding an onClick handler to your child element may conflict with Clerk's internal event handling depending on your implementation. If you need custom behavior alongside the modal trigger, you can often define a wrapper function for the onClick event, though it is usually recommended to rely on Clerk's built-in handling to ensure the authentication flow is not interrupted [1][2]. Example implementation: <button onClick={(e) => console.log('Custom logic', e)}> Sign In Key points: - Mode: Use mode="modal" to open the sign-in interface in a modal instead of redirecting [1][3][4]. - asChild: Set asChild={true} (or simply asChild) to render the SignInButton's behavior onto your provided child component [2]. - Child constraints: The component accepts only one child; if you have multiple elements, wrap them in a single parent (e.g., a or ) [1][2][3]. - Event handling: When using asChild, Clerk merges its event listeners with your child component's props [2]. Ensure any custom onClick logic does not call event.stopPropagation if you intend for Clerk's sign-in trigger to execute [1].
Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== profile-toggle.tsx ==\n'
sed -n '1,180p' components/profile-toggle.tsx
printf '\n== SignInButton usage ==\n'
rg -n "SignInButton|DropdownMenuItem|LogOut|LogIn" components src . -g '!**/node_modules/**'Repository: QueueLab/QCX Length of output: 1947 🌐 Web query:
💡 Result: In Radix UI, the DropdownMenuItem component is designed to close the menu automatically upon selection [1]. When you nest an interactive element (like a SignInButton triggering a modal) inside the item, the Radix menu's built-in closing logic often triggers before or during your child component's interaction, causing conflicts such as immediate closure, loss of focus, or pointer event issues [2][3][4][5]. To resolve this, you generally have two primary approaches: 1. Recommended: Use a Controlled Dialog Pattern (Avoid Nested Overlays) Instead of placing the modal trigger directly inside the menu, keep your modal's state at a parent level [6]. - Use the DropdownMenuItem's onSelect event to update a state variable that controls the modal's open state [6]. - Do not call event.preventDefault in this case; let the menu close normally, and then open the modal based on your state [6]. This ensures proper focus management and avoids conflict between the two portals [6][5]. 2. Alternative: Prevent Menu Closure (for specific use cases) If you must keep the menu open (e.g., for multi-step interactions or specific custom behavior), you can prevent the default closing behavior [7][8]. - Inside your DropdownMenuItem, use onSelect={(e) => e.preventDefault} [7][8]. - This keeps the menu open, allowing your child button's onClick event to fire independently [7]. - Note: This approach requires you to manually manage the menu's closing logic later, as it will no longer close automatically upon selection [7][5]. Common pitfalls to avoid: - Focus Traps/Modal Mode: By default, Radix DropdownMenu uses modal behavior, which can interfere with other overlays [4]. If you experience issues with pointer events or immediate closure, setting modal={false} on the DropdownMenu.Root can sometimes resolve these conflicts, though the state-controlled pattern is generally more robust [4][5]. - Event Propagation: If your child's click event is not firing, ensure that you are not accidentally stopping propagation in a way that interferes with Radix's internal event handling [4][9]. Conversely, if the menu is closing due to bubbling, stopPropagation on the child's onClick can sometimes help [9]. Citations:
🌐 Web query:
💡 Result: To use a custom child element with the Clerk SignInButton component while maintaining control over the click behavior and triggering a modal, you should use the asChild prop [1][2][3]. The asChild prop is the recommended approach for customizing unstyled components like SignInButton [3]. When set to true, it instructs the component to render its children as the underlying element rather than creating a new container (like an extra button or div), effectively merging the Clerk functionality (like the onClick handler) onto your custom element [2][3]. To use it with a modal: 1. Wrap your custom trigger (e.g., a button or a custom styled element) inside the SignInButton component. 2. Set the mode prop to 'modal' [1][4][5]. 3. Add the asChild prop to the SignInButton component [2]. Example: Sign In Key points: - Mode: Setting mode="modal" ensures the sign-in flow opens as a modal instead of redirecting the user to a separate page [1][2][5]. - Children: The component requires a single child; if you need to render multiple elements, ensure they are wrapped in a single parent element (such as a div or a fragment) [1][2][4]. - Deprecation: The older 'as' prop has been deprecated and replaced by 'asChild' [3]. Using asChild allows for better composition and accessibility by ensuring the Clerk logic is applied directly to your intended UI element [3].Citations:
Move the sign-in trigger out of the dropdown item and swap the icon. 🤖 Prompt for AI Agents |
||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| </DropdownMenuContent> | ||||||||||||||||||||||
| </DropdownMenu> | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,20 @@ | ||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import { useEffect } from 'react'; | ||||||||||||||||||||||
| import { useUser } from '@clerk/nextjs'; | ||||||||||||||||||||||
| import { syncUser } from '@/lib/actions/sync-user'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Client component that triggers user synchronization on mount if authenticated. | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| export function UserSync() { | ||||||||||||||||||||||
| const { isLoaded, isSignedIn } = useUser(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||
| if (isLoaded && isSignedIn) { | ||||||||||||||||||||||
| syncUser(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }, [isLoaded, isSignedIn]); | ||||||||||||||||||||||
|
Comment on lines
+13
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Handle the The server action result is neither awaited nor caught; a failed sync surfaces as an unhandled promise rejection. Add a ♻️ Suggested change- if (isLoaded && isSignedIn) {
- syncUser();
- }
+ if (isLoaded && isSignedIn) {
+ syncUser().catch((err) => console.error('[UserSync] sync failed:', err));
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return null; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Svix verification needs the exact raw request payload. Parsing with
req.json()and thenJSON.stringify(payload)can change the byte representation, which can cause valid Clerk webhooks to fail signature verification.Suggested fix: read the raw body first (
const body = await req.text()), verify that raw string, and only then parse JSON for downstream logic.