refactor: use betterauth instead of workos

This commit is contained in:
2025-10-05 20:21:45 +00:00
parent b654f50ddd
commit 483aa19351
23 changed files with 4141 additions and 273 deletions

View File

@@ -1,12 +1,12 @@
import "@/styles/globals.css"
import { ConvexProviderWithAuthKit } from "@convex-dev/workos"
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { createRootRoute, Outlet } from "@tanstack/react-router"
import { AuthKitProvider, useAuth } from "@workos-inc/authkit-react"
import { ConvexReactClient } from "convex/react"
import { toast } from "sonner"
import { formatError } from "@/lib/error"
import { useKeyboardModifierListener } from "@/lib/keyboard"
import { authClient } from "../auth-client"
export const Route = createRootRoute({
component: RootLayout,
@@ -30,17 +30,12 @@ function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<AuthKitProvider
clientId={process.env.BUN_PUBLIC_WORKOS_CLIENT_ID!}
redirectUri={process.env.BUN_PUBLIC_WORKOS_REDIRECT_URI!}
<ConvexBetterAuthProvider
client={convexClient}
authClient={authClient}
>
<ConvexProviderWithAuthKit
client={convexClient}
useAuth={useAuth}
>
<Outlet />
</ConvexProviderWithAuthKit>
</AuthKitProvider>
<Outlet />
</ConvexBetterAuthProvider>
</QueryClientProvider>
)
}

View File

@@ -4,7 +4,6 @@ import {
Outlet,
useLocation,
} from "@tanstack/react-router"
import { useAuth } from "@workos-inc/authkit-react"
import {
Authenticated,
AuthLoading,
@@ -12,6 +11,7 @@ import {
useConvexAuth,
} from "convex/react"
import { useEffect, useState } from "react"
import { authClient } from "@/auth-client"
import { LoadingSpinner } from "@/components/ui/loading-spinner"
export const Route = createFileRoute("/_authenticated")({
@@ -21,7 +21,7 @@ export const Route = createFileRoute("/_authenticated")({
function AuthenticatedLayout() {
const { search } = useLocation()
const { isLoading } = useConvexAuth()
const { isLoading: authKitLoading } = useAuth()
const { isPending: sessionLoading } = authClient.useSession()
const [hasProcessedAuth, setHasProcessedAuth] = useState(false)
// Check if we're in the middle of processing an auth code
@@ -29,17 +29,17 @@ function AuthenticatedLayout() {
// Track when auth processing is complete
useEffect(() => {
if (!authKitLoading && !isLoading) {
if (!sessionLoading && !isLoading) {
// Delay to ensure auth state is fully synchronized
const timer = setTimeout(() => {
setHasProcessedAuth(true)
}, 500)
return () => clearTimeout(timer)
}
}, [authKitLoading, isLoading])
}, [sessionLoading, isLoading])
// Show loading during auth code processing or while auth state is syncing
if (hasAuthCode || authKitLoading || isLoading || !hasProcessedAuth) {
if (hasAuthCode || sessionLoading || isLoading || !hasProcessedAuth) {
return (
<div className="flex h-screen w-full items-center justify-center">
<LoadingSpinner className="size-10" />

View File

@@ -1,5 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"
import { useAuth } from "@workos-inc/authkit-react"
import { Button } from "../components/ui/button"
export const Route = createFileRoute("/login")({
@@ -7,11 +6,9 @@ export const Route = createFileRoute("/login")({
})
function RouteComponent() {
const { signIn } = useAuth()
return (
<div className="flex h-screen w-full items-center justify-center">
<Button onClick={() => signIn()}>Login</Button>
<Button onClick={() => {}}>Login</Button>
</div>
)
}

View File

@@ -0,0 +1,195 @@
import { useMutation } from "@tanstack/react-query"
import { createFileRoute, useNavigate } from "@tanstack/react-router"
import { GalleryVerticalEnd } from "lucide-react"
import type React from "react"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { type AuthErrorCode, authClient } from "../auth-client"
export const Route = createFileRoute("/sign-up")({
component: SignupPage,
})
type SignUpParams = {
displayName: string
email: string
password: string
confirmPassword: string
}
class PasswordMismatchError extends Error {
constructor() {
super("Passwords do not match")
}
}
class BetterAuthError extends Error {
constructor(public readonly errorCode: AuthErrorCode) {
super(`better-auth error: ${errorCode}`)
}
}
function SignupPage() {
return (
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div className="flex w-full max-w-xl flex-col gap-6">
<a
href="#"
className="flex items-center gap-2 self-center font-medium text-xl"
>
<div className="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
<GalleryVerticalEnd className="size-4" />
</div>
Drexa
</a>
<SignUpFormContainer>
<SignupForm />
</SignUpFormContainer>
</div>
</div>
)
}
function SignUpFormContainer({ children }: React.PropsWithChildren) {
return (
<div className="flex flex-col gap-6">
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">
Create your account
</CardTitle>
<CardDescription>
Enter your email below to create your account
</CardDescription>
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
</div>
)
}
function SignupForm() {
const navigate = useNavigate()
const {
mutate: signUp,
isPending,
error: signUpError,
} = useMutation({
mutationFn: async (data: SignUpParams) => {
if (data.password !== data.confirmPassword) {
throw new PasswordMismatchError()
}
const { data: signUpData, error } = await authClient.signUp.email({
name: data.displayName,
email: data.email,
password: data.password,
})
if (error) {
throw new BetterAuthError(error.code as AuthErrorCode)
}
return signUpData
},
onSuccess: () => {
navigate({
to: "/",
replace: true,
})
},
})
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const formData = new FormData(event.currentTarget)
signUp({
displayName: formData.get("displayName") as string,
email: formData.get("email") as string,
password: formData.get("password") as string,
confirmPassword: formData.get("confirmPassword") as string,
})
}
return (
<form onSubmit={handleSubmit}>
<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Your Name</FieldLabel>
<Input
name="displayName"
type="text"
placeholder="John Doe"
required
/>
<FieldDescription>
This is how you will be referred to on Drexa. You can
change this later.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
name="email"
type="email"
placeholder="m@example.com"
required
/>
{signUpError instanceof BetterAuthError &&
signUpError.errorCode ===
authClient.$ERROR_CODES.INVALID_EMAIL ? (
<FieldError>Invalid email address</FieldError>
) : null}
</Field>
<Field>
<Field className="grid grid-rows-2 md:grid-rows-1 md:grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="password">Password</FieldLabel>
<Input name="password" type="password" required />
</Field>
<Field>
<FieldLabel htmlFor="confirm-password">
Confirm Password
</FieldLabel>
<Input
name="confirmPassword"
type="password"
required
/>
</Field>
</Field>
{signUpError instanceof PasswordMismatchError ? (
<FieldError>Passwords do not match</FieldError>
) : (
<FieldDescription>
Must be at least 8 characters long.
</FieldDescription>
)}
</Field>
<Field>
<Button
type="submit"
loading={isPending}
disabled={isPending}
>
{isPending ? "Creating account…" : "Create Account"}
</Button>
<FieldDescription className="text-center">
Already have an account? <a href="#">Sign in</a>
</FieldDescription>
</Field>
</FieldGroup>
</form>
)
}