Next13でNextAuthを使いつつログインフォームを作ってみた
今回やること
前回Next13の新機能ServerActionsを使ってユーザー登録を作ったので、ログインフォームを作っていく。
Nextで実装できる箇所についてはNext側で実装していくためNextAuthはログインでの認証のみ実装。
作ってみた
流れと実装方法
実装の流れ
- NextAuthでログインの処理
- ログインしているかどうかでヘッダーの内容を変更
- セッションからユーザーのIDを取得しServer側でユーザーデータを取得する
本編とは逸れるのでここに記載するが今回からNextUIとReactIconsを導入。
導入方法は割愛するので各ドキュメント等参照。
NextAuthの準備
NextAuthについて。
認証機能をNextに実装できるライブラリ。
インストール
まずはドキュメントに合わせてインストールしていく。
pnpm add next-auth
NextAuthで指定するoptionを作成
認証でどんな事するかの設定を書いていく。
今回emailとpasswordでの認証を行うのでcredentials
を使用。
import { PrismaAdapter } from '@next-auth/prisma-adapter'
import { compareSync } from 'bcrypt'
import type { NextAuthOptions } from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import prisma from '~/lib/prisma'
export const authOptions: NextAuthOptions = {
session: {
strategy: 'jwt',
},
adapter: PrismaAdapter(prisma),
providers: [
CredentialsProvider({
name: 'Signin',
credentials: {
email: {
label: 'Email',
type: 'email',
placeholder: 'example@example.com',
},
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null
const user = await prisma.user.findUnique({
where: {
email: credentials?.email,
},
})
if (!user) return null
const isValidPassword = compareSync(credentials?.password, user.password)
if (!isValidPassword) return null
console.log('last')
return user
},
}),
],
callbacks: {
jwt: async ({ token, user, account }) => {
if (user) {
token.user = user
token.id = user.id
}
if (account) {
token.accessToken = account.access_token
}
return token
},
session: ({ session, token }) => {
return {
...session,
user: {
...session.user,
id: token.id,
},
}
},
},
}
- セッションからユーザーのIDを取得しServer側でユーザーデータを取得する
上記の目的のためidがほしいのでcallbacks
でsessionにidを突っ込む処理を追加。
あとはprismaでの認証の流れを記述して完了とする。
API作成
If you're using Next.js 13.2 or above with the new App Router (app/), you can initialize the configuration using the new Route Handlers by following our guide.
App directoryでの作成になるので公式ドキュメントで言及されている通りapp/api/auth/[...nextauth]/route.ts
に作成。
import NextAuth from 'next-auth/next'
import { authOptions } from '~/lib/auth'
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
providerを挿入
useSession
を使用するためSessionProvider
をlayoutに記述する。
'use client'
import './global.css'
import { SessionProvider } from 'next-auth/react'
import { ReactNode } from 'react'
import Header from '~/features/AppHeader'
import NextuiProviders from '~/providers/nextui'
const RootLayout = ({ children }: { children: ReactNode }) => {
return (
<html lang='ja'>
<body className='min-h-screen w-screen'>
<NextuiProviders>
<SessionProvider>
<Header />
<main className='h-screen w-full bg-white pt-[56px] text-zinc-900'>{children}</main>
</SessionProvider>
</NextuiProviders>
</body>
</html>
)
}
export default RootLayout
これでNextAuthの設定は一通り終了。
ログインフォーム作成
ログインページ
'use client'
import { Button } from '@nextui-org/react'
import { NextPage } from 'next'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useSession } from 'next-auth/react'
import LoginForm from '~/features/user/LoginForm'
const LoginPage: NextPage = () => {
const { data } = useSession()
const router = useRouter()
if (data) router.push('/account')
return (
<div className='p-4'>
<LoginForm />
<div className='mt-8 flex items-center justify-center gap-4'>
<p>登録してない方は</p>
<Link href='/signup'>
<Button radius='sm' className='bg-rose-400 font-bold'>
Signup
</Button>
</Link>
</div>
</div>
)
}
export default LoginPage
フォームの部分
'use client'
import { yupResolver } from '@hookform/resolvers/yup'
import { Button, Input } from '@nextui-org/react'
import { useRouter } from 'next/navigation'
import { signIn } from 'next-auth/react'
import { FC } from 'react'
import { useForm } from 'react-hook-form'
import { loginSchema } from '~/schemas/user'
const LoginForm: FC = () => {
const router = useRouter()
const {
register,
handleSubmit,
setError,
formState: { errors },
} = useForm({
mode: 'onSubmit',
resolver: yupResolver(loginSchema),
})
const onSubmit = handleSubmit((data) => {
signIn('credentials', {
redirect: false,
...data,
})
.then((res) => {
if (res?.error) {
setError('email', { type: 'login' })
setError('password', { type: 'login', message: 'emailかpassworが違います' })
return
}
router.push('/account')
})
.catch((err) => {
console.error(err)
})
})
return (
<form className='mx-auto flex max-w-md flex-col gap-4' onSubmit={onSubmit}>
<section className='flex flex-col'>
<Input
{...register('email')}
label={'email'}
isInvalid={!!errors.email}
className='max-w-md'
/>
{errors.email && <span className='text-xs text-red-500'>{errors.email.message}</span>}
</section>
<section className='flex flex-col'>
<Input
{...register('password')}
label={'password'}
isInvalid={!!errors.password}
className='max-w-md'
/>
{errors.password && <span className='text-xs text-red-500'>{errors.password.message}</span>}
</section>
<div className='flex justify-center'>
<Button type='submit' radius='sm' className='bg-amber-500 font-bold'>
Login
</Button>
</div>
</form>
)
}
export default LoginForm
ログイン処理
データを入力してsubmitしたときにNextAuthのsignIn
関数を使用。
問題なければaccountに遷移する。
ここで注意したいのがsignIn
はサーバー側でできないこと。
公式にも書いてあるので処理はクライアント側でやるように注意する。(前回の流れでサーバー側でやろうとして20分悩んだ)
Client Side: Yes
Server Side: No
エラーの処理も.then
の方に書いているが、認証の処理の中でnullを返しているとres.error
で返ってくるのでこのような実装になった。
認証でエラーがある場合react-hook-formでsetErrorする。
const onSubmit = handleSubmit((data) => {
signIn('credentials', {
redirect: false,
...data,
})
.then((res) => {
if (res?.error) {
setError('email', { type: 'login' })
setError('password', { type: 'login', message: 'emailかpassworが違います' })
return
}
router.push('/account')
})
.catch((err) => {
console.error(err)
})
})
エラー時。
これでログインフォーム完成。
Headerについて
現状ログイン機能を実装したのでログインしているかしていないかで、アイコンの出し分けを作る。
といってもセッションの有無で表示を切り替えるだけの簡単な実装。
ログイン前(トップページ)
ログイン後(トップページ)
'use client'
import Link from 'next/link'
import { useSession } from 'next-auth/react'
import { BiLogIn, BiUser } from 'react-icons/bi'
const Header: React.FC = () => {
const { data } = useSession()
return (
<header className='fixed top-0 flex w-full items-center justify-between px-4 py-3 shadow-md'>
<div className='text-2xl font-bold text-amber-600'>
<Link href='/'>App</Link>
</div>
<div className='text-2xl text-amber-600'>
{!data && (
<Link href='/login' className=''>
<BiLogIn />
</Link>
)}
{data && (
<Link href='/account'>
<BiUser />
</Link>
)}
</div>
</header>
)
}
export default Header
app直下のlayoutでheaderを呼び出す。
アカウントのページ
ひとまずアカウントのデータを表示させるだけのページ + ログアウトボタン。
sessionからidを取得してprismaでuser情報を取得する。
(hoge)/hoge/page.tsx
などのルーティングについてはNext13のRoute Groupsを参照。
import { NextPage } from 'next'
import { getServerSession } from 'next-auth'
import UserProfile, { User } from '~/features/user/component/UserProfile'
import { authOptions } from '~/lib/auth'
const Userpage: NextPage = async () => {
const session = await getServerSession(authOptions)
const user = await prisma.user.findUnique({
where: {
id: (session?.user as User)?.id,
},
})
if (!user) return null
return (
<div className='mt-8'>
<h1 className='text-center text-xl'>Welcome</h1>
<div className='mt-4 flex justify-center'>
<UserProfile user={{ ...user }} />
</div>
</div>
)
}
export default Userpage
'use client'
import { Button } from '@nextui-org/react'
import { signOut } from 'next-auth/react'
import { FC } from 'react'
export type User = {
id: string
name: string
email: string
}
type Props = {
user: User
}
const UserProfile: FC<Props> = ({ user }) => {
return (
<div>
<p className='text-center'>{user.id}</p>
<p className='text-center'>{user.name}</p>
<p className='text-center'>{user.email}</p>
<div className='mt-16 flex justify-center'>
<Button onClick={() => signOut()} className='mx-auto bg-sky-600 font-bold text-white'>
Logout
</Button>
</div>
</div>
)
}
export default UserProfile
レイアウト
(auth)下のページはセッションがない場合にloginページに遷移させたいのでレイアウトに記述。
Route GroupsができてAuthなどのレイアウト作成は直感的にわかりやすくなった。
import { redirect } from 'next/navigation'
import { getServerSession } from 'next-auth'
import { ReactNode } from 'react'
import { authOptions } from '~/lib/auth'
const AuthLayout = async ({ children }: { children: ReactNode }) => {
const session = await getServerSession(authOptions)
if (!session) redirect('/login')
return <>{children}</>
}
export default AuthLayout
これで実装したい機能を一通り実装。
やってみて
今回始めてNextAuthを使ったが、最低限の実装はできた。
セッションの取得も楽なので、セッションの有無でヘッダーなどのコンテンツを切り替える場合はかなり簡単に実装できる。
今回は実装しなかったがリフレッシュトークンつけたい場合など公式ドキュメントになにかと載っているし、NextAuthを使っている先駆者がたくさんいるので記事を漁ってみるのもいいと思う。
ただ、Next13以前の実装方法だとserver・clientの違いで手こずる部分があるかもしれないので、そのときは大人しくドキュメントを見た方がいい(Next13での変更は大体載っている)。
ドキュメントを見ていてもまだ触っていない部分がたくさんあるので、NextAuthをメインで使っていくようになったらまた深く掘り下げたいと思う。
今回は型指定まで掘り下げる時間がなく禁断のas
を使用した箇所もあるので、次使用するときは型に重きをおいてしっかり作りたい。
Discussion