【Next.js】ユーザーの登録とログイン画面を実装してみる（MySQL使用）

# サンプルイメージ


:::note info
おしらせ
※必要最低限のUIコンポーネントで作っていますで、画面UIがしょぼいですがご容赦ください。
:::

<b>【手順】</b>
登録画面で①Emailと②パスワードを入力して登録ボタンをクリックする。

![登録1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/ad997689-49c4-6d94-3ae7-25db7b0b34c5.png)

登録完了後、ログイン画面にて①Emailと②パスワードを入力してログインボタンをクリックする。
![ログイン画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/75666995-0d77-b26e-10e3-a5a227e5060d.png)

ログインに成功したら、自動でダッシュボードに遷移する。
![ログイン後のページ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/c86d99d7-b20a-2771-3c37-a7a0cce6bd8b.png)

↑のような画面イメージになっています。



## 使用した技術スタック
|ソフトウェア|バージョン|
|-----------|---------|
|Next.js    |15.0.3   |
|Typescript |5以上    |
|MySQLモジュール|3.11.4|
|Prisma     |5.22.0   |
|bcrypt     |2.4.3    |
|Daisy UI   |4.12.14  |
|Tailwind CSS|3.4.1  |

# アプリのディレクトリ構造

今回ソースコードをいじるのは、下記です。
①app/authフォルダ配下のファイル群、
②libフォルダ配下のファイル群、
③pages/apiフォルダ配下のファイル群、
④globals.css、⑤prismaフォルダのファイル、
⑤tailwind.cssです。

```ディレクトリ構造
.
├── .next/
│   ├── cache
│   ├── server
│   ├── static
│   ├── types
│   ├── {}app-build-manifest.json
│   ├── {}build-manifest.json
│   ├── {}package.json
│   └── {}react-loadable-manifest.json
├── app/
│   ├── auth/
│   │   ├── login/
│   │   │   ├── page.tsx
│   │   │   └── dashboard/
│   │   │       └── page.tsx
│   │   └── register/
│   │       └── page.tsx
│   ├── fonts
│   ├── lib/
│   │   ├── db.ts
│   │   └── prisma.ts
│   ├── pages/
│   │   └── api/
│   │       ├── login.ts
│   │       └── register.ts
│   ├── globals.css　※←アプリ全体のCSS設計時に使用するファイル
│   ├── layout.tsx
│   ├── page.tsx　※←トップページのファイル
│   ├── node-module
│   └── prisma/
│       └── schema.prisma
├── next.config.ts/
│   ├── package.json
│   └── package-lock.json
└── tailwind.config.ts/
```

# Tailwind.css「Daisy UI」のインストール

プロジェクトにディレクトリ移動してDaisy UIを下記のコメントでインストールしましょう。

```
npm i -D daisyui@latest
```

https://daisyui.com/docs/install/

インストールが完了したら、「tailwind.config.ts」を開いてアプリ全体でこのコンポーネントを使うように宣言します。

```
plugins: [require('daisyui')],
```

修正後のソース構成はこちらです。↓
```tailwind.config.ts
import type { Config } from "tailwindcss";

export default {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        background: "var(--background)",
        foreground: "var(--foreground)",
      },
    },
  },
  plugins: [require('daisyui')],
} satisfies Config;

```

# MySQLの接続場を.envファイルに追加する
アプリとMySQLを接続するための情報を<b>「.env」ファイル</b>に追加します。
```.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://(ユーザー情報):(パスワード)@localhost:3306/(データベース名)"
```

# O/Rマッパー「Prisma」の構成を修正

データベースにテーブルを作るための構成はこちらになります。

```prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model users {
  id  Int @id @default(autoincrement())
  email  String
  password String
  role Int
}

```

# データベース情報をアプリに読み込ませる
アプリとデータベースを連携させるための設定を追加していきましょう。
MtSQL情報をアプリに追加します。
このgetDB関数は、以降の登録画面やログイン画面で使用するメソッドになります。
```app/lib/db.ts
import mysql from 'mysql2/promise';

export const getDB = async()=>{
    const connection = await mysql.createConnection({
        host:'localhost',
        user:'root',
        password:'pass1234',
        database:'webapp'
    });
    return connection;
};
```

# Prisma Clientをインストールして追加しておく
Prismaをアプリ側でも使えるように設定しておきましょう。

```app/lib/prisma.ts
declare global{
    var prisma:PrismaClient;
}

import {PrismaClient} from '@prisma/client';

let prisma:PrismaClient;

if(process.env.NODE_ENV === 'production'){
    prisma = new PrismaClient();
}else{
    if(!global.prisma){
        global.prisma = new PrismaClient();
    }
    prisma = global.prisma;
}

export default prisma;


```


# 登録画面を作る
:::note warn
<b>はじめの注意書き</b>
Next.jsでは、各Javascriptファイルの先頭行にクライアント側の処理名なのか、それともサーバ側の処理なのかをきちっと明記する必要があります。
なので、クライアント側で使うときは<b>「use client」</b>、サーバ側で使うときは<b>「use server」</b>と書きましょう。
:::

では、いよいよ登録画面を作ってみます。
Emailとパスワードを入力して、登録ボタンをクリックしたら、userRegister関数が起動する仕様になっています。

```app/auth/register/page.tsx
'use client'
import type { NextPage } from "next"
import {any, z} from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from "react-hook-form";
import { useState } from "react";
import userRegister from '@/app/pages/api/register';

export default function LoginForm(){
  const {
    register,
    //handleSubmit,
    formState: { errors },
  } = useForm();
  const [email,setEmail] = useState('');
  const [password,setPassword] = useState('');

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    
    e.preventDefault();

    if(email == ''){
      const errorElement = document.getElementById('errorInputEmail');
           
      //nullチェックを明示的に行う
      if(errorElement&&email === '') {
        errorElement.innerHTML = 'E-mail入力が必須の項目です';  // オプショナルチェイニングを使用
      }
    }

    if(password ==''){
      const errorPassword:any = document.getElementById('errorInputPassword');
      if(errorPassword&&password===''){
        errorPassword.innerHTML = 'パスワード入力が必須の項目です';    
      }
    }

    console.log(email, password);
    userRegister(email,password);
  };

    return (
        <div className="App">
          <h1>登録画面</h1>
          <form onSubmit={handleSubmit}>
            <div>
              <label htmlFor="email">Email</label>
              <input id="email" {...register('email', { required: true })} onChange={e=>setEmail(e.target.value)}/>
              {errors.email && <div>E-mail入力が必須の項目です</div>}
              <div id='errorInputEmail' className="bg-secondary"></div>
            </div>
            <div>
              <label htmlFor="password">Password</label>
              <input id="password" {...register('password',{required:true})} type="password" onChange={e=>setPassword(e.target.value)}/>
              {errors.password && <div>パスワード入力が必須の項目です</div>}
              <div id="errorInputPassword"></div>
            </div>
            <button type="submit" className="btn btn-primary" id="loginBtn">登録</button>
          </form>
        </div>
      );
    }
```

次に、サーバとデータベースのやりとりを指示するソースコードを書いていきましょう。
userRegister関数(下の関数になります)で、パスワードをハッシュ化してINSETクエリを投げて登録します。

```app/pages/api/register.ts
// ユーザー登録時の処理例 (APIルートで実行)
'use server' //追加

import bcrypt from 'bcryptjs';
import { getDB } from '@/app/lib/db';

const registerUser = async (email: string, password: string) => { //export const registerUser = async (email: string, password: string) => {
  const hashedPassword = await bcrypt.hash(password, 10);

  const connection = await getDB();
  await connection.execute(
    'INSERT INTO users (email, password) VALUES (?, ?)',
    [email, hashedPassword]
  );
};

export default async function userRegister(email:string, password: string){ //export const registerUser = async (email: string, password: string) => {
  const hashedPassword = await bcrypt.hash(password, 10);

  const connection = await getDB();
  //一旦デフォルト
  var role =1;
  await connection.execute(
    'INSERT INTO users (email, password,role) VALUES (?, ?,?)',
    [email, hashedPassword,role]
  );
};

```

# ログイン画面を作成する
つづいて、ログイン画面を作成しましょう。
①ログイン画面では、非同期でデータベースからユーザー情報を取得します。
```app/auth/login/page.tsx
    //非同期でデータを取得する
    const user = await loginUser(email,password);
```
②ユーザー情報が取得できた時は「user.length」関数で条件分岐します。
※取得できた場合は、ユーザー情報は配列構造で返却されるためConsole.logで確認してみましょう。
```app/auth/login/page.tsx
      //取得したユーザーのID値を取得する
      console.log("取得したユーザーのIDは、" + user[0].id);
      //取得したユーザーのメールアドレス値を取得する
      console.log("取得したユーザーのメールアドレスは、" + user[0].email);
      //取得したユーザーのIパスワード値を取得する
      console.log("取得したユーザーのパスワードは、" + user[0].password);
```

③取得したユーザ情報を遷移先のページでも使うのでSessionStrrageに保存します。
```app/auth/login/page.tsx
      sessionStorage.setItem("id",user[0].id);
      sessionStorage.setItem("email",user[0].email);
      sessionStorage.setItem("password",user[0].password); 
```

④遷移先のページURLを作成するためのクエリパラメータを作成します。
```app/auth/login/page.tsx
      // クエリパラメータとして渡すためにURLを作成
      const queryString = new URLSearchParams(queryParameter).toString();
```

⑤Router関数を使って、自動でページ戦で着るようにします。

:::note info
今回使用するRouterライブラリについては、下記を使用します。
import { useRouter } from "next/navigation";

:::
詳細はこちらに説明されています。↓

https://qiita.com/taisei_code/items/c2d2c2c0d41d6d808bf4



```app/auth/login/page.tsx
      //auth\login\dashboardへ遷移する 
      router.push("/auth/login/dashboard"); 
```





```app/auth/login/page.tsx
'use client'

import { useForm } from "react-hook-form";
import { useState } from "react";
import loginUser from "@/app/pages/api/login";
//import userRegister from "@/app/pages/api/register";
import { useRouter } from "next/navigation";// useRouterは、next/navigationを使用する
import { cookies } from "next/headers";//ユーザ管理情報のため使用する
import { string } from "zod";


export default function LoginForm(){
  const router = useRouter();

  const {
    register,
    //handleSubmit,
    formState: { errors },
  } = useForm();  

  const [email,setEmail] = useState('');
  const [password,setPassword] = useState('');

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    

    if(email == ''){
      const errorElement = document.getElementById('errorInputEmail');
           
      //nullチェックを明示的に行う
      if(errorElement&&email === '') {
        errorElement.innerHTML = 'E-mail入力が必須の項目です';  // オプショナルチェイニングを使用
      }
    }

    if(password ==''){
      const errorPassword:any = document.getElementById('errorInputPassword');
      if(errorPassword&&password===''){
        errorPassword.innerHTML = 'パスワード入力が必須の項目です';    
      }
    }

    //非同期でデータを取得する
    const user = await loginUser(email,password);
    
    //ユーザー情報が存在する場合
    if(user.length > 0){
      console.log("取得したユーザーは、" + user.length + "件");
      
      //取得したユーザーのID値を取得する
      console.log("取得したユーザーのIDは、" + user[0].id);
      //取得したユーザーのメールアドレス値を取得する
      console.log("取得したユーザーのメールアドレスは、" + user[0].email);
      //取得したユーザーのIパスワード値を取得する
      console.log("取得したユーザーのパスワードは、" + user[0].password);

      const queryParameter = {
        id:user[0].id,
        email:user[0].email,
        password:user[0].password
      };

      sessionStorage.setItem("id",user[0].id);
      sessionStorage.setItem("email",user[0].email);
      sessionStorage.setItem("password",user[0].password);   

      // クエリパラメータとして渡すためにURLを作成
      const queryString = new URLSearchParams(queryParameter).toString();

      //auth\login\dashboardへ遷移する 
      router.push("/auth/login/dashboard");    
      //router.push(`/auth/login/dashboard?${queryString}`);    

    }else{
      console.log("ユーザー情報は存在しません。");
    }              
    
  };

    return (
        <div className="App">
          <h1>ログイン画面</h1>
          <form onSubmit={handleSubmit}>
            <div>
              <label htmlFor="email">Email</label>
              <input id="email" {...register('email', { required: true })} onChange={e=>setEmail(e.target.value)}/>
              {errors.email && <div>E-mail入力が必須の項目です</div>}
              <div id='errorInputEmail' className="bg-secondary"></div>
            </div>
            <div>
              <label htmlFor="password">Password</label>
              <input id="password" {...register('password',{required:true})} type="password" onChange={e=>setPassword(e.target.value)}/>
              {errors.password && <div>パスワード入力が必須の項目です</div>}
              <div id="errorInputPassword"></div>
            </div>
            <button type="submit" className="btn btn-primary" id="loginBtn">ログイン</button>
          </form>
        </div>
      );
    }

```

つぎに、データベースとのやり取りを作っていきます。
①WHERE句にEmailキーワードだけを入力してレコードを取得します。
```app/pages/api/login.ts
      //EmailのみをWhere句に入れてレコード検索する。
      const [rows] = await connection.query( 
        'SELECT * FROM users WHERE email = ?',
        [email]
      );
```
②取得したパスワードの情報を照合していきます。

```app/pages/api/login.ts
      // パスワードの比較
      const isMatch = await bcrypt.compare(password,user[0].password);
      if(!isMatch){
        console.log("パスワードが正しくありません。");
        return null;
      }else{
        //パスワードが正しい場合
        console.log("パスワードは、正しいです。");
      }

```

全体のコードはこちら↓

```app/pages/api/login.ts
'use server' //←忘れない

import { getDB } from "@/app/lib/db";
import { RowDataPacket } from 'mysql2/promise';
import bcrypt from 'bcryptjs';

interface User extends RowDataPacket {
    id: number;
    email: string;
    password: string;
}

export default async function loginUser(email: string, password: string) {
  //パスワードをハッシュ化
  const hashedPassword = await bcrypt.hash(password, 10);
  const connection = await getDB();

  try{
      //EmailのみをWhere句に入れてレコード検索する。
      const [rows] = await connection.query( 
        'SELECT * FROM users WHERE email = ?',
        [email]
      );
 
      console.log(rows);

      //ユーザー情報が存在しない場合
      if([rows].length === 0){
        console.log("ユーザー情報が存在しません。");
        return null;
      }else{
        //ユーザー情報が存在する場合
        console.log("ユーザー情報が存在します。");
      }

      //ユーザー情報を取得する
      const user = [rows][0];
  
      console.log("ユーザーIDは、" + user[0].id);
      console.log("ユーザーのEmailは、" + user[0].email);
      console.log("ユーザーのパスワードは、" + user[0].password);
      console.log("ユーザーの権限は、" + user[0].role);

      // パスワードの比較
      const isMatch = await bcrypt.compare(password,user[0].password);
      if(!isMatch){
        console.log("パスワードが正しくありません。");
        return null;
      }else{
        //パスワードが正しい場合
        console.log("パスワードは、正しいです。");
      }
      //ログイン成功
      console.log('ユーザーのログインは成功です。');
      return user;
  }catch(error){
    console.error('Error during login',error);
    return null;
  }finally{
    // DB接続をクローズ
    await connection.end();
  }
  
}


```
さいごに、ログイン成功のだっしょボード画面を作っていきます。
SessionStrageでセットした値をgetItem関数で取得して画面に表示させます。

```app/auth/login/dashboard/page.tsx
'use client'//←忘れずに記載すること

import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";

export default function dashboard(){
    const router = useRouter();
    const userId = sessionStorage.getItem("id");
    const userEmail = sessionStorage.getItem("email");
    const userPassword = sessionStorage.getItem("password");
    
    return(
        <div>
            ログイン後のページ
            <div>ユーザーID：{userId}</div>
            <div>ユーザーEmail：{userEmail}</div>
            <div>ユーザーパスワード：{userPassword}</div>
        </div>
    );
}

```

このようなかんじで、ログインユーザごとにページの表示の制御のための土台を作ることができました。

# おまけ
ディレクトリ構造をコマンドで作成するためのおすすめツール
[tree.nathanfriend.com](https://tree.nathanfriend.com/)

https://tree.nathanfriend.com/

サクサク作れるのでお勧めです。コピペしてQIIta記事にも晴れるのがGOOD！！


![tree.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/c61a8fd9-fb90-96c2-562d-5d4a31b95f23.png)


# スペシャルサンクス

お世話になったサイトたち

https://qiita.com/taisei_code/items/c2d2c2c0d41d6d808bf4

https://b-risk.jp/blog/2021/07/sessionstorage/


https://zenn.dev/pk_yakkun/articles/39dbcac61d569b


https://qiita.com/RinTenma/items/9ec06ee560ab09ddb1ae

https://ramble.impl.co.jp/847/

https://nakanishi-s.jp/2989-2/