サンプルイメージ
おしらせ
※必要最低限のUIコンポーネントで作っていますで、画面UIがしょぼいですがご容赦ください。
【手順】
登録画面で①Emailと②パスワードを入力して登録ボタンをクリックする。
登録完了後、ログイン画面にて①Emailと②パスワードを入力してログインボタンをクリックする。
↑のような画面イメージになっています。
使用した技術スタック
ソフトウェア | バージョン |
---|---|
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')],
修正後のソース構成はこちらです。↓
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」ファイルに追加します。
# 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」の構成を修正
データベースにテーブルを作るための構成はこちらになります。
// 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関数は、以降の登録画面やログイン画面で使用するメソッドになります。
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をアプリ側でも使えるように設定しておきましょう。
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関数が起動する仕様になっています。
'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クエリを投げて登録します。
// ユーザー登録時の処理例 (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]
);
};
ログイン画面を作成する
つづいて、ログイン画面を作成しましょう。
①ログイン画面では、非同期でデータベースからユーザー情報を取得します。
//非同期でデータを取得する
const user = await loginUser(email,password);
②ユーザー情報が取得できた時は「user.length」関数で条件分岐します。
※取得できた場合は、ユーザー情報は配列構造で返却されるためConsole.logで確認してみましょう。
//取得したユーザーのID値を取得する
console.log("取得したユーザーのIDは、" + user[0].id);
//取得したユーザーのメールアドレス値を取得する
console.log("取得したユーザーのメールアドレスは、" + user[0].email);
//取得したユーザーのIパスワード値を取得する
console.log("取得したユーザーのパスワードは、" + user[0].password);
③取得したユーザ情報を遷移先のページでも使うのでSessionStrrageに保存します。
sessionStorage.setItem("id",user[0].id);
sessionStorage.setItem("email",user[0].email);
sessionStorage.setItem("password",user[0].password);
④遷移先のページURLを作成するためのクエリパラメータを作成します。
// クエリパラメータとして渡すためにURLを作成
const queryString = new URLSearchParams(queryParameter).toString();
⑤Router関数を使って、自動でページ戦で着るようにします。
今回使用するRouterライブラリについては、下記を使用します。
import { useRouter } from "next/navigation";
詳細はこちらに説明されています。↓
//auth\login\dashboardへ遷移する
router.push("/auth/login/dashboard");
'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キーワードだけを入力してレコードを取得します。
//EmailのみをWhere句に入れてレコード検索する。
const [rows] = await connection.query(
'SELECT * FROM users WHERE email = ?',
[email]
);
②取得したパスワードの情報を照合していきます。
// パスワードの比較
const isMatch = await bcrypt.compare(password,user[0].password);
if(!isMatch){
console.log("パスワードが正しくありません。");
return null;
}else{
//パスワードが正しい場合
console.log("パスワードは、正しいです。");
}
全体のコードはこちら↓
'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関数で取得して画面に表示させます。
'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!!
スペシャルサンクス
お世話になったサイトたち