実現したいこと
- ローカル環境でMySQLのDBを用意して、NextAuthのライブラリを使ってユーザーのログイン・ログアウトを実現する
- ログイン済みユーザー・未ログインユーザーで表示させる画面を制御する
実装手順
1. プロジェクト作成
2. ローカル環境でMySQLを起動
3. Prismaを利用するための設定
4. PrismaDBのスキーマを定義してmigrate
5. NextAuthを利用するための設定
6. NextAuthを使ったログイン処理
7. seedデータを用意してDBに登録
8. ログインUIの作成
9. 新規登録のAPI登録
プロジェクト作成
ターミナル
// study-for-prisma という名前でNext13のプロジェクトを作成
$ mkdir study-for-prisma
$ cd study-for-prisma
$ npx create-next-app@13 . --typescript --tailwind --eslint
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
✔ What import alias would you like configured? … @/*
ターミナル
// Prismaを利用する上で必要なパッケージと設定
$ npm install typescript ts-node @types/node --save-dev
$ npm install prisma --save-dev
$ npm install @prisma/client
$ npx prisma init --datasource-provider mysql
next.config.js
// serverActionの機能を有効にする設定を加える
/** @type {import('next').NextConfig} */
const nextConfig = {
// ↓を追加
experimental: {
serverActions: true,
},
}
module.exports = nextConfig
ローカル環境でMySQLを起動
- Dockerを使ってローカル環境でMySQLを起動する
- Nextのプロジェクト内にdocker-compose.yamlファイルを作成してコンテナを立ち上げる
- MacのM1チップを使っている場合は
platform: linux/amd64
を指定しないと立ち上がらないので要注意
docker-compose.yaml
version: "3.9"
services:
db:
image: mysql:8.0
platform: linux/amd64
ports:
- 3306:3306
volumes:
- mysql:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=study-for-prisma
volumes:
mysql:
ターミナル
// コンテナ起動
$ docker-compose up -d
Prismaを利用するための設定
DATABASE_URL=mysql://root:設定したパスワード@localhost:3306/DB名
で設定する
.env
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL=mysql://root:password@localhost:3306/study-for-prisma
- プロジェクトのsrcディレクトリ配下に
lib
ディレクトリを作成してdb.ts
ファイルを作って下記を記述 -
npm run dev
コマンドで開発環境を立ち上げた場合、実行時にNode.jsキャッシュをクリアされ、データベースへの接続を作成するホットリロードにより毎回新しいPrismaClientインスタンスを初期化される - これにより各PrismaClientインスタンスが独自の接続プールを保持するため、データベース接続をすぐに使い果たす可能性がある
- 単一のインスタンスPrismaClientをインスタンス化し、globalThisオブジェクトに保存する処理を行うことで上記を回避する
- PrismaClientがglobalThisオブジェクトにない場合のみインスタンス化するチェックを行い、そうでない場合は同じインスタンスを再度使用し、余分なPrismaClientインスタンスのインスタンス化を防ぐ
/lib/db.ts
import { PrismaClient } from '@prisma/client'
const prismaClientSingleton = () => {
return new PrismaClient()
}
type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClientSingleton | undefined
}
export const prisma = globalForPrisma.prisma ?? prismaClientSingleton()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
PrismaDBのスキーマを定義してmigrate
prismaをインストールすると自動的に作られる prisma/schema.prisma
のファイルにスキーマを記述していく
prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
// ユーザーModelを定義し、Schemaを記述
model User {
id Int @id @default(autoincrement())
email String @unique
password String
name String?
}
ターミナル
// initというマイグレートファイル名で上記のSchemaの内容をマイグレート
$ npx prisma migrate dev --name init
NextAuthを利用するための設定
ターミナル
$ npm install next-auth
// パスワードのハッシュ化や検証をしてくれるライブラリをインストール
$ npm install bcrypt
$ npm install @types/bcrypt --save-dev
NextAuthを使ったログイン処理
- Next13の
App Router
を使ったプロジェクトのためRoute Handlers
の記述に従って設定する - src/app/api/auth/[...nextauth] ディレクトリを作成して、
route.ts
ファイルを用意して記述する
/app/api/auth/[...nextauth]/route.ts
import NextAuth, { type NextAuthOptions } from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import { compare } from 'bcrypt'
import { prisma } from '@/lib/db'
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
// サインインフォームに表示する名前を指定します(/api/auth/signinで使用されますが、今回は独自のサインインページを作成しているため、こちらの設定は使用されません)
name: 'Sign in',
// credentialsはサインインページにフォームを生成するために使用されます
// credentialsオブジェクトにキーを追加することで、送信するフィールドを指定できます。
credentials: {
email: {
label: 'Email',
type: 'text',
placeholder: 'test@example.com',
},
password: { label: 'Password', type: 'password' },
},
// authorizeメソッドは、サインインフォームから送信された認証情報を受け取ります。
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null // 認証情報がない場合はnullを返すことで、認証失敗を表現します。
}
// prismaを使用して、ユーザーが存在するか検証します。
const user = await prisma.user.findUnique({
where: {
email: credentials.email,
},
})
if (!user) {
return null
}
// bcryptを使用して、パスワードが正しいか検証します。
const isPasswordValid = await compare(
credentials.password,
user.password
)
if (!isPasswordValid) {
return null
}
return {
id: user.id.toString(),
name: user.name,
email: user.email,
}
},
}),
],
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // サーバー側でセッションを保持する時間を秒単位で指定します。
updateAge: 24 * 60 * 60, // セッションの有効期限を更新する頻度を秒単位で指定します。
},
pages: {
signIn: '/signin', // // サインインページのURLを指定します(今回は独自のサインインページを作成しているため、こちらの設定は使用されません)
},
secret: process.env.NEXTAUTH_SECRET,
}
seedデータを用意してDBに登録
- seed.tsにダミーデータを入れる
- package.json に設定を追記しないとエラーになるので注意
package.jsonに追記
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
}
prisma/seed.ts
import { PrismaClient } from '@prisma/client'
import { hash } from 'bcrypt'
const prisma = new PrismaClient()
async function main() {
const password = await hash('test', 12)
const user = await prisma.user.upsert({
where: { email: 'test@test.com' },
update: {},
create: {
email: 'test@test.com',
name: 'Test User',
password,
},
})
console.log({ user })
}
main()
.then(() => prisma.$disconnect())
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
ターミナル
$ npx prisma db seed
// seedデータがDBに反映される
ログインUIの作成
- seedで作成したテストデータの情報を入力してログインボタンを押すとトップページに遷移すればOK
/app/(auth)/login/page.tsx
import { LoginForm } from './_components/login-form'
export default function LoginPage() {
return (
<>
<h1 className="text-2xl mb-8">login</h1>
<LoginForm />
</>
)
}
- NextAuthのデフォルトの設定では認証に失敗するとブラウザをリロードする
- これを回避するに
signIn()
メソッドにredirect: false
を追加する - これによりリダイレクト後の処理を定義して動作させることができる
- これを回避するに
- 認証に成功した場合は
callbackUrl
にリダイレクトされる
/app/(auth)/login/_components/login-form.tsx
'use client'
import React, { FormEvent, useState } from 'react'
import { signIn } from 'next-auth/react'
export const LoginForm = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
try {
const res = await signIn('credentials', {
redirect: false,
email,
password,
callbackUrl: '/',
})
if (!res?.ok) {
setError(`${res?.status}: ${res?.error}`)
}
} catch (error: any) {
setError(error)
}
}
return (
<form className="flex flex-col gap-y-4" onSubmit={handleSubmit}>
<input
type="text"
value={email}
placeholder="email"
className="px-4 py-2 rounded-md text-slate-700"
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
placeholder="password"
className="px-4 py-2 rounded-md text-slate-700"
onChange={(e) => setPassword(e.target.value)}
/>
<button
type="submit"
className="px-4 py-2 rounded-md bg-white text-slate-700"
>
ログイン
</button>
{error && <p className="text-red-500">{error}</p>}
</form>
)
}