サイトアイコン 上尾市のWEBプログラマーによるブログ

「Next.jsでつくるフルスタックアプリ前編」の感想・備忘録2

ログイン機能

スキーマの作成

import mongoose from 'mongoose'

const Schema = mongoose.Schema;
const ItemSchema = new Schema({
  title: String,
  image: String,
  price: String,
  description: String,
  email: String
});

// 追加
const UserSchema = new Schema({
  name: {
    type: String,
    required: true,
    unique: true
  },
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true
  },
});
export const ItemModel = mongoose.models.Item || mongoose.model("Item", ItemSchema);

// 追加
export const UserModel = mongoose.models.Item || mongoose.model("User", UserSchema);

ユーザー登録APIの作成

touch pages/api/user/register.js

import connectDB from '../../../utils/database'
import { UserModel } from "../../../utils/schemaModel";

const registerUser = async (req, res) => {
  try {
    await connectDB();
    await UserModel.create(req.body)
    return res.status(200).json({ msg: '登録しました。' })
  } catch (err) {
    return res.status(400).json({ msg: '失敗しました。' })
  }
}
export default registerUser;

ユーザー登録フォームの作成

touch register.html

<form action="http://localhost:3000/api/user/register" method="POST">
  <p>name: <input type="text" name="name"></p>
  <p>email: <input type="text" name="email"></p>
  <p>password: <input type="text" name="password"></p>
  <p><button type="submit">登録</button></p>
</form>ユーザー登録とログインを追加

ログインAPIの作成

touch pages/api/user/login.js

import connectDB from '../../../utils/database'
import { UserModel } from "../../../utils/schemaModel";

const loginUser = async (req, res) => {
  try {
    await connectDB();
    const savedUser = await UserModel.findOne({email: req.body.email});
    if (savedUser && req.body.password === savedUser.password) {
      return res.status(200).json({ msg: 'ログインに成功しました。' });
    } else {
      return res.status(400).json({ msg: 'ログインに失敗しました。' })
    }
  } catch (err) {
    return res.status(400).json({ msg: 'エラーが発生しました。' })
  }
}
export default loginUser;

ログインフォームの作成

touch login.html

<h2>ログイン</h2>
<form action="http://localhost:3000/api/user/login" method="POST">
  <p>email: <input type="text" name="email"></p>
  <p>password: <input type="text" name="password"></p>
  <p><button type="submit">登録</button></p>
</form>

トークンの発行

import jwt from 'jsonwebtoken' // 追加
import connectDB from '../../../utils/database'
import { UserModel } from "../../../utils/schemaModel";

const loginUser = async (req, res) => {
  try {
    await connectDB();
    const savedUser = await UserModel.findOne({email: req.body.email});
    if (savedUser && req.body.password === savedUser.password) {
      // 追加
      const token = jwt.sign({email: req.body.email}, 'hogehogehoge', {expiresIn: '23h'});
      // tokenを追加
      return res.status(200).json({ msg: 'ログインに成功しました。', token: token });
    } else {
      return res.status(400).json({ msg: 'ログインに失敗しました。' })
    }
  } catch (err) {
    return res.status(400).json({ msg: 'エラーが発生しました。' })
  }
}
export default loginUser;

ログイン状態の判定

touch utils/auth.js
このファイルはリクエストを処理するjsファイルが実行される前にログイン状態を調べるものである。
このようなファイルをミドルウェアと呼ぶ。

import jwt from 'jsonwebtoken'

const middleware = (handler) => {
  return async (req, res) => {
    if (req.method === 'GET') {
      return handler(req, res);
    }
    const token = req.headers.authorization.split(' ')[1]; // headersへのセットは後編の書籍
    if (!token) {
      return res.status(401).json({ msg: 'トークンがありません。' })
    }
    try {
      const decoded = jwt.verify(token, 'hogehogehoge');
      return handler(req, res);
    } catch (err) {
      return res.status(401).json({ msg: 'トークンが正しくありません。' })
    }
  };
};
export default middleware;

ミドルウェアの適用

import connectDB from '../../../utils/database'
import { ItemModel } from "../../../utils/schemaModel";
import auth from "../../../utils/auth"; // 追加

const createItem = async (req, res) => {
  try {
    await connectDB();
    await ItemModel.create(req.body)
    return res.status(200).json({ msg: '作成しました。' })
  } catch (err) {
    return res.status(400).json({ msg: '失敗しました。' })
  }
}
export default auth(createItem); // auth()をコール
import connectDB from '../../../../utils/database'
import { ItemModel } from "../../../../utils/schemaModel";
import auth from "../../../utils/auth"; // 追加

const updateItem = async (req, res) => {
  try {
    await connectDB();
    await ItemModel.updateOne({ _id: req.query.id }, req.body);
    return res.status(200).json({ msg: '更新成功' });
  } catch (err) {
    return res.status(400).json({ msg: '更新失敗' });
  }
}
export default auth(updateItem); // auth()をコール
import connectDB from '../../../../utils/database'
import { ItemModel } from "../../../../utils/schemaModel";
import auth from "../../../utils/auth"; // 追加

const deleteItem = async (req, res) => {
  try {
    await connectDB();
    await ItemModel.deleteOne({_id: req.query.id});
    return res.status(200).json({ msg: '削除成功' });
  } catch (err) {
    return res.status(400).json({ msg: '削除失敗' })
  }
}
export default auth(deleteItem); // auth()をコール

投稿者のみ投稿修正・削除可能にする

ミドルウェアでreq.bodyにemailを追加する。

import jwt from 'jsonwebtoken'

const middleware = (handler) => {
  return async (req, res) => {
    if (req.method === 'GET') {
      return handler(req, res);
    }
    const token = req.headers.authorization.split(' ')[1];
    if (!token) {
      return res.status(401).json({ msg: 'トークンがありません。' })
    }
    try {
      const decoded = jwt.verify(token, 'hogehogehoge');
      req.body.email = decoded.email; // 追加
      return handler(req, res);
    } catch (err) {
      return res.status(401).json({ msg: 'トークンが正しくありません。' })
    }
  };
};
export default middleware;

呼び出し側で投稿者かどうかをチェック。

import connectDB from '../../../../utils/database'
import { ItemModel } from "../../../../utils/schemaModel";
import auth from "../../../utils/auth";

const updateItem = async (req, res) => {
  try {
    await connectDB();
    const singleItem = await ItemModel.findById(req.query.id); // 追加
    if (singleItem.email === req.body.email) { // 追加
      await ItemModel.updateOne({ _id: req.query.id }, req.body);
      return res.status(200).json({ msg: '更新成功' });
    } else {
      throw new Error();
    }
  } catch (err) {
    return res.status(400).json({ msg: '更新失敗' });
  }
}
export default auth(updateItem);
import connectDB from '../../../../utils/database'
import { ItemModel } from "../../../../utils/schemaModel";
import auth from "../../../utils/auth";

const updateItem = async (req, res) => {
  try {
    await connectDB();
    const singleItem = await ItemModel.findById(req.query.id); // 追加
    if (singleItem.email === req.body.email) { // 追加
      await ItemModel.updateOne({ _id: req.query.id }, req.body);
      return res.status(200).json({ msg: '更新成功' });
    } else {
      throw new Error();
    }
  } catch (err) {
    return res.status(400).json({ msg: '更新失敗' });
  }
}
export default auth(updateItem);
モバイルバージョンを終了