1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NextAuthとPrismaを使って認証機能の実装方法

Last updated at Posted at 2023-11-10

実現したいこと

  • ローカル環境で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>
  )
}

新規登録のAPI登録

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?