0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-12-08

サンプルイメージ

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

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

登録1.png

登録完了後、ログイン画面にて①Emailと②パスワードを入力してログインボタンをクリックする。
ログイン画面.png

ログインに成功したら、自動でダッシュボードに遷移する。
ログイン後のページ.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

インストールが完了したら、「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を接続するための情報を「.env」ファイルに追加します。

.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;


登録画面を作る

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

では、いよいよ登録画面を作ってみます。
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関数を使って、自動でページ戦で着るようにします。

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

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

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

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

tree.png

スペシャルサンクス

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

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?