Skip to content

Commit

Permalink
feat: migrate to better-auth (#1036)
Browse files Browse the repository at this point in the history
Migrate to better-auth
  • Loading branch information
tszhong0411 authored Mar 1, 2025
2 parents 4c34c6f + cc20c9c commit d1cdef4
Show file tree
Hide file tree
Showing 52 changed files with 1,125 additions and 1,435 deletions.
7 changes: 3 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ WAKATIME_API_KEY=

# ---------------------------------------------------------------------------------------------------------
# Authentication
# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
# @see https://next-auth.js.org/getting-started/example
# @see https://www.better-auth.com/docs/installation
# ---------------------------------------------------------------------------------------------------------
AUTH_SECRET=
AUTH_TRUST_HOST="true"
BETTER_AUTH_SECRET=
BETTER_AUTH_URL="http://localhost:3000"

# Google OAuth
GOOGLE_CLIENT_ID=
Expand Down
4 changes: 2 additions & 2 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ runs:
RESEND_API_KEY=re_fake_api_key
UPSTASH_REDIS_REST_URL=http://127.0.0.1:8079
UPSTASH_REDIS_REST_TOKEN=honghongme
AUTH_SECRET=honghongme
AUTH_TRUST_HOST=true
BETTER_AUTH_SECRET=1234567890
BETTER_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_FLAG_COMMENT=true
NEXT_PUBLIC_FLAG_AUTH=true
NEXT_PUBLIC_FLAG_STATS=true
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ cd honghong.me
cp .env.example .env.local
```

4. Fill in the NextAuth secret:
4. Fill in the Better Auth secret:

```properties
# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
AUTH_SECRET=""
# https://www.better-auth.com/docs/installation
BETTER_AUTH_SECRET=""
```

5. Install the dependencies:
Expand Down
4 changes: 2 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
"with-env": "dotenv -e ../../.env.local --"
},
"dependencies": {
"@auth/drizzle-adapter": "1.7.2",
"@hookform/resolvers": "^4.1.2",
"@icons-pack/react-simple-icons": "12.0.0",
"@number-flow/react": "^0.5.5",
Expand All @@ -40,6 +39,8 @@
"@tszhong0411/ui": "workspace:*",
"@tszhong0411/utils": "workspace:*",
"@vercel/speed-insights": "^1.2.0",
"better-auth": "^1.2.0",
"better-call": "^1.0.3",
"canvas-confetti": "^1.9.3",
"class-variance-authority": "^0.7.1",
"cobe": "^0.6.3",
Expand All @@ -51,7 +52,6 @@
"markdown-to-jsx": "^7.7.4",
"motion": "^12.4.7",
"next": "^15.2.0",
"next-auth": "5.0.0-beta.25",
"next-intl": "^3.26.5",
"next-themes": "^0.4.4",
"nuqs": "^2.4.0",
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/app/[locale]/(admin)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { SidebarProvider } from '@tszhong0411/ui'

import AdminHeader from '@/components/admin/admin-header'
import AdminSidebar from '@/components/admin/admin-sidebar'
import { getCurrentUser } from '@/lib/auth'
import { getSession } from '@/lib/auth'

type LayoutProps = {
params: Promise<{
Expand All @@ -16,9 +16,9 @@ type LayoutProps = {
const Layout = async (props: LayoutProps) => {
const { children } = props
const { locale } = await props.params
const session = await getCurrentUser()
const session = await getSession()

if (!session || session.role !== 'admin') {
if (!session || session.user.role !== 'admin') {
redirect({
href: '/',
locale
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import type { TOC } from '@tszhong0411/mdx-plugins'

import { useTranslations } from '@tszhong0411/i18n/client'
import { useRouter } from '@tszhong0411/i18n/routing'
import { Button, Popover, PopoverContent, PopoverTrigger } from '@tszhong0411/ui'
import { AlignLeftIcon } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useState } from 'react'

import Link from '@/components/link'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import type { TOC } from '@tszhong0411/mdx-plugins'

import { useTranslations } from '@tszhong0411/i18n/client'
import { useRouter } from '@tszhong0411/i18n/routing'
import { SegmentGroup, SegmentGroupItem } from '@tszhong0411/ui'
import { useRouter } from 'next/navigation'

import { useScrollspy } from '@/hooks/use-scrollspy'

Expand Down
30 changes: 25 additions & 5 deletions apps/web/src/app/[locale]/(main)/guestbook/message-box.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
'use client'

import type { User } from '@/lib/auth'

import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslations } from '@tszhong0411/i18n/client'
import { useRouter } from '@tszhong0411/i18n/routing'
import {
Avatar,
AvatarFallback,
Expand All @@ -18,11 +17,12 @@ import {
Textarea,
toast
} from '@tszhong0411/ui'
import { signOut } from 'next-auth/react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'

import { signOut, type User } from '@/lib/auth-client'
import { api } from '@/trpc/react'
import { getDefaultImage } from '@/utils/get-default-image'

type FormProps = {
user: User
Expand All @@ -32,6 +32,7 @@ const MessageBox = (props: FormProps) => {
const { user } = props
const utils = api.useUtils()
const t = useTranslations()
const router = useRouter()

const guestbookFormSchema = z.object({
message: z.string().min(1, {
Expand Down Expand Up @@ -61,10 +62,18 @@ const MessageBox = (props: FormProps) => {
})
}

const defaultImage = getDefaultImage(user.id)

return (
<div className='flex gap-3'>
<Avatar>
<AvatarImage src={user.image} width={40} height={40} alt={user.name} className='size-10' />
<AvatarImage
src={user.image ?? defaultImage}
width={40}
height={40}
alt={user.name}
className='size-10'
/>
<AvatarFallback className='bg-transparent'>
<Skeleton className='size-10 rounded-full' />
</AvatarFallback>
Expand All @@ -84,7 +93,18 @@ const MessageBox = (props: FormProps) => {
)}
/>
<div className='mt-4 flex justify-end gap-2'>
<Button variant='outline' onClick={() => void signOut()}>
<Button
variant='outline'
onClick={async () => {
await signOut({
fetchOptions: {
onSuccess: () => {
router.refresh()
}
}
})
}}
>
{t('common.sign-out')}
</Button>
<Button
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/app/[locale]/(main)/guestbook/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import type { GetInfiniteMessagesOutput } from '@/trpc/routers/guestbook'
import { keepPreviousData } from '@tanstack/react-query'
import { useTranslations } from '@tszhong0411/i18n/client'
import { Avatar, AvatarFallback, AvatarImage, Skeleton } from '@tszhong0411/ui'
import { useSession } from 'next-auth/react'
import { useEffect, useMemo } from 'react'
import { useInView } from 'react-intersection-observer'

import { type MessageContext, MessageProvider } from '@/contexts/message'
import { useFormattedDate } from '@/hooks/use-formatted-date'
import { useSession } from '@/lib/auth-client'
import { api } from '@/trpc/react'

import DeleteButton from './delete-button'
Expand Down Expand Up @@ -89,7 +89,7 @@ const Messages = () => {

const Message = (props: MessageProps) => {
const { message } = props
const { data } = useSession()
const { data: session } = useSession()

const {
message: {
Expand All @@ -107,7 +107,7 @@ const Message = (props: MessageProps) => {
[message]
)

const isAuthor = data?.user && userId === data.user.id
const isAuthor = session?.user && userId === session.user.id

return (
<MessageProvider value={context}>
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/app/[locale]/(main)/guestbook/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { i18n } from '@tszhong0411/i18n/config'
import { getTranslations, setRequestLocale } from '@tszhong0411/i18n/server'

import PageTitle from '@/components/page-title'
import { getCurrentUser } from '@/lib/auth'
import { getSession } from '@/lib/auth'
import { SITE_URL } from '@/lib/constants'
import { getLocalizedPath } from '@/utils/get-localized-path'

Expand Down Expand Up @@ -63,7 +63,7 @@ const Page = async (props: PageProps) => {

const { locale } = await props.params
setRequestLocale(locale)
const user = await getCurrentUser()
const session = await getSession()
const t = await getTranslations()
const title = t('guestbook.title')
const description = t('guestbook.description')
Expand All @@ -90,7 +90,7 @@ const Page = async (props: PageProps) => {
<PageTitle title={title} description={description} />
<div className='mx-auto max-w-xl space-y-10'>
<Pinned />
{user ? <MessageBox user={user} /> : <SignIn />}
{session ? <MessageBox user={session.user} /> : <SignIn />}
<Messages />
</div>
</>
Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import { cn } from '@tszhong0411/utils'
import { SpeedInsights } from '@vercel/speed-insights/next'
import { GeistMono } from 'geist/font/mono'
import { GeistSans } from 'geist/font/sans'
import Script from 'next/script'
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import { Monitoring } from 'react-scan/monitoring/next'

import Analytics from '@/components/analytics'
import Hello from '@/components/hello'
import ReactScan from '@/components/react-scan'
import SignInDialog from '@/components/sign-in-dialog'
import { SITE_KEYWORDS, SITE_NAME, SITE_URL } from '@/lib/constants'

Expand Down Expand Up @@ -139,7 +139,11 @@ const Layout = async (props: LayoutProps) => {
className={cn(GeistSans.variable, GeistMono.variable)}
suppressHydrationWarning
>
<ReactScan />
<head>
{env.NODE_ENV === 'development' ? (
<Script src='https://unpkg.com/react-scan/dist/auto.global.js' />
) : null}
</head>
<body className='relative flex min-h-screen flex-col'>
{env.REACT_SCAN_MONITOR_API_KEY ? (
<Monitoring
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/app/api/auth/[...all]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { toNextJsHandler } from 'better-auth/next-js'

import { auth } from '@/lib/auth'

export const { POST, GET } = toNextJsHandler(auth)
1 change: 0 additions & 1 deletion apps/web/src/app/api/auth/[...nextauth]/route.ts

This file was deleted.

23 changes: 10 additions & 13 deletions apps/web/src/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client'

import { Toaster, TooltipProvider } from '@tszhong0411/ui'
import { SessionProvider } from 'next-auth/react'
import { ThemeProvider } from 'next-themes'

import { TRPCReactProvider } from '@/trpc/react'
Expand All @@ -22,18 +21,16 @@ const Providers = (props: ProvidesProps) => {
enableColorScheme
disableTransitionOnChange
>
<SessionProvider>
<TooltipProvider>
{children}
<Toaster
toastOptions={{
duration: 2500
}}
visibleToasts={5}
expand
/>
</TooltipProvider>
</SessionProvider>
<TooltipProvider>
{children}
<Toaster
toastOptions={{
duration: 2500
}}
visibleToasts={5}
expand
/>
</TooltipProvider>
</ThemeProvider>
</TRPCReactProvider>
)
Expand Down
16 changes: 8 additions & 8 deletions apps/web/src/components/admin/admin-profile-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,39 @@ import {
DropdownMenuTrigger,
Skeleton
} from '@tszhong0411/ui'
import { useSession } from 'next-auth/react'

import { useSession } from '@/lib/auth-client'
import { useDialogsStore } from '@/store/dialogs'
import { getAvatarAbbreviation } from '@/utils/get-avatar-abbreviation'
import { getDefaultUser } from '@/utils/get-default-user'
import { getDefaultImage } from '@/utils/get-default-image'

const AdminProfileDropdown = () => {
const { data, status } = useSession()
const { data: session, isPending } = useSession()
const t = useTranslations()
const { setIsSignInOpen } = useDialogsStore()

if (status === 'loading') {
if (isPending) {
return <Skeleton className='size-9 rounded-full' />
}

if (!data) {
if (!session) {
return (
<Button size='sm' onClick={() => setIsSignInOpen(true)}>
{t('common.sign-in')}
</Button>
)
}

const { id, image, name, email } = data.user
const { defaultImage, defaultName } = getDefaultUser(id)
const { id, image, name, email } = session.user
const defaultImage = getDefaultImage(id)

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className='size-9 rounded-full' variant='ghost'>
<Avatar className='size-9'>
<AvatarImage className='size-9' src={image ?? defaultImage} />
<AvatarFallback>{getAvatarAbbreviation(name ?? defaultName)}</AvatarFallback>
<AvatarFallback>{getAvatarAbbreviation(name)}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
Expand Down
Loading

0 comments on commit d1cdef4

Please sign in to comment.